diff --git a/core_search_facets/src/Plugin/facets/facet_source/CoreNodeSearchFacetSource.php b/core_search_facets/src/Plugin/facets/facet_source/CoreNodeSearchFacetSource.php index 28411e0..57f8e03 100644 --- a/core_search_facets/src/Plugin/facets/facet_source/CoreNodeSearchFacetSource.php +++ b/core_search_facets/src/Plugin/facets/facet_source/CoreNodeSearchFacetSource.php @@ -153,6 +153,11 @@ class CoreNodeSearchFacetSource extends FacetSourcePluginBase implements CoreSea case 'entity_reference': $query_types['string'] = 'core_node_search_string'; break; + + case 'created': + $query_types['string'] = 'core_node_search_date'; + break; + } return $query_types; @@ -231,6 +236,7 @@ class CoreNodeSearchFacetSource extends FacetSourcePluginBase implements CoreSea 'type' => $this->t('Content Type'), 'uid' => $this->t('Author'), 'langcode' => $this->t('Language'), + 'created' => $this->t('Post date'), ]; } diff --git a/core_search_facets/src/Plugin/facets/query_type/CoreNodeSearchDate.php b/core_search_facets/src/Plugin/facets/query_type/CoreNodeSearchDate.php new file mode 100644 index 0000000..cb8219a --- /dev/null +++ b/core_search_facets/src/Plugin/facets/query_type/CoreNodeSearchDate.php @@ -0,0 +1,196 @@ +get('facets.utility.date_handler'); + + /** @var \Drupal\core_search_facets\Plugin\CoreSearchFacetSourceInterface $facet_source */ + $facet_source = $this->facet->getFacetSource(); + + // Gets the last active date, bails if there isn't one. + $active_items = $this->facet->getActiveItems(); + if (!$active_item = end($active_items)) { + return; + } + + // Gets facet query and this facet's query info. + /** @var \Drupal\core_search_facets\FacetsQuery $facet_query */ + $facet_query = $facet_source->getFacetQueryExtender(); + $query_info = $facet_source->getQueryInfo($this->facet); + $tables_joined = []; + + $active_item = $date_handler->extractActiveItems($active_item); + + foreach ($query_info['fields'] as $field_info) { + + // Adds join to the facet query. + $facet_query->addFacetJoin($query_info, $field_info['table_alias']); + + // Adds adds join to search query, makes sure it is only added once. + if (isset($query_info['joins'][$field_info['table_alias']])) { + if (!isset($tables_joined[$field_info['table_alias']])) { + $tables_joined[$field_info['table_alias']] = TRUE; + $join_info = $query_info['joins'][$field_info['table_alias']]; + $this->query->join($join_info['table'], $join_info['alias'], $join_info['condition']); + } + } + + // Adds field conditions to the facet and search query. + $field = $field_info['table_alias'] . '.' . $field_info['field']; + $this->query->condition($field, $active_item['start']['timestamp'], '>='); + $this->query->condition($field, $active_item['end']['timestamp'], '<'); + $facet_query->condition($field, $active_item['start']['timestamp'], '>='); + $facet_query->condition($field, $active_item['end']['timestamp'], '<'); + } + } + + /** + * {@inheritdoc} + */ + public function build() { + $parent_facet_results = []; + /** @var \Drupal\facets\Utility\FacetsDateHandler $date_handler */ + $date_handler = \Drupal::getContainer()->get('facets.utility.date_handler'); + + // Gets base facet query, adds facet field and filters. + /* @var \Drupal\core_search_facets\Plugin\CoreSearchFacetSourceInterface $facet_source */ + $facet_source = $this->facet->getFacetSource(); + $query_info = $facet_source->getQueryInfo($this->facet); + + /** @var \Drupal\core_search_facets\FacetsQuery $facet_query */ + $facet_query = $facet_source->getFacetQueryExtender(); + $facet_query->addFacetField($query_info); + + foreach ($query_info['joins'] as $table_alias => $join_info) { + $facet_query->addFacetJoin($query_info, $table_alias); + } + + if ($facet_query->getSearchExpression()) { + // Executes query, iterates over results. + $result = $facet_query->execute(); + + foreach ($result as $record) { + $raw_values[$record->value] = $record->count; + } + ksort($raw_values); + + // Gets active facets, starts building hierarchy. + $parent = NULL; + $gap = NULL; + $last_parent = NULL; + + foreach ($this->facet->getActiveItems() as $value => $item) { + if ($active_item = $date_handler->extractActiveItems($item)) { + $date_gap = $date_handler->getDateGap($active_item['start']['iso'], $active_item['end']['iso']); + $gap = $date_handler->getNextDateGap($date_gap, $date_handler::FACETS_DATE_MINUTE); + $last_parent = '[' . $active_item['start']['iso'] . ' TO ' . $active_item['end']['iso'] . ']'; + $result = new Result($last_parent, $date_handler->formatTimestamp($active_item['start']['timestamp'], $date_gap), NULL); + $result->setActiveState(TRUE); + // Sets the children for the current parent.. + if ($parent) { + $parent->setChildren($result); + } + else { + $parent = $parent_facet_results[] = $result; + } + } + } + + // Mind the gap! Calculates gap from min and max timestamps. + $timestamps = array_keys($raw_values); + if (is_null($parent)) { + if (count($raw_values) > 1) { + $gap = $date_handler->getTimestampGap(min($timestamps), max($timestamps)); + } + else { + $gap = $date_handler::FACETS_DATE_HOUR; + } + } + + // Converts all timestamps to dates in ISO 8601 format. + $dates = []; + foreach ($timestamps as $timestamp) { + $dates[$timestamp] = $date_handler->isoDate($timestamp, $gap); + } + + // Treat each date as the range start and next date as the range end. + $range_end = []; + $previous = NULL; + foreach (array_unique($dates) as $date) { + if (!is_null($previous)) { + $range_end[$previous] = $date_handler->getNextDateIncrement($previous, $gap); + } + $previous = $date; + } + $range_end[$previous] = $date_handler->getNextDateIncrement($previous, $gap); + + $facet_results = []; + foreach ($raw_values as $value => $count) { + $new_value = '[' . $dates[$value] . ' TO ' . $range_end[$dates[$value]] . ']'; + + // Avoid to repeat the last value. + if ($new_value === $last_parent) { + $this->facet->setResults($parent_facet_results); + return $this->facet; + } + + // Groups dates by the range they belong to. + /** @var \Drupal\facets\Result\Result $last_element */ + $last_value = end($facet_results); + if ($last_value) { + if ($new_value != $last_value->getRawValue()) { + $facet_results[] = new Result($new_value, $date_handler->formatTimestamp($value, $gap), $count); + } + else { + $last_value->setCount($last_value->getCount() + 1); + } + } + else { + $facet_results[] = new Result($new_value, $date_handler->formatTimestamp($value, $gap), $count); + } + } + + // Populate the parent with children. + $parent = end($parent_facet_results); + if ($parent instanceof ResultInterface) { + foreach ($facet_results as $result) { + $parent->setChildren($result); + $this->facet->setResults($parent_facet_results); + } + } + else { + // Set results directly when missing parents. + $this->facet->setResults($facet_results); + } + } + + return $this->facet; + } + +} diff --git a/core_search_facets/src/Tests/IntegrationTest.php b/core_search_facets/src/Tests/IntegrationTest.php index 39f9dfb..0eeecd9 100644 --- a/core_search_facets/src/Tests/IntegrationTest.php +++ b/core_search_facets/src/Tests/IntegrationTest.php @@ -2,6 +2,8 @@ namespace Drupal\core_search_facets\Tests; +use Drupal\facets\Tests\ExampleContentTrait; + /** * Tests the admin UI with the core search facet source. * @@ -9,6 +11,8 @@ namespace Drupal\core_search_facets\Tests; */ class IntegrationTest extends WebTestBase { + use ExampleContentTrait; + /** * The block entities used by this test. * @@ -95,6 +99,52 @@ class IntegrationTest extends WebTestBase { } /** + * 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); + + // Assert date facets. + $this->drupalGet('search/node', ['query' => ['keys' => 'test']]); + $this->assertLink('March 2016'); + $this->assertLink('April 2016'); + $this->assertResponse(200); + + $this->clickLink('March 2016'); + $this->assertResponse(200); + $this->assertLink('March 8, 2016'); + $this->assertLink('March 9, 2016'); + + $this->clickLink('March 9, 2016'); + $this->assertResponse(200); + $this->assertLink('10 AM'); + $this->assertLink('12 PM'); + + $this->drupalGet('search/node', ['query' => ['keys' => 'test']]); + $this->assertLink('April 2016'); + $this->clickLink('April 2016'); + $this->assertResponse(200); + $this->assertLink('April 1, 2016'); + $this->assertLink('April 2, 2016'); + } + + /** * Configures the possibility to show the amount of results for facet blocks. * * @param string $facet_name diff --git a/core_search_facets/src/Tests/WebTestBase.php b/core_search_facets/src/Tests/WebTestBase.php index a911de5..240b381 100644 --- a/core_search_facets/src/Tests/WebTestBase.php +++ b/core_search_facets/src/Tests/WebTestBase.php @@ -59,20 +59,31 @@ abstract class WebTestBase extends SimpletestWebTestBase { $this->drupalCreateContentType(['type' => 'article']); // Adding 10 pages. - for ($i = 0; $i < 10; $i++) { + for ($i = 1; $i <= 9; $i++) { + $created_time = new \DateTime('March ' . $i . ' 2016 ' . str_pad($i, 2, STR_PAD_LEFT, 0) . 'PM'); $this->drupalCreateNode(array( 'title' => 'foo bar' . $i, 'body' => 'test page' . $i, 'type' => 'page', + 'created' => $created_time->format('U'), )); } + $created_time = new \DateTime('March 9 2016 11PM'); + $this->drupalCreateNode(array( + 'title' => 'foo bar10', + 'body' => 'test page10', + 'type' => 'page', + 'created' => $created_time->format('U'), + )); // Adding 10 articles. - for ($i = 0; $i < 10; $i++) { + for ($i = 1; $i <= 10; $i++) { + $created_time = new \DateTime('April ' . $i . ' 2016 ' . str_pad($i, 2, STR_PAD_LEFT, 0) . 'PM'); $this->drupalCreateNode(array( 'title' => 'foo baz' . $i, 'body' => 'test article' . $i, 'type' => 'article', + 'created' => $created_time->format('U'), )); } diff --git a/facets.services.yml b/facets.services.yml index 1be585e..07dc307 100644 --- a/facets.services.yml +++ b/facets.services.yml @@ -22,3 +22,7 @@ services: - '@plugin.manager.facets.facet_source' - '@plugin.manager.facets.processor' - '@entity_type.manager' + facets.utility.date_handler: + class: Drupal\facets\Utility\FacetsDateHandler + arguments: + - '@date.formatter' diff --git a/src/Plugin/facets/url_processor/QueryString.php b/src/Plugin/facets/url_processor/QueryString.php index c37aa18..1904adb 100644 --- a/src/Plugin/facets/url_processor/QueryString.php +++ b/src/Plugin/facets/url_processor/QueryString.php @@ -70,6 +70,13 @@ class QueryString extends UrlProcessorPluginBase { /** @var \Drupal\facets\Result\ResultInterface[] $results */ foreach ($results as &$result) { + // Flag if children filter params need to be removed. + $remove_children = FALSE; + // Sets the url for children. + if ($children = $result->getChildren()) { + $this->buildUrls($facet, $children); + } + $filter_string = $this->urlAlias . self::SEPARATOR . $result->getRawValue(); $result_get_params = clone $get_params; @@ -78,6 +85,10 @@ class QueryString extends UrlProcessorPluginBase { if ($result->isActive()) { foreach ($filter_params as $key => $filter_param) { if ($filter_param == $filter_string) { + $remove_children = TRUE; + unset($filter_params[$key]); + } + elseif ($remove_children) { unset($filter_params[$key]); } } @@ -142,7 +153,7 @@ class QueryString extends UrlProcessorPluginBase { // Explode the active params on the separator. foreach ($active_params as $param) { - list($key, $value) = explode(self::SEPARATOR, $param); + list($key, $value) = explode(self::SEPARATOR, $param, 2); if (!isset($this->activeFilters[$key])) { $this->activeFilters[$key] = [$value]; } diff --git a/src/Plugin/facets/widget/LinksWidget.php b/src/Plugin/facets/widget/LinksWidget.php index a59d9f0..9d2fa7f 100644 --- a/src/Plugin/facets/widget/LinksWidget.php +++ b/src/Plugin/facets/widget/LinksWidget.php @@ -6,6 +6,7 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Link; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\facets\FacetInterface; +use Drupal\facets\Result\ResultInterface; use Drupal\facets\Widget\WidgetInterface; /** @@ -13,7 +14,7 @@ use Drupal\facets\Widget\WidgetInterface; * * @FacetsWidget( * id = "links", - * label = @Translation("List of links"), + * label = @Translation("Links"), * description = @Translation("A simple widget that shows a list of links"), * ) */ @@ -33,21 +34,7 @@ class LinksWidget implements WidgetInterface { $show_numbers = empty($configuration['show_numbers']) ? FALSE : (bool) $configuration['show_numbers']; foreach ($results as $result) { - // Get the link. - $text = $result->getDisplayValue(); - if ($show_numbers) { - $text .= ' (' . $result->getCount() . ')'; - } - if ($result->isActive()) { - $text = '(-) ' . $text; - } - - if (is_null($result->getUrl())) { - $items[] = $text; - } - else { - $items[] = new Link($text, $result->getUrl()); - } + $items[] = $this->buildListItems($result, $show_numbers); } $build = [ @@ -60,9 +47,110 @@ class LinksWidget implements WidgetInterface { ], ], ]; + return $build; } + /** + * Builds a renderable array of result items. + * + * @param \Drupal\facets\Result\ResultInterface $result + * A result item. + * @param bool $show_numbers + * A boolean that's true when the numbers should be shown. + * + * @return array|Link|string + * A renderable array of the result or a link when the result has no + * children. + */ + protected function buildListItems(ResultInterface $result, $show_numbers) { + if ($children = $result->getChildren()) { + $link = $this->prepareLink($result, $show_numbers); + + $children_markup = []; + foreach ($children as $child) { + $children_markup[] = $this->buildChildren($child, $show_numbers); + } + + $items = [ + '#markup' => $link->toString(), + '#wrapper_attributes' => [ + 'class' => ['expanded'], + ], + 'children' => [$children_markup], + ]; + + } + else { + $items = $this->prepareLink($result, $show_numbers); + } + + return $items; + } + + /** + * Returns the text or link for an item. + * + * @param \Drupal\facets\Result\ResultInterface $result + * A result item. + * @param bool $show_numbers + * A boolean that's true when the numbers should be shown. + * + * @return Link|string + * The item, can be a link or just the text. + */ + protected function prepareLink(ResultInterface $result, $show_numbers) { + $text = $result->getDisplayValue(); + + if ($show_numbers && $result->getCount()) { + $text .= ' (' . $result->getCount() . ')'; + } + if ($result->isActive()) { + $text = '(-) ' . $text; + } + + if (is_null($result->getUrl())) { + $link = $text; + } + else { + $link = new Link($text, $result->getUrl()); + } + + return $link; + } + + /** + * Builds a renderable array of a result. + * + * @param \Drupal\facets\Result\ResultInterface $child + * A result item. + * @param bool $show_numbers + * A boolean that's true when the numbers should be shown. + * + * @return array|Link|string + * A renderable array of the result. + */ + protected function buildChildren(ResultInterface $child, $show_numbers) { + $text = $child->getDisplayValue(); + if ($show_numbers && $child->getCount()) { + $text .= ' (' . $child->getCount() . ')'; + } + if ($child->isActive()) { + $text = '(-) ' . $text; + } + + if (!is_null($child->getUrl())) { + $link = new Link($text, $child->getUrl()); + $text = $link->toString(); + } + + return [ + '#markup' => $text, + '#wrapper_attributes' => [ + 'class' => ['leaf'], + ], + ]; + } /** * {@inheritdoc} diff --git a/src/Result/Result.php b/src/Result/Result.php index e841f68..7f410b2 100644 --- a/src/Result/Result.php +++ b/src/Result/Result.php @@ -40,6 +40,9 @@ class Result implements ResultInterface { */ protected $active = FALSE; + + protected $children = []; + /** * Constructs a new result value object. * @@ -94,6 +97,13 @@ class Result implements ResultInterface { /** * {@inheritdoc} */ + public function setCount($count) { + $this->count = $count; + } + + /** + * {@inheritdoc} + */ public function setActiveState($active) { $this->active = $active; } @@ -112,4 +122,18 @@ class Result implements ResultInterface { $this->displayValue = $display_value; } + /** + * {@inheritdoc} + */ + public function setChildren(ResultInterface $children) { + $this->children[] = $children; + } + + /** + * {@inheritdoc} + */ + public function getChildren() { + return $this->children; + } + } diff --git a/src/Result/ResultInterface.php b/src/Result/ResultInterface.php index a223242..2de8d03 100644 --- a/src/Result/ResultInterface.php +++ b/src/Result/ResultInterface.php @@ -74,4 +74,20 @@ interface ResultInterface { */ public function setDisplayValue($display_value); + /** + * Sets children results. + * + * @param \Drupal\facets\Result\ResultInterface $children + * The children to be added. + */ + public function setChildren(ResultInterface $children); + + /** + * Returns children results. + * + * @return \Drupal\facets\Result\ResultInterface $children + * The children results. + */ + public function getChildren(); + } diff --git a/src/Utility/FacetsDateHandler.php b/src/Utility/FacetsDateHandler.php new file mode 100644 index 0000000..a37c35d --- /dev/null +++ b/src/Utility/FacetsDateHandler.php @@ -0,0 +1,427 @@ +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) { + 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 string $date + * A string containing the date as an ISO date string. + * @param int $gap + * An integer containing the gap, see FACETS_DATE_* constants for valid + * values, defaults to YEAR. + * @param string $callback + * The formatting callback, defaults to "FACETS_format_timestamp". This is + * a string that can be called as a valid callback. + * + * @return string + * 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 int $gap1 + * An integer containing the gap, see FACETS_DATE_* constants for valid + * values. + * @param int $gap2 + * An integer containing the gap, see FACETS_DATE_* constants for valid + * values. + * + * @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; + } + +} diff --git a/tests/src/Unit/Utility/FacetsDateHandlerTest.php b/tests/src/Unit/Utility/FacetsDateHandlerTest.php new file mode 100644 index 0000000..7a906d4 --- /dev/null +++ b/tests/src/Unit/Utility/FacetsDateHandlerTest.php @@ -0,0 +1,243 @@ +getMock('Drupal\Core\Entity\EntityStorageInterface'); + + $em = $this->getMock('Drupal\Core\Entity\EntityManagerInterface'); + $em->expects($this->any()) + ->method('getStorage') + ->with('date_format') + ->willReturn($entity_storage); + + $language = new Language(['id' => 'en']); + + $lm = $this->getMock('Drupal\Core\Language\LanguageManagerInterface'); + $lm->method('getCurrentLanguage') + ->willReturn($language); + $st = $this->getMock('Drupal\Core\StringTranslation\TranslationInterface'); + $rs = $this->getMock('Symfony\Component\HttpFoundation\RequestStack'); + $cf = $this->getConfigFactoryStub(); + + $config_factory = $this->getConfigFactoryStub([ + 'system.date' => ['country' => ['default' => 'GB']], + ]); + $container = new ContainerBuilder(); + $container->set('config.factory', $config_factory); + \Drupal::setContainer($container); + + $date_formatter = new DateFormatter($em, $lm, $st, $cf, $rs); + + $this->handler = new FacetsDateHandler($date_formatter); + } + + /** + * Tests the isoDate method. + * + * @dataProvider provideIsoDates + */ + public function testIsoDate($iso_date, $gap) { + $fd = $this->handler; + $this->assertEquals($iso_date, $fd->isoDate(static::TIMESTAMP, $gap)); + } + + /** + * Tests for ::getNextDateGap. + */ + public function testGetNextDateGap() { + $fd = $this->handler; + + $gap = $fd->getNextDateGap($fd::FACETS_DATE_SECOND); + $this->assertEquals($fd::FACETS_DATE_SECOND, $gap); + + $gap = $fd->getNextDateGap($fd::FACETS_DATE_MINUTE); + $this->assertEquals($fd::FACETS_DATE_SECOND, $gap); + + $gap = $fd->getNextDateGap($fd::FACETS_DATE_SECOND, $fd::FACETS_DATE_MINUTE); + $this->assertEquals($fd::FACETS_DATE_MINUTE, $gap); + + $gap = $fd->getNextDateGap($fd::FACETS_DATE_MINUTE, $fd::FACETS_DATE_MINUTE); + $this->assertEquals($fd::FACETS_DATE_MINUTE, $gap); + + $gap = $fd->getNextDateGap($fd::FACETS_DATE_SECOND, $fd::FACETS_DATE_HOUR); + $this->assertEquals($fd::FACETS_DATE_HOUR, $gap); + + $gap = $fd->getNextDateGap($fd::FACETS_DATE_MINUTE, $fd::FACETS_DATE_HOUR); + $this->assertEquals($fd::FACETS_DATE_HOUR, $gap); + + $gap = $fd->getNextDateGap($fd::FACETS_DATE_HOUR, $fd::FACETS_DATE_HOUR); + $this->assertEquals($fd::FACETS_DATE_HOUR, $gap); + } + + /** + * Tests for ::getTimestampGap. + */ + public function testGetTimestampGap() { + $fd = $this->handler; + + // The best search gap between two dates must be a year. + $date_gap = $this->handler->getTimestampGap(static::TIMESTAMP, static::TIMESTAMP + 31536000); + $this->assertEquals($fd::FACETS_DATE_YEAR, $date_gap); + + // The best search gap between two dates must be a month. + $date_gap = $this->handler->getTimestampGap(static::TIMESTAMP, static::TIMESTAMP + 86400 * 60); + $this->assertEquals($fd::FACETS_DATE_MONTH, $date_gap); + + // The best search gap between two dates must be a day. + $date_gap = $this->handler->getTimestampGap(static::TIMESTAMP, static::TIMESTAMP + 86400); + $this->assertEquals($fd::FACETS_DATE_DAY, $date_gap); + + // The best search gap between two dates must be an hour. + $date_gap = $this->handler->getTimestampGap(static::TIMESTAMP, static::TIMESTAMP + 3600); + $this->assertEquals($fd::FACETS_DATE_HOUR, $date_gap); + + // The best search gap between two dates must be a minute. + $date_gap = $this->handler->getTimestampGap(static::TIMESTAMP, static::TIMESTAMP + 60); + $this->assertEquals($fd::FACETS_DATE_MINUTE, $date_gap); + + // The best search gap between two dates must be a second. + $date_gap = $this->handler->getTimestampGap(static::TIMESTAMP, static::TIMESTAMP + 59); + $this->assertEquals($fd::FACETS_DATE_SECOND, $date_gap); + } + + /** + * Tests for ::getDateGap method. + */ + public function testGetDateGap() { + $fd = $this->handler; + + // Cannot convert to timestamp. + $this->assertFalse($fd->getDateGap(static::TIMESTAMP, static::TIMESTAMP)); + + // The min. gap is MONTH but the result is larger. + $this->assertEquals($fd::FACETS_DATE_YEAR, $fd->getDateGap('1983-03-03T20:43:04Z', '1987-11-26T20:43:04Z', $fd::FACETS_DATE_MONTH)); + + // The gap is YEAR. + $this->assertEquals($fd::FACETS_DATE_YEAR, $fd->getDateGap('1983-03-03T20:43:04Z', '1987-11-26T20:43:04Z')); + + // The gap is MONTH. + $this->assertEquals($fd::FACETS_DATE_MONTH, $fd->getDateGap('1983-03-03T20:43:04Z', '1983-11-26T20:43:04Z')); + + // The gap is DAY. + $this->assertEquals($fd::FACETS_DATE_DAY, $fd->getDateGap('1983-03-03T20:43:04Z', '1983-03-26T20:43:04Z')); + + // The gap is HOUR. + $this->assertEquals($fd::FACETS_DATE_HOUR, $fd->getDateGap('1983-03-03T20:43:04Z', '1983-03-03T21:44:04Z')); + + // The gap is MINUTE. + $this->assertEquals($fd::FACETS_DATE_MINUTE, $fd->getDateGap('1983-03-03T20:43:04Z', '1983-03-03T20:44:04Z')); + + // The gap is SECOND. + $this->assertEquals($fd::FACETS_DATE_SECOND, $fd->getDateGap('1983-03-03T20:43:04Z', '1983-03-03T20:43:55Z')); + } + + /** + * Tests for ::nextDateIncrement method. + * + * @dataProvider provideNextDateIncrementData + */ + public function testNextDateIncrement($incremented_iso_date, $gap) { + $this->assertEquals($incremented_iso_date, $this->handler->getNextDateIncrement(static::ISO_DATE, $gap)); + } + + /** + * Tests for ::gapCompare method. + */ + public function testGapCompare() { + $fd = $this->handler; + + // Timestamps are equals. + $this->assertEquals(0, $fd->gapCompare(static::TIMESTAMP, static::TIMESTAMP)); + + // Timestamps are equals. + $this->assertEquals(0, $fd->gapCompare($fd::FACETS_DATE_YEAR, $fd::FACETS_DATE_YEAR)); + + // gap1 is less than gap2. + $this->assertEquals(-1, $fd->gapCompare($fd::FACETS_DATE_MONTH, $fd::FACETS_DATE_YEAR)); + + // gap1 is less than gap2. + $this->assertEquals(1, $fd->gapCompare($fd::FACETS_DATE_MONTH, $fd::FACETS_DATE_DAY)); + } + + /** + * Tests for ::formatTimestamp method. + */ + public function testFormatTimestamp() { + $fd = $this->handler; + + $year = $fd->formatTimestamp(static::TIMESTAMP); + $this->assertEquals(1987, $year); + } + + /** + * Returns a data provider for the ::testIsoDate(). + * + * @return array + * Arrays with data for the test data. + */ + public function provideIsoDates() { + return [ + ['1987-11-26T20:43:04Z', FacetsDateHandler::FACETS_DATE_SECOND], + ['1987-11-26T20:43:00Z', FacetsDateHandler::FACETS_DATE_MINUTE], + ['1987-11-26T20:00:00Z', FacetsDateHandler::FACETS_DATE_HOUR], + ['1987-11-26T00:00:00Z', FacetsDateHandler::FACETS_DATE_DAY], + ['1987-11-01T00:00:00Z', FacetsDateHandler::FACETS_DATE_MONTH], + ['1987-01-01T00:00:00Z', FacetsDateHandler::FACETS_DATE_YEAR], + ['1987-11-26T20:43:04Z', FacetsDateHandler::FACETS_DATE_ISO8601], + ]; + } + + /** + * Returns a data provider for the ::testNextDateIncrement(). + * + * @return array + * Arrays with data for the test data. + */ + public function provideNextDateIncrementData() { + return [ + ['1987-11-26T20:43:05Z', FacetsDateHandler::FACETS_DATE_SECOND], + ['1987-11-26T20:44:04Z', FacetsDateHandler::FACETS_DATE_MINUTE], + ['1987-11-26T21:43:04Z', FacetsDateHandler::FACETS_DATE_HOUR], + ['1987-11-27T20:43:04Z', FacetsDateHandler::FACETS_DATE_DAY], + ['1987-12-26T20:43:04Z', FacetsDateHandler::FACETS_DATE_MONTH], + ['1988-11-26T20:43:04Z', FacetsDateHandler::FACETS_DATE_YEAR], + ]; + } + +}