diff --git a/core_search_facets/src/Plugin/facets/query_type/CoreNodeSearchDate.php b/core_search_facets/src/Plugin/facets/query_type/CoreNodeSearchDate.php index eefad40..0fba256 100644 --- a/core_search_facets/src/Plugin/facets/query_type/CoreNodeSearchDate.php +++ b/core_search_facets/src/Plugin/facets/query_type/CoreNodeSearchDate.php @@ -32,7 +32,7 @@ class CoreNodeSearchDate extends QueryTypePluginBase { * {@inheritdoc} */ public function execute() { - /** @var \Drupal\facets\Utility\FacetsDateHandler $date_handler */ + /** @var \Drupal\facets\utility\FacetsDateHandler $date_handler */ $date_handler = \Drupal::getContainer()->get('facets.utility.date_handler'); /** @var \Drupal\core_search_facets\Plugin\CoreSearchFacetSourceInterface $facet_source */ diff --git a/core_search_facets/src/Tests/IntegrationTest.php b/core_search_facets/src/Tests/IntegrationTest.php index 0d71bfa..cb3ba74 100644 --- a/core_search_facets/src/Tests/IntegrationTest.php +++ b/core_search_facets/src/Tests/IntegrationTest.php @@ -8,6 +8,7 @@ namespace Drupal\core_search_facets\Tests; use Drupal\core_search_facets\Tests\WebTestBase as CoreSearchFacetsWebTestBase; +use Drupal\facets\Tests\ExampleContentTrait; /** * Tests the admin UI with the core search facet source. @@ -16,6 +17,8 @@ use Drupal\core_search_facets\Tests\WebTestBase as CoreSearchFacetsWebTestBase; */ class IntegrationTest extends CoreSearchFacetsWebTestBase { + use ExampleContentTrait; + /** * The block entities used by this test. * @@ -102,6 +105,31 @@ class IntegrationTest extends CoreSearchFacetsWebTestBase { } /** + * Tests the date integration. + */ + public function testDate() { + $id = 'tardigrade'; + $name = 'Tardigrade'; + + $this->drupalGet('admin/config/search/facets/add-facet'); + $form_values = [ + 'id' => $id, + 'status' => 1, + 'url_alias' => $id, + 'name' => $name, + 'weight' => 2, + 'facet_source_id' => 'core_node_search:node_search', + 'facet_source_configs[core_node_search:node_search][field_identifier]' => 'created', + ]; + $this->drupalPostForm(NULL, ['facet_source_id' => 'core_node_search:node_search'], $this->t('Configure facet source')); + $this->drupalPostForm(NULL, $form_values, $this->t('Save')); + + $this->createFacetBlock($id); + + $this->drupalGet('search/node', ['query' => ['keys' => 'test']]); + } + + /** * Configures the possibility to show the amount of results for facet blocks. * * @param string $facet_name diff --git a/src/Utility/FacetsDateHandler.php b/src/Utility/FacetsDateHandler.php new file mode 100644 index 0000000..ad6e087 --- /dev/null +++ b/src/Utility/FacetsDateHandler.php @@ -0,0 +1,418 @@ +dateFormatter = $date_formatter; + } + + /** + * Converts dates from Unix timestamps into ISO 8601 format. + * + * @param int $timestamp + * An integer containing the Unix timestamp being converted. + * @param string $gap + * A string containing the gap, see FACETS_DATE_* constants for valid + * values. Defaults to FACETS_DATE_SECOND. + * + * @return string + * A string containing the date in ISO 8601 format. + */ + public function isoDate($timestamp, $gap = 'SECOND') { + switch ($gap) { + case static::FACETS_DATE_SECOND: + $format = static::FACETS_DATE_ISO8601; + break; + + case static::FACETS_DATE_MINUTE: + $format = 'Y-m-d\TH:i:00\Z'; + break; + + case static::FACETS_DATE_HOUR: + $format = 'Y-m-d\TH:00:00\Z'; + break; + + case static::FACETS_DATE_DAY: + $format = 'Y-m-d\T00:00:00\Z'; + break; + + case static::FACETS_DATE_MONTH: + $format = 'Y-m-01\T00:00:00\Z'; + break; + + case static::FACETS_DATE_YEAR: + $format = 'Y-01-01\T00:00:00\Z'; + break; + + default: + $format = static::FACETS_DATE_ISO8601; + break; + } + return gmdate($format, $timestamp); + } + + /** + * Return a date gap one increment smaller than the one passed. + * + * @param string $gap + * A string containing the gap, see FACETS_DATE_* constants for valid + * values. + * @param string $min_gap + * A string containing the the minimum gap that can be returned, defaults to + * FACETS_DATE_SECOND. This is useful for defining the smallest increment + * that can be used in a date drilldown. + * + * @return string + * A string containing the smaller date gap, NULL if there is no smaller gap. + * See FACETS_DATE_* constants for valid values. + */ + public function getNextDateGap($gap, $min_gap = self::FACETS_DATE_SECOND) { + // Array of numbers used to determine whether the next gap is smaller than + // the minimum gap allowed in the drilldown. + $gap_numbers = array( + static::FACETS_DATE_YEAR => 6, + static::FACETS_DATE_MONTH => 5, + static::FACETS_DATE_DAY => 4, + static::FACETS_DATE_HOUR => 3, + static::FACETS_DATE_MINUTE => 2, + static::FACETS_DATE_SECOND => 1, + ); + + // Gets gap numbers for both the gap and minimum gap, checks if the next gap + // is within the limit set by the $min_gap parameter. + $gap_num = isset($gap_numbers[$gap]) ? $gap_numbers[$gap] : 6; + $min_num = isset($gap_numbers[$min_gap]) ? $gap_numbers[$min_gap] : 1; + return ($gap_num > $min_num) ? array_search($gap_num - 1, $gap_numbers) : $min_gap; + } + + /** + * Determines the best search gap to use for an arbitrary date range. + * + * Generally, we use the maximum gap that fits between the start and end date. + * If they are more than a year apart, 1 year; if they are more than a month + * apart, 1 month; etc. + * + * This function uses Unix timestamps for its computation and so is not useful + * for dates outside that range. + * + * @param int $start_time + * A string containing the start date as an ISO date string. + * @param int $end_time + * A string containing the end date as an ISO date string. + * @param string|NULL $min_gap + * (Optional) The minimum gap that should be returned. + * + * @return string + * A string containing the gap, see FACETS_DATE_* constants for valid + * values. Returns FALSE of either of the dates cannot be converted to a + * timestamp. + */ + public function getTimestampGap($start_time, $end_time, $min_gap = NULL) { + $time_diff = $end_time - $start_time; + switch (TRUE) { + // NOTE: 31536000 == 60 * 60 * 24 * 365 + case ($time_diff >= 31536000): + $gap = static::FACETS_DATE_YEAR; + break; + + case ($time_diff >= 86400 * gmdate('t', $start_time)): + $gap = static::FACETS_DATE_MONTH; + break; + + case ($time_diff >= 86400): + $gap = static::FACETS_DATE_DAY; + break; + + case ($time_diff >= 3600): + $gap = static::FACETS_DATE_HOUR; + break; + + case ($time_diff >= 60): + $gap = static::FACETS_DATE_MINUTE; + break; + + default: + $gap = static::FACETS_DATE_SECOND; + break; + } + + // Return the calculated gap if a minimum gap was not passed of the calculated + // gap is a larger interval than the minimum gap. + if (is_null($min_gap) || $this->gapCompare($gap, $min_gap) >= 0) { + return $gap; + } + else { + return $min_gap; + } + } + + /** + * Converts ISO date strings to Unix timestamps, passes values to the + * FACETS_get_timestamp_gap() function to calculate the gap. + * + * @param string $start_date + * A string containing the start date as an ISO date string. + * @param string $end_date + * A string containing the end date as an ISO date string. + * @param string|NULL $min_gap + * (Optional) The minimum gap that should be returned. + * + * @return string + * A string containing the gap, see FACETS_DATE_* constants for valid + * values. Returns FALSE of either of the dates cannot be converted to a + * timestamp. + * + * @see FACETS_get_timestamp_gap() + */ + public function getDateGap($start_date, $end_date, $min_gap = NULL) { + $range = array(strtotime($start_date), strtotime($end_date)); + if (!in_array(FALSE, $range, TRUE)) { + return $this->getTimestampGap($range[0], $range[1], $min_gap); + } + return FALSE; + } + + /** + * Returns a formatted date based on the passed timestamp and gap. + * + * This function assumes that gaps less than one day will be displayed in a + * search context in which a larger containing gap including a day is already + * displayed. So, HOUR, MINUTE, and SECOND gaps only display time information, + * without date. + * + * @param int $timestamp + * An integer containing the Unix timestamp. + * @param string $gap + * A string containing the gap, see FACETS_DATE_* constants for valid + * values, defaults to YEAR. + * + * @return string + * A gap-appropriate display date used in the facet link. + */ + public function formatTimestamp($timestamp, $gap = self::FACETS_DATE_YEAR) { + switch ($gap) { + case static::FACETS_DATE_MONTH: + return $this->dateFormatter->format($timestamp, 'custom', 'F Y', 'UTC'); + + case static::FACETS_DATE_DAY: + return $this->dateFormatter->format($timestamp, 'custom', 'F j, Y', 'UTC'); + + case static::FACETS_DATE_HOUR: + return $this->dateFormatter->format($timestamp, 'custom', 'g A', 'UTC'); + + case static::FACETS_DATE_MINUTE: + return $this->dateFormatter->format($timestamp, 'custom', 'g:i A', 'UTC'); + + case static::FACETS_DATE_SECOND: + return $this->dateFormatter->format($timestamp, 'custom', 'g:i:s A', 'UTC'); + + default: + return $this->dateFormatter->format($timestamp, 'custom', 'Y', 'UTC'); + } + } + + /** + * Returns a formatted date based on the passed ISO date string and gap. + * + * @param $date + * A string containing the date as an ISO date string. + * @param $gap + * A string containing the gap, see FACETS_DATE_* constants for valid + * values, defaults to YEAR. + * @param $callback + * The formatting callback, defaults to "FACETS_format_timestamp". + * + * @return + * A gap-appropriate display date used in the facet link. + * + * @see FACETS_format_timestamp() + */ + public function formatDate($date, $gap = self::FACETS_DATE_YEAR, $callback = 'facets_format_timestamp') { + $timestamp = strtotime($date); + return $callback($timestamp, $gap); + } + + /** + * Returns the next increment from the given ISO date and gap. This function is + * useful for getting the upper limit of a date range from the given start + * date. + * + * @param string $date + * A string containing the date as an ISO date string. + * @param string $gap + * A string containing the gap, see FACETS_DATE_* constants for valid + * values, defaults to YEAR. + * + * @return string + * A string containing the date, FALSE if the passed date could not be parsed. + */ + public function getNextDateIncrement($date, $gap) { + if (preg_match(static::FACETS_REGEX_DATE, $date, $match)) { + + // Increments the timestamp. + switch ($gap) { + case static::FACETS_DATE_MONTH: + $match[2] += 1; + break; + case static::FACETS_DATE_DAY: + $match[3] += 1; + break; + case static::FACETS_DATE_HOUR: + $match[4] += 1; + break; + case static::FACETS_DATE_MINUTE: + $match[5] += 1; + break; + case static::FACETS_DATE_SECOND: + $match[6] += 1; + break; + default: + $match[1] += 1; + break; + } + + // Gets the next increment. + return $this->isoDate( + gmmktime($match[4], $match[5], $match[6], $match[2], $match[3], $match[1]) + ); + } + return FALSE; + } + + /** + * Compares two timestamp gaps. + * + * @param string $gap1 + * @param string $gap2 + * + * @return int + * Returns -1 if gap1 is less than gap2, 1 if gap1 is greater than gap2, and 0 + * if they are equal. + */ + public function gapCompare($gap1, $gap2) { + + $gap_numbers = array( + static::FACETS_DATE_YEAR => 6, + static::FACETS_DATE_MONTH => 5, + static::FACETS_DATE_DAY => 4, + static::FACETS_DATE_HOUR => 3, + static::FACETS_DATE_MINUTE => 2, + static::FACETS_DATE_SECOND => 1, + ); + + $gap1_num = isset($gap_numbers[$gap1]) ? $gap_numbers[$gap1] : 6; + $gap2_num = isset($gap_numbers[$gap2]) ? $gap_numbers[$gap2] : 6; + + if ($gap1_num == $gap2_num) { + return 0; + } + else { + return ($gap1_num < $gap2_num) ? -1 : 1; + } + } + + /** + * Extracts "start" and "end" dates from an active items + * + * @param string $item + * The active item to extract the dates. + * + * @return mixed + * Returns FALSE if no item found and an array with the dates if the dates + * were extracted as expected. + */ + public function extractActiveItems($item) { + $active_item = []; + if (preg_match(static::FACETS_REGEX_DATE_RANGE, $item, $matches)) { + + $active_item['start'] = [ + 'timestamp' => strtotime($matches[1]), + 'iso' => $matches[1], + ]; + + $active_item['end'] = [ + 'timestamp' => strtotime($matches[8]), + 'iso' => $matches[8], + ]; + + return $active_item; + } + return FALSE; + } +} +