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 @@
+<?php
+
+namespace Drupal\core_search_facets\Plugin\facets\query_type;
+
+use Drupal\facets\QueryType\QueryTypePluginBase;
+use Drupal\facets\Result\Result;
+use Drupal\facets\Result\ResultInterface;
+
+/**
+ * A date query type for core search.
+ *
+ * @FacetsQueryType(
+ *   id = "core_node_search_date",
+ *   label = @Translation("Date"),
+ * )
+ */
+class CoreNodeSearchDate extends QueryTypePluginBase {
+
+  /**
+   * The backend's native query object.
+   *
+   * @var \Drupal\search_api\Query\QueryInterface
+   */
+  protected $query;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute() {
+    /** @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 */
+    $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 @@
+<?php
+
+namespace Drupal\facets\Utility;
+
+use Drupal\Core\Datetime\DateFormatter;
+
+/**
+ * Dates Handler service.
+ */
+class FacetsDateHandler {
+
+  /**
+   * String that represents a time gap of a day between two dates.
+   */
+  const FACETS_DATE_DAY = 'DAY';
+
+  /**
+   * String that represents a time gap of a year between two dates.
+   */
+  const FACETS_DATE_YEAR = 'YEAR';
+
+  /**
+   * String that represents a time gap of a month between two dates.
+   */
+  const FACETS_DATE_MONTH = 'MONTH';
+
+  /**
+   * String that represents a time gap of an hour between two dates.
+   */
+  const FACETS_DATE_HOUR = 'HOUR';
+
+  /**
+   * String that represents a time gap of a minute between two dates.
+   */
+  const FACETS_DATE_MINUTE = 'MINUTE';
+
+  /**
+   * String that represents a time gap of a second between two dates.
+   */
+  const FACETS_DATE_SECOND = 'SECOND';
+
+  /**
+   * Date string for ISO 8601 date formats.
+   */
+  const FACETS_DATE_ISO8601 = 'Y-m-d\TH:i:s\Z';
+
+  /**
+   * Regex pattern for range queries.
+   */
+  const FACETS_REGEX_RANGE = '/^[\[\{](\S+) TO (\S+)[\]\}]$/';
+
+  /**
+   * Regex pattern for date queries.
+   */
+  const FACETS_REGEX_DATE = '/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z$/';
+
+  /**
+   * Regex pattern for date ranges.
+   */
+  const FACETS_REGEX_DATE_RANGE = '/^\[((\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z) TO ((\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z)\]$/';
+
+  /**
+   * The date formatting service.
+   *
+   * @var \Drupal\Core\Datetime\DateFormatter
+   */
+  protected $dateFormatter;
+
+  /**
+   * FacetsDateHandler constructor.
+   *
+   * @param \Drupal\Core\Datetime\DateFormatter $date_formatter
+   *   The date formatting service.
+   */
+  public function __construct(DateFormatter $date_formatter) {
+    $this->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 @@
+<?php
+
+namespace Drupal\Tests\facets\Unit\Utility;
+
+use Drupal\Core\Datetime\DateFormatter;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\Language\Language;
+use Drupal\facets\Utility\FacetsDateHandler;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Unit test for Date Handler Service.
+ *
+ * @group facets
+ */
+class FacetsDateHandlerTest extends UnitTestCase {
+
+  /**
+   * Timestamp used by tests: Thu, 26 Nov 1987 20:43:04 GMT.
+   */
+  const TIMESTAMP = 564957784;
+
+  /**
+   * ISO date used by tests: Thu, 26 Nov 1987 20:43:04 GMT.
+   */
+  const ISO_DATE = '1987-11-26T20:43:04Z';
+
+  /**
+   * The system under test.
+   *
+   * @var \Drupal\facets\Utility\FacetsDateHandler
+   */
+  protected $handler;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $entity_storage = $this->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],
+    ];
+  }
+
+}
