diff --git a/core/modules/datetime/config/schema/datetime.schema.yml b/core/modules/datetime/config/schema/datetime.schema.yml
index 406a2fd..4a3ed9d 100644
--- a/core/modules/datetime/config/schema/datetime.schema.yml
+++ b/core/modules/datetime/config/schema/datetime.schema.yml
@@ -1,5 +1,7 @@
 # Schema for the configuration files of the Datetime module.
 
+# Datetime field type.
+
 field.storage_settings.datetime:
   type: mapping
   label: 'Datetime settings'
@@ -83,3 +85,84 @@ field.widget.settings.datetime_datelist:
 field.widget.settings.datetime_default:
   type: mapping
   label: 'Datetime default display format settings'
+
+# Daterange field type.
+
+field.storage_settings.daterange:
+  type: mapping
+  label: 'Date range settings'
+  mapping:
+    daterange_type:
+      type: string
+      label: 'Date type'
+
+field.field_settings.daterange:
+  type: mapping
+  label: 'Date range settings'
+
+field.value.daterange:
+  type: mapping
+  label: 'Default value'
+  mapping:
+    default_start_date_type:
+      type: string
+      label: 'Default start date type'
+    default_start_date:
+      type: string
+      label: 'Default start date value'
+    default_end_date_type:
+      type: string
+      label: 'Default end date type'
+    default_end_date:
+      type: string
+      label: 'Default end date value'
+
+field.formatter.settings.daterange_base:
+  type: mapping
+  mapping:
+    separator:
+      type: string
+      label: 'Separator'
+    timezone_override:
+      type: string
+      label: 'Time zone override'
+
+field.formatter.settings.daterange_default:
+  type: field.formatter.settings.daterange_base
+  label: 'Date range default display format settings'
+  mapping:
+    format_type:
+      type: string
+      label: 'Date format'
+
+field.formatter.settings.daterange_plain:
+  type: field.formatter.settings.daterange_base
+  label: 'Date range plain display format settings'
+
+field.formatter.settings.daterange_custom:
+  type: field.formatter.settings.daterange_base
+  label: 'Date range custom display format settings'
+  mapping:
+    date_format:
+      type: string
+      label: 'Date/time format'
+      translatable: true
+      translation context: 'PHP date format'
+
+field.widget.settings.daterange_datelist:
+  type: mapping
+  label: 'Date range select list display format settings'
+  mapping:
+    increment:
+      type: integer
+      label: 'Time increments'
+    date_order:
+      type: string
+      label: 'Date part order'
+    time_type:
+      type: string
+      label: 'Time type'
+
+field.widget.settings.daterange_default:
+  type: mapping
+  label: 'Date range default display format settings'
diff --git a/core/modules/datetime/datetime.views.inc b/core/modules/datetime/datetime.views.inc
index d3b0d18..e3f3789 100644
--- a/core/modules/datetime/datetime.views.inc
+++ b/core/modules/datetime/datetime.views.inc
@@ -13,40 +13,42 @@
 function datetime_field_views_data(FieldStorageConfigInterface $field_storage) {
   // @todo This code only covers configurable fields, handle base table fields
   //   in https://www.drupal.org/node/2489476.
-  $data = views_field_default_views_data($field_storage);
-  foreach ($data as $table_name => $table_data) {
-    // Set the 'datetime' filter type.
-    $data[$table_name][$field_storage->getName() . '_value']['filter']['id'] = 'datetime';
+  if ($field_storage->getType() == 'datetime') {
+    $data = views_field_default_views_data($field_storage);
+    foreach ($data as $table_name => $table_data) {
+      // Set the 'datetime' filter type.
+      $data[$table_name][$field_storage->getName() . '_value']['filter']['id'] = 'datetime';
 
-    // Set the 'datetime' argument type.
-    $data[$table_name][$field_storage->getName() . '_value']['argument']['id'] = 'datetime';
+      // Set the 'datetime' argument type.
+      $data[$table_name][$field_storage->getName() . '_value']['argument']['id'] = 'datetime';
 
-    // Create year, month, and day arguments.
-    $group = $data[$table_name][$field_storage->getName() . '_value']['group'];
-    $arguments = [
-      // Argument type => help text.
-      'year' => t('Date in the form of YYYY.'),
-      'month' => t('Date in the form of MM (01 - 12).'),
-      'day' => t('Date in the form of DD (01 - 31).'),
-      'week' => t('Date in the form of WW (01 - 53).'),
-      'year_month' => t('Date in the form of YYYYMM.'),
-      'full_date' => t('Date in the form of CCYYMMDD.'),
-    ];
-    foreach ($arguments as $argument_type => $help_text) {
-      $data[$table_name][$field_storage->getName() . '_value_' . $argument_type] = [
-        'title' => $field_storage->getLabel() . ' (' . $argument_type . ')',
-        'help' => $help_text,
-        'argument' => [
-          'field' => $field_storage->getName() . '_value',
-          'id' => 'datetime_' . $argument_type,
-        ],
-        'group' => $group,
+      // Create year, month, and day arguments.
+      $group = $data[$table_name][$field_storage->getName() . '_value']['group'];
+      $arguments = [
+        // Argument type => help text.
+        'year' => t('Date in the form of YYYY.'),
+        'month' => t('Date in the form of MM (01 - 12).'),
+        'day' => t('Date in the form of DD (01 - 31).'),
+        'week' => t('Date in the form of WW (01 - 53).'),
+        'year_month' => t('Date in the form of YYYYMM.'),
+        'full_date' => t('Date in the form of CCYYMMDD.'),
       ];
+      foreach ($arguments as $argument_type => $help_text) {
+        $data[$table_name][$field_storage->getName() . '_value_' . $argument_type] = [
+          'title' => $field_storage->getLabel() . ' (' . $argument_type . ')',
+          'help' => $help_text,
+          'argument' => [
+            'field' => $field_storage->getName() . '_value',
+            'id' => 'datetime_' . $argument_type,
+          ],
+          'group' => $group,
+        ];
+      }
+
+      // Set the 'datetime' sort handler.
+      $data[$table_name][$field_storage->getName() . '_value']['sort']['id'] = 'datetime';
     }
 
-    // Set the 'datetime' sort handler.
-    $data[$table_name][$field_storage->getName() . '_value']['sort']['id'] = 'datetime';
+    return $data;
   }
-
-  return $data;
 }
diff --git a/core/modules/datetime/src/DateTimeComputed.php b/core/modules/datetime/src/DateTimeComputed.php
index 6939994..49f0512 100644
--- a/core/modules/datetime/src/DateTimeComputed.php
+++ b/core/modules/datetime/src/DateTimeComputed.php
@@ -40,10 +40,12 @@ public function getValue($langcode = NULL) {
       return $this->date;
     }
 
+    /** @var \Drupal\Core\Field\FieldItemBase $item */
     $item = $this->getParent();
     $value = $item->{($this->definition->getSetting('date source'))};
+    $type = $item->getFieldDefinition()->getType();
 
-    $storage_format = $item->getFieldDefinition()->getSetting('datetime_type') == 'date' ? DATETIME_DATE_STORAGE_FORMAT : DATETIME_DATETIME_STORAGE_FORMAT;
+    $storage_format = $item->getFieldDefinition()->getSetting($type . '_type') == 'date' ? DATETIME_DATE_STORAGE_FORMAT : DATETIME_DATETIME_STORAGE_FORMAT;
     try {
       $date = DrupalDateTime::createFromFormat($storage_format, $value, DATETIME_STORAGE_TIMEZONE);
       if ($date instanceof DrupalDateTime && !$date->hasErrors()) {
diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeCustomFormatter.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeCustomFormatter.php
new file mode 100644
index 0000000..1834d4c
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeCustomFormatter.php
@@ -0,0 +1,119 @@
+<?php
+
+namespace Drupal\datetime\Plugin\Field\FieldFormatter;
+
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\datetime\Plugin\Field\FieldType\DateRangeItem;
+
+/**
+ * Plugin implementation of the 'Custom' formatter for 'daterange' fields.
+ *
+ * This formatter renders the data range as plain text, with fully
+ * configurable a date format using the PHP date syntax and separator.
+ *
+* @FieldFormatter(
+ *   id = "daterange_custom",
+ *   label = @Translation("Custom"),
+ *   field_types = {
+ *     "daterange"
+ *   }
+ * )
+ */
+class DateRangeCustomFormatter extends DateRangeFormatterBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function defaultSettings() {
+    return [
+      'date_format' => DATETIME_DATETIME_STORAGE_FORMAT,
+    ] + parent::defaultSettings();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function viewElements(FieldItemListInterface $items, $langcode) {
+    $elements = [];
+    $separator = $this->getSetting('separator');
+
+    foreach ($items as $delta => $item) {
+      if ($item->start_date && $item->end_date) {
+        /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
+        $start_date = $item->start_date;
+        /** @var \Drupal\Core\Datetime\DrupalDateTime $end_date */
+        $end_date = $item->end_date;
+
+        if ($this->getFieldSetting('daterange_type') == DateRangeItem::DATERANGE_TYPE_DATE) {
+          // A date without time will pick up the current time, use the default.
+          datetime_date_default_time($start_date);
+          datetime_date_default_time($end_date);
+        }
+
+        $this->setTimeZone($start_date);
+        $this->setTimeZone($end_date);
+
+        $start = $this->formatDate($start_date);
+        $end = $this->formatDate($end_date);
+        if ($start !== $end) {
+          $output = $this->formatDate($start_date) . ' ' . $separator . ' ' . $this->formatDate($end_date);
+        }
+        else {
+          $output = $start;
+        }
+
+        $elements[$delta] = [
+          '#cache' => [
+            'contexts' => [
+              'timezone',
+            ],
+          ],
+          '#plain_text' => $output,
+        ];
+      }
+    }
+
+    return $elements;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function formatDate($date) {
+    $format = $this->getSetting('date_format');
+    $timezone = $this->getSetting('timezone_override') ?: $date->getTimezone()->getName();
+    return $this->dateFormatter->format($date->getTimestamp(), 'custom', $format, $timezone != '' ? $timezone : NULL);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsForm(array $form, FormStateInterface $form_state) {
+    $form = parent::settingsForm($form, $form_state);
+
+    $form['date_format'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Date/time format'),
+      '#description' => $this->t('See <a href="http://php.net/manual/function.date.php" target="_blank">the documentation for PHP date formats</a>.'),
+      '#default_value' => $this->getSetting('date_format'),
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsSummary() {
+    $summary = parent::settingsSummary();
+
+    $date = DrupalDateTime::createFromTimestamp($this->requestStack->getCurrentRequest()->server->get('REQUEST_TIME'));
+    $this->setTimeZone($date);
+    $summary[] = $this->t('Format: @display', ['@display' => $this->formatDate($date)]);
+
+    return $summary;
+  }
+
+}
diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeDefaultFormatter.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeDefaultFormatter.php
new file mode 100644
index 0000000..9006fc8
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeDefaultFormatter.php
@@ -0,0 +1,164 @@
+<?php
+
+namespace Drupal\datetime\Plugin\Field\FieldFormatter;
+
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\datetime\Plugin\Field\FieldType\DateRangeItem;
+
+/**
+ * Plugin implementation of the 'Default' formatter for 'daterange' fields.
+ *
+ * This formatter renders the data range using <time> elements, with
+ * configurable date formats (from the list of configured formats) and
+ * separator.
+ *
+ * @FieldFormatter(
+ *   id = "daterange_default",
+ *   label = @Translation("Default"),
+ *   field_types = {
+ *     "daterange"
+ *   }
+ * )
+ */
+class DateRangeDefaultFormatter extends DateRangeFormatterBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function defaultSettings() {
+    return [
+      'format_type' => 'medium',
+    ] + parent::defaultSettings();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function viewElements(FieldItemListInterface $items, $langcode) {
+    $elements = [];
+    $separator = $this->getSetting('separator');
+
+    foreach ($items as $delta => $item) {
+      if ($item->start_date && $item->end_date) {
+        /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
+        $start_date = $item->start_date;
+        /** @var \Drupal\Core\Datetime\DrupalDateTime $end_date */
+        $end_date = $item->end_date;
+
+        if ($this->getFieldSetting('daterange_type') == DateRangeItem::DATERANGE_TYPE_DATE) {
+          // A date without time will pick up the current time, use the default.
+          datetime_date_default_time($start_date);
+          datetime_date_default_time($end_date);
+        }
+
+        // Create the ISO dates in Universal Time.
+        $start_iso_date = $start_date->format("Y-m-d\TH:i:s") . 'Z';
+        $end_iso_date = $end_date->format("Y-m-d\TH:i:s") . 'Z';
+
+        $this->setTimeZone($start_date);
+        $this->setTimeZone($end_date);
+
+        // Display the dates using theme datetime.
+        $elements[$delta] = [
+          '#cache' => [
+            'contexts' => [
+              'timezone',
+            ],
+          ],
+        ];
+
+        $start = $this->formatDate($start_date);
+        $end = $this->formatDate($end_date);
+
+        if ($start !== $end) {
+          $elements[$delta][] = [
+            '#theme' => 'time',
+            '#text' => $start,
+            '#html' => FALSE,
+            '#attributes' => [
+              'datetime' => $start_iso_date,
+            ]
+          ];
+          $elements[$delta][] = ['#plain_text' => ' ' . $separator . ' '];
+          $elements[$delta][] = [
+            '#theme' => 'time',
+            '#text' => $end,
+            '#html' => FALSE,
+            '#attributes' => [
+              'datetime' => $end_iso_date,
+            ]
+          ];
+        }
+        else {
+          $elements[$delta][] = [
+            '#theme' => 'time',
+            '#text' => $start,
+            '#html' => FALSE,
+            '#attributes' => [
+              'datetime' => $start_iso_date,
+            ]
+          ];
+        }
+
+        if (!empty($item->_attributes)) {
+          $elements[$delta]['#attributes'] += $item->_attributes;
+          // Unset field item attributes since they have been included in the
+          // formatter output and should not be rendered in the field template.
+          unset($item->_attributes);
+        }
+      }
+    }
+
+    return $elements;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function formatDate($date) {
+    $format_type = $this->getSetting('format_type');
+    $timezone = $this->getSetting('timezone_override') ?: $date->getTimezone()->getName();
+    return $this->dateFormatter->format($date->getTimestamp(), $format_type, '', $timezone != '' ? $timezone : NULL);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsForm(array $form, FormStateInterface $form_state) {
+    $form = parent::settingsForm($form, $form_state);
+
+    $format_types = $this->dateFormatStorage->loadMultiple();
+    $options = [];
+
+    foreach ($format_types as $type => $type_info) {
+      $format = $this->dateFormatter->format(REQUEST_TIME, $type);
+      $options[$type] = $type_info->label() . ' (' . $format . ')';
+    }
+
+    $form['format_type'] = [
+      '#type' => 'select',
+      '#title' => t('Date format'),
+      '#description' => $this->t('Choose a format for displaying the dates. Be sure to set a format appropriate for the field, i.e. omitting time for a field that only has a date.'),
+      '#options' => $options,
+      '#default_value' => $this->getSetting('format_type'),
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsSummary() {
+    $summary = parent::settingsSummary();
+
+    $date = DrupalDateTime::createFromTimestamp($this->requestStack->getCurrentRequest()->server->get('REQUEST_TIME'));
+    $this->setTimeZone($date);
+    $summary[] = $this->t('Format: @display', ['@display' => $this->formatDate($date)]);
+
+    return $summary;
+  }
+
+}
diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeFormatterBase.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeFormatterBase.php
new file mode 100644
index 0000000..56f4ac0
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeFormatterBase.php
@@ -0,0 +1,194 @@
+<?php
+
+namespace Drupal\datetime\Plugin\Field\FieldFormatter;
+
+use Drupal\Core\Datetime\DateFormatterInterface;
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FormatterBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\datetime\Plugin\Field\FieldType\DateRangeItem;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+
+/**
+ * Base class for 'DateRange Field formatter' plugin implementations.
+ */
+abstract class DateRangeFormatterBase extends FormatterBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The date formatter service.
+   *
+   * @var \Drupal\Core\Datetime\DateFormatterInterface
+   */
+  protected $dateFormatter;
+
+  /**
+   * The date format entity storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $dateFormatStorage;
+
+  /**
+   * The request stack.
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStack
+   */
+  protected $requestStack;
+
+  /**
+   * Constructs a new object which extends DateRangeFormatterBase.
+   *
+   * @param string $plugin_id
+   *   The plugin_id for the formatter.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The definition of the field to which the formatter is associated.
+   * @param array $settings
+   *   The formatter settings.
+   * @param string $label
+   *   The formatter label display setting.
+   * @param string $view_mode
+   *   The view mode.
+   * @param array $third_party_settings
+   *   Third party settings.
+   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
+   *   The date formatter service.
+   * @param \Drupal\Core\Entity\EntityStorageInterface $date_format_storage
+   *   The date format entity storage.
+   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+   *   The request stack used to retrieve the current request.
+   */
+  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, DateFormatterInterface $date_formatter, EntityStorageInterface $date_format_storage, RequestStack $request_stack) {
+    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
+
+    $this->dateFormatter = $date_formatter;
+    $this->dateFormatStorage = $date_format_storage;
+    $this->requestStack = $request_stack;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $plugin_id,
+      $plugin_definition,
+      $configuration['field_definition'],
+      $configuration['settings'],
+      $configuration['label'],
+      $configuration['view_mode'],
+      $configuration['third_party_settings'],
+      $container->get('date.formatter'),
+      $container->get('entity.manager')->getStorage('date_format'),
+      $container->get('request_stack')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function defaultSettings() {
+    return [
+      'separator' => '-',
+      'timezone_override' => '',
+    ] + parent::defaultSettings();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsForm(array $form, FormStateInterface $form_state) {
+    $form = parent::settingsForm($form, $form_state);
+
+    $form['separator'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Date separator'),
+      '#description' => $this->t('The string to separate the start and end dates'),
+      '#default_value' => $this->getSetting('separator'),
+    ];
+
+    $form['timezone_override'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Time zone override'),
+      '#description' => $this->t('The time zone selected here will always be used'),
+      '#options' => system_time_zones(TRUE),
+      '#default_value' => $this->getSetting('timezone_override'),
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsSummary() {
+    $summary = parent::settingsSummary();
+
+    if ($separator = $this->getSetting('separator')) {
+      $summary[] = $this->t('Separator: %separator', ['%separator' => $separator]);
+    }
+
+    if ($override = $this->getSetting('timezone_override')) {
+      $summary[] = $this->t('Time zone: @timezone', ['@timezone' => $override]);
+    }
+
+    return $summary;
+  }
+
+  /**
+   * Creates a formatted date as a string.
+   *
+   * @param \Drupal\Core\Datetime\DrupalDateTime $date
+   *   The date.
+   *
+   * @return string
+   *   A formatted date range string using the chosen format.
+   */
+  abstract protected function formatDate($date);
+
+  /**
+   * Sets the proper time zone on a DrupalDateTime object for the current user.
+   *
+   * A DrupalDateTime object loaded from the database will have the UTC time
+   * zone applied to it.  This method will apply the time zone for the current
+   * user, based on system and user settings.
+   *
+   * @see drupal_get_user_timezone()
+   *
+   * @param \Drupal\Core\Datetime\DrupalDateTime $date
+   *   A DrupalDateTime object.
+   */
+  protected function setTimeZone(DrupalDateTime $date) {
+    if ($this->getFieldSetting('daterange_type') === DateRangeItem::DATERANGE_TYPE_DATE) {
+      // A date without time has no timezone conversion.
+      $timezone = DATETIME_STORAGE_TIMEZONE;
+    }
+    else {
+      $timezone = drupal_get_user_timezone();
+    }
+    $date->setTimeZone(timezone_open($timezone));
+  }
+
+  /**
+   * Gets a settings array suitable for DrupalDateTime::format().
+   *
+   * @return array
+   *   The settings array that can be passed to DrupalDateTime::format().
+   */
+  protected function getFormatSettings() {
+    $settings = [];
+
+    if ($this->getSetting('timezone_override') != '') {
+      $settings['timezone'] = $this->getSetting('timezone_override');
+    }
+
+    return $settings;
+  }
+
+}
diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangePlainFormatter.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangePlainFormatter.php
new file mode 100644
index 0000000..5bdcc6a
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangePlainFormatter.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Drupal\datetime\Plugin\Field\FieldFormatter;
+
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\datetime\Plugin\Field\FieldType\DateRangeItem;
+
+/**
+ * Plugin implementation of the 'Plain' formatter for 'daterange' fields.
+ *
+ * This formatter renders the data range as a plain text string, with a
+ * configurable separator using an ISO-like date format string.
+ *
+ * @FieldFormatter(
+ *   id = "daterange_plain",
+ *   label = @Translation("Plain"),
+ *   field_types = {
+ *     "daterange"
+ *   }
+ * )
+ */
+class DateRangePlainFormatter extends DateRangeFormatterBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function viewElements(FieldItemListInterface $items, $langcode) {
+    $elements = [];
+    $separator = $this->getSetting('separator');
+
+    foreach ($items as $delta => $item) {
+      if ($item->start_date && $item->end_date) {
+        /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
+        $start_date = $item->start_date;
+        /** @var \Drupal\Core\Datetime\DrupalDateTime $end_date */
+        $end_date = $item->end_date;
+
+        if ($this->getFieldSetting('daterange_type') == DateRangeItem::DATERANGE_TYPE_DATE) {
+          // A date without time will pick up the current time, use the default.
+          datetime_date_default_time($start_date);
+          datetime_date_default_time($end_date);
+        }
+
+        $this->setTimeZone($start_date);
+        $this->setTimeZone($end_date);
+
+        $start = $this->formatDate($start_date);
+        $end = $this->formatDate($end_date);
+        if ($start !== $end) {
+          $output = $this->formatDate($start_date) . ' ' . $separator . ' ' . $this->formatDate($end_date);
+        }
+        else {
+          $output = $start;
+        }
+
+        $elements[$delta] = [
+          '#cache' => [
+            'contexts' => [
+              'timezone',
+            ],
+          ],
+          '#plain_text' => $output,
+        ];
+      }
+    }
+
+    return $elements;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function formatDate($date) {
+    $format = $this->getFieldSetting('daterange_type') == DateRangeItem::DATERANGE_TYPE_DATE ? DATETIME_DATE_STORAGE_FORMAT : DATETIME_DATETIME_STORAGE_FORMAT;
+    $timezone = $this->getSetting('timezone_override') ?: $date->getTimezone()->getName();
+    return $this->dateFormatter->format($date->getTimestamp(), 'custom', $format, $timezone != '' ? $timezone : NULL);
+  }
+
+}
diff --git a/core/modules/datetime/src/Plugin/Field/FieldType/DateRangeFieldItemList.php b/core/modules/datetime/src/Plugin/Field/FieldType/DateRangeFieldItemList.php
new file mode 100644
index 0000000..d6dbf7d
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldType/DateRangeFieldItemList.php
@@ -0,0 +1,149 @@
+<?php
+
+namespace Drupal\datetime\Plugin\Field\FieldType;
+
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldItemList;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Represents a configurable entity daterange field.
+ */
+class DateRangeFieldItemList extends FieldItemList {
+
+  /**
+   * Defines the default value as now.
+   */
+  const DEFAULT_VALUE_NOW = 'now';
+
+  /**
+   * Defines the default value as relative.
+   */
+  const DEFAULT_VALUE_CUSTOM = 'relative';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultValuesForm(array &$form, FormStateInterface $form_state) {
+    if (empty($this->getFieldDefinition()->getDefaultValueCallback())) {
+      $default_value = $this->getFieldDefinition()->getDefaultValueLiteral();
+
+      $element = [
+        '#parents' => ['default_value_input'],
+        'default_start_date_type' => [
+          '#type' => 'select',
+          '#title' => $this->t('Default start date'),
+          '#description' => $this->t('Set a default value for the start date.'),
+          '#default_value' => isset($default_value[0]['default_start_date_type']) ? $default_value[0]['default_start_date_type'] : '',
+          '#options' => [
+            static::DEFAULT_VALUE_NOW => $this->t('Current date'),
+            static::DEFAULT_VALUE_CUSTOM => $this->t('Relative date'),
+          ],
+          '#empty_value' => '',
+        ],
+        'default_start_date' => [
+          '#type' => 'textfield',
+          '#title' => $this->t('Relative default value'),
+          '#description' => $this->t("Describe a time by reference to the current day, like '+90 days' (90 days from the day the field is created) or '+1 Saturday' (the next Saturday). See <a href=\"http://php.net/manual/function.strtotime.php\">strtotime</a> for more details."),
+          '#default_value' => (isset($default_value[0]['default_start_date_type']) && $default_value[0]['default_start_date_type'] == static::DEFAULT_VALUE_CUSTOM) ? $default_value[0]['default_start_date'] : '',
+          '#states' => [
+            'visible' => [
+              ':input[id="edit-default-value-input-default-start-date-type"]' => ['value' => static::DEFAULT_VALUE_CUSTOM],
+            ],
+          ],
+        ],
+        'default_end_date_type' => [
+          '#type' => 'select',
+          '#title' => $this->t('Default end date'),
+          '#description' => $this->t('Set a default value for the end date.'),
+          '#default_value' => isset($default_value[0]['default_end_date_type']) ? $default_value[0]['default_end_date_type'] : '',
+          '#options' => [
+            static::DEFAULT_VALUE_NOW => $this->t('Current date'),
+            static::DEFAULT_VALUE_CUSTOM => $this->t('Relative date'),
+          ],
+          '#empty_value' => '',
+        ],
+        'default_end_date' => [
+          '#type' => 'textfield',
+          '#title' => $this->t('Relative default value'),
+          '#description' => $this->t("Describe a time by reference to the current day, like '+90 days' (90 days from the day the field is created) or '+1 Saturday' (the next Saturday). See <a href=\"http://php.net/manual/function.strtotime.php\">strtotime</a> for more details."),
+          '#default_value' => (isset($default_value[0]['default_end_date_type']) && $default_value[0]['default_end_date_type'] == static::DEFAULT_VALUE_CUSTOM) ? $default_value[0]['default_end_date'] : '',
+          '#states' => [
+            'visible' => [
+              ':input[id="edit-default-value-input-default-end-date-type"]' => ['value' => static::DEFAULT_VALUE_CUSTOM],
+            ],
+          ],
+        ],
+      ];
+
+      return $element;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultValuesFormValidate(array $element, array &$form, FormStateInterface $form_state) {
+    if ($form_state->getValue(['default_value_input', 'default_start_date_type']) == static::DEFAULT_VALUE_CUSTOM) {
+      $is_strtotime = @strtotime($form_state->getValue(['default_value_input', 'default_start_date']));
+      if (!$is_strtotime) {
+        $form_state->setErrorByName('default_value_input][default_start_date', $this->t('The relative start date value entered is invalid.'));
+      }
+    }
+
+    if ($form_state->getValue(['default_value_input', 'default_end_date_type']) == static::DEFAULT_VALUE_CUSTOM) {
+      $is_strtotime = @strtotime($form_state->getValue(['default_value_input', 'default_end_date']));
+      if (!$is_strtotime) {
+        $form_state->setErrorByName('default_value_input][default_end_date', $this->t('The relative end date value entered is invalid.'));
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultValuesFormSubmit(array $element, array &$form, FormStateInterface $form_state) {
+    if ($form_state->getValue(['default_value_input', 'default_start_date_type']) && $form_state->getValue(['default_value_input', 'default_end_date_type'])) {
+      if ($form_state->getValue(['default_value_input', 'default_start_date_type']) == static::DEFAULT_VALUE_NOW) {
+        $form_state->setValueForElement($element['default_start_date'], static::DEFAULT_VALUE_NOW);
+      }
+      if ($form_state->getValue(['default_value_input', 'default_end_date_type']) == static::DEFAULT_VALUE_NOW) {
+        $form_state->setValueForElement($element['default_end_date'], static::DEFAULT_VALUE_NOW);
+      }
+      return [$form_state->getValue('default_value_input')];
+    }
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function processDefaultValue($default_value, FieldableEntityInterface $entity, FieldDefinitionInterface $definition) {
+    $default_value = parent::processDefaultValue($default_value, $entity, $definition);
+
+    if (isset($default_value[0]['default_start_date_type']) && isset($default_value[0]['default_end_date_type'])) {
+      // A default value should be in the format and timezone used for date
+      // storage. All-day ranges are stored the same as date+time ranges.
+      $storage_format = $definition->getSetting('daterange_type') == DateRangeItem::DATERANGE_TYPE_DATE ? DATETIME_DATE_STORAGE_FORMAT : DATETIME_DATETIME_STORAGE_FORMAT;
+      $start_date = new DrupalDateTime($default_value[0]['default_start_date'], DATETIME_STORAGE_TIMEZONE);
+      $start_value = $start_date->format($storage_format);
+      $end_date = new DrupalDateTime($default_value[0]['default_end_date'], DATETIME_STORAGE_TIMEZONE);
+      $end_value = $end_date->format($storage_format);
+      // We only provide a default value for the first item, as do all fields.
+      // Otherwise, there is no way to clear out unwanted values on multiple
+      // value fields.
+      $default_value = [
+        [
+          'value' => $start_value,
+          'start_date' => $start_date,
+          'value2' => $end_value,
+          'end_date' => $end_date,
+        ],
+      ];
+    }
+    return $default_value;
+  }
+
+}
diff --git a/core/modules/datetime/src/Plugin/Field/FieldType/DateRangeItem.php b/core/modules/datetime/src/Plugin/Field/FieldType/DateRangeItem.php
new file mode 100644
index 0000000..7e01116
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldType/DateRangeItem.php
@@ -0,0 +1,173 @@
+<?php
+
+namespace Drupal\datetime\Plugin\Field\FieldType;
+
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\TypedData\DataDefinition;
+use Drupal\Core\Field\FieldItemBase;
+use Drupal\datetime\DateTimeComputed;
+
+/**
+ * Plugin implementation of the 'daterange' field type.
+ *
+ * @FieldType(
+ *   id = "daterange",
+ *   label = @Translation("Date range"),
+ *   description = @Translation("Create and store date ranges."),
+ *   default_widget = "daterange_default",
+ *   default_formatter = "daterange_default",
+ *   list_class = "\Drupal\datetime\Plugin\Field\FieldType\DateRangeFieldItemList"
+ * )
+ */
+class DateRangeItem extends FieldItemBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function defaultStorageSettings() {
+    return [
+      'daterange_type' => 'datetime',
+    ] + parent::defaultStorageSettings();
+  }
+
+  /**
+   * Value for the 'daterange_type' setting: store only a date.
+   */
+  const DATERANGE_TYPE_DATE = 'date';
+
+  /**
+   * Value for the 'daterange_type' setting: store a date and time.
+   */
+  const DATERANGE_TYPE_DATETIME = 'datetime';
+
+  /**
+   * Value for the 'daterange_type' setting: store a date and time.
+   */
+  const DATERANGE_TYPE_ALLDAY = 'allday';
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
+    $properties['value'] = DataDefinition::create('datetime_iso8601')
+      ->setLabel(t('Start date value'))
+      ->setRequired(TRUE);
+
+    $properties['start_date'] = DataDefinition::create('any')
+      ->setLabel(t('Computed start date'))
+      ->setDescription(t('The computed start DateTime object.'))
+      ->setComputed(TRUE)
+      ->setClass(DateTimeComputed::class)
+      ->setSetting('date source', 'value');
+
+    $properties['value2'] = DataDefinition::create('datetime_iso8601')
+      ->setLabel(t('End date value'))
+      ->setRequired(TRUE);
+
+    $properties['end_date'] = DataDefinition::create('any')
+      ->setLabel(t('Computed end date'))
+      ->setDescription(t('The computed end DateTime object.'))
+      ->setComputed(TRUE)
+      ->setClass(DateTimeComputed::class)
+      ->setSetting('date source', 'value2');
+
+    return $properties;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function schema(FieldStorageDefinitionInterface $field_definition) {
+    return [
+      'columns' => [
+        'value' => [
+          'description' => 'The start date value.',
+          'type' => 'varchar',
+          'length' => 20,
+        ],
+        'value2' => [
+          'description' => 'The end date value.',
+          'type' => 'varchar',
+          'length' => 20,
+        ],
+      ],
+      'indexes' => [
+        'value' => ['value'],
+        'value2' => ['value2'],
+      ],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) {
+    $element = [];
+
+    $element['daterange_type'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Date type'),
+      '#description' => $this->t('Choose the type of date to create.'),
+      '#default_value' => $this->getSetting('daterange_type'),
+      '#options' => [
+        static::DATERANGE_TYPE_DATETIME => t('Date and time'),
+        static::DATERANGE_TYPE_DATE => t('Date only'),
+        static::DATERANGE_TYPE_ALLDAY => t('All Day'),
+      ],
+      '#disabled' => $has_data,
+    ];
+
+    return $element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
+    $type = $field_definition->getSetting('daterange_type');
+
+    // Just pick a date in the past year. No guidance is provided by this Field
+    // type.
+    $start = REQUEST_TIME - mt_rand(0, 86400 * 365) - 86400;
+    $end = $start + 86400;
+    if ($type == static::DATERANGE_TYPE_DATE) {
+      $values['value'] = gmdate(DATETIME_DATE_STORAGE_FORMAT, $start);
+      $values['value2'] = gmdate(DATETIME_DATE_STORAGE_FORMAT, $end);
+    }
+    elseif ($type == static::DATERANGE_TYPE_ALLDAY) {
+      $values['value'] = gmdate(DATETIME_DATE_STORAGE_FORMAT, $start);
+      $values['value2'] = gmdate(DATETIME_DATE_STORAGE_FORMAT, $end);
+    }
+    else {
+      $values['value'] = gmdate(DATETIME_DATETIME_STORAGE_FORMAT, $start);
+      $values['value2'] = gmdate(DATETIME_DATETIME_STORAGE_FORMAT, $end);
+    }
+    return $values;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isEmpty() {
+    $start_value = $this->get('value')->getValue();
+    $end_value = $this->get('value2')->getValue();
+    return ($start_value === NULL || $start_value === '') && ($end_value === NULL || $end_value === '');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onChange($property_name, $notify = TRUE) {
+    // Enforce that the computed date is recalculated.
+    if ($property_name == 'value') {
+      $this->start_date = NULL;
+    }
+    elseif ($property_name == 'value2') {
+      $this->end_date = NULL;
+    }
+    parent::onChange($property_name, $notify);
+  }
+
+}
diff --git a/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeDatelistWidget.php b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeDatelistWidget.php
new file mode 100644
index 0000000..f92955c
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeDatelistWidget.php
@@ -0,0 +1,157 @@
+<?php
+
+namespace Drupal\datetime\Plugin\Field\FieldWidget;
+
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\datetime\Plugin\Field\FieldType\DateRangeItem;
+
+/**
+ * Plugin implementation of the 'daterange_datelist' widget.
+ *
+ * @FieldWidget(
+ *   id = "daterange_datelist",
+ *   label = @Translation("Select list"),
+ *   field_types = {
+ *     "daterange"
+ *   }
+ * )
+ */
+class DateRangeDatelistWidget extends DateRangeWidgetBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function defaultSettings() {
+    return array(
+      'increment' => '15',
+      'date_order' => 'YMD',
+      'time_type' => '24',
+    ) + parent::defaultSettings();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
+    $element = parent::formElement($items, $delta, $element, $form, $form_state);
+
+    $date_order = $this->getSetting('date_order');
+
+    if ($this->getFieldSetting('daterange_type') == DateRangeItem::DATERANGE_TYPE_DATETIME) {
+      $time_type = $this->getSetting('time_type');
+      $increment = $this->getSetting('increment');
+    }
+    else {
+      $time_type = '';
+      $increment = '';
+    }
+
+    // Set up the date part order array.
+    switch ($date_order) {
+      default:
+      case 'YMD':
+        $date_part_order = array('year', 'month', 'day');
+        break;
+
+      case 'MDY':
+        $date_part_order = array('month', 'day', 'year');
+        break;
+
+      case 'DMY':
+        $date_part_order = array('day', 'month', 'year');
+        break;
+    }
+    switch ($time_type) {
+      case '24':
+        $date_part_order = array_merge($date_part_order, array('hour', 'minute'));
+        break;
+
+      case '12':
+        $date_part_order = array_merge($date_part_order, array('hour', 'minute', 'ampm'));
+        break;
+
+      case 'none':
+        break;
+    }
+
+    $element['value'] = [
+      '#type' => 'datelist',
+      '#date_increment' => $increment,
+      '#date_part_order' => $date_part_order,
+    ] + $element['value'];
+
+    $element['value2'] = [
+      '#type' => 'datelist',
+      '#date_increment' => $increment,
+      '#date_part_order' => $date_part_order,
+    ] + $element['value2'];
+
+    return $element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  function settingsForm(array $form, FormStateInterface $form_state) {
+    $element = parent::settingsForm($form, $form_state);
+
+    $element['date_order'] = array(
+      '#type' => 'select',
+      '#title' => t('Date part order'),
+      '#default_value' => $this->getSetting('date_order'),
+      '#options' => array('MDY' => t('Month/Day/Year'), 'DMY' => t('Day/Month/Year'), 'YMD' => t('Year/Month/Day')),
+    );
+
+    if ($this->getFieldSetting('daterange_type') == DateRangeItem::DATERANGE_TYPE_DATETIME) {
+      $element['time_type'] = array(
+        '#type' => 'select',
+        '#title' => t('Time type'),
+        '#default_value' => $this->getSetting('time_type'),
+        '#options' => array('24' => t('24 hour time'), '12' => t('12 hour time')),
+      );
+
+      $element['increment'] = [
+        '#type' => 'select',
+        '#title' => t('Time increments'),
+        '#default_value' => $this->getSetting('increment'),
+        '#options' => [
+          1 => t('1 minute'),
+          5 => t('5 minute'),
+          10 => t('10 minute'),
+          15 => t('15 minute'),
+          30 => t('30 minute'),
+        ],
+      ];
+    }
+    else {
+      $element['time_type'] = array(
+        '#type' => 'hidden',
+        '#value' => 'none',
+      );
+
+      $element['increment'] = [
+        '#type' => 'hidden',
+        '#value' => $this->getSetting('increment'),
+      ];
+    }
+
+    return $element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsSummary() {
+    $summary = array();
+
+    $summary[] = t('Date part order: @order', array('@order' => $this->getSetting('date_order')));
+    if ($this->getFieldSetting('daterange_type') == DateRangeItem::DATERANGE_TYPE_DATETIME) {
+      $summary[] = t('Time type: @time_type', array('@time_type' => $this->getSetting('time_type')));
+      $summary[] = t('Time increments: @increment', array('@increment' => $this->getSetting('increment')));
+    }
+
+    return $summary;
+  }
+
+}
diff --git a/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php
new file mode 100644
index 0000000..24af1ce
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php
@@ -0,0 +1,107 @@
+<?php
+
+namespace Drupal\datetime\Plugin\Field\FieldWidget;
+
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\datetime\Plugin\Field\FieldType\DateRangeItem;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Plugin implementation of the 'daterange_default' widget.
+ *
+ * @FieldWidget(
+ *   id = "daterange_default",
+ *   label = @Translation("Date and time range"),
+ *   field_types = {
+ *     "daterange"
+ *   }
+ * )
+ */
+class DateRangeDefaultWidget extends DateRangeWidgetBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The date format storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $dateStorage;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityStorageInterface $date_storage) {
+    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
+
+    $this->dateStorage = $date_storage;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $plugin_id,
+      $plugin_definition,
+      $configuration['field_definition'],
+      $configuration['settings'],
+      $configuration['third_party_settings'],
+      $container->get('entity.manager')->getStorage('date_format')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
+    $element = parent::formElement($items, $delta, $element, $form, $form_state);
+
+    // Identify the type of date and time elements to use.
+    switch ($this->getFieldSetting('daterange_type')) {
+      case DateRangeItem::DATERANGE_TYPE_DATE:
+        $date_type = 'date';
+        $time_type = 'none';
+        $date_format = $this->dateStorage->load('html_date')->getPattern();
+        $time_format = '';
+        break;
+
+      case DateRangeItem::DATERANGE_TYPE_ALLDAY:
+        $date_type = 'date';
+        $time_type = 'none';
+        $date_format = $this->dateStorage->load('html_date')->getPattern();
+        $time_format = '';
+        break;
+
+      default:
+        $date_type = 'date';
+        $time_type = 'time';
+        $date_format = $this->dateStorage->load('html_date')->getPattern();
+        $time_format = $this->dateStorage->load('html_time')->getPattern();
+        break;
+    }
+
+    $element['value'] += array(
+      '#date_date_format' => $date_format,
+      '#date_date_element' => $date_type,
+      '#date_date_callbacks' => array(),
+      '#date_time_format' => $time_format,
+      '#date_time_element' => $time_type,
+      '#date_time_callbacks' => array(),
+    );
+
+    $element['value2'] += array(
+      '#date_date_format' => $date_format,
+      '#date_date_element' => $date_type,
+      '#date_date_callbacks' => array(),
+      '#date_time_format' => $time_format,
+      '#date_time_element' => $time_type,
+      '#date_time_callbacks' => array(),
+    );
+
+    return $element;
+  }
+
+}
diff --git a/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeWidgetBase.php b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeWidgetBase.php
new file mode 100644
index 0000000..7d4013d
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeWidgetBase.php
@@ -0,0 +1,178 @@
+<?php
+
+namespace Drupal\datetime\Plugin\Field\FieldWidget;
+
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Field\WidgetBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\datetime\Plugin\Field\FieldType\DateRangeItem;
+
+/**
+ * Base class for the 'daterange_*' widgets.
+ */
+class DateRangeWidgetBase extends WidgetBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
+    // We are nesting some sub-elements inside the parent, so we need a wrapper.
+    // We also need to add another #title attribute at the top level for ease in
+    // identifying this item in error messages. We do not want to display this
+    // title because the actual title display is handled at a higher level by
+    // the Field module.
+
+    $element['#theme_wrappers'][] = 'datetime_wrapper';
+    $element['#attributes']['class'][] = 'container-inline';
+    $element['#element_validate'][] = [$this, 'validateStartEnd'];
+
+    $element['value'] = array(
+      '#title' => $this->t('Start'),
+      '#type' => 'datetime',
+      '#default_value' => NULL,
+      '#date_increment' => 1,
+      '#date_timezone' => drupal_get_user_timezone(),
+      '#required' => $element['#required'],
+    );
+
+    $element['value2'] = array(
+      '#title' => $this->t('End'),
+      '#type' => 'datetime',
+      '#default_value' => NULL,
+      '#date_increment' => 1,
+      '#date_timezone' => drupal_get_user_timezone(),
+      '#required' => $element['#required'],
+    );
+
+    if ($this->getFieldSetting('daterange_type') == DateRangeItem::DATERANGE_TYPE_DATE) {
+      // A date-only field should have no timezone conversion performed, so
+      // use the same timezone as for storage.
+      $element['value']['#date_timezone'] = DATETIME_STORAGE_TIMEZONE;
+      $element['value2']['#date_timezone'] = DATETIME_STORAGE_TIMEZONE;
+    }
+
+    if ($items[$delta]->start_date) {
+      /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
+      $start_date = $items[$delta]->start_date;
+      // The date was created and verified during field_load(), so it is safe to
+      // use without further inspection.
+      if ($this->getFieldSetting('daterange_type') == DateRangeItem::DATERANGE_TYPE_DATE) {
+        // A date without time will pick up the current time, use the default
+        // time.
+        datetime_date_default_time($start_date);
+      }
+      $start_date->setTimezone(new \DateTimeZone($element['value']['#date_timezone']));
+      $element['value']['#default_value'] = $start_date;
+    }
+
+    if ($items[$delta]->end_date) {
+      /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
+      $end_date = $items[$delta]->end_date;
+      // The date was created and verified during field_load(), so it is safe to
+      // use without further inspection.
+      if ($this->getFieldSetting('daterange_type') == DateRangeItem::DATERANGE_TYPE_DATE) {
+        // A date without time will pick up the current time, use the default
+        // time.
+        datetime_date_default_time($end_date);
+      }
+      $end_date->setTimezone(new \DateTimeZone($element['value2']['#date_timezone']));
+      $element['value2']['#default_value'] = $end_date;
+    }
+
+    return $element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
+    // The widget form element type has transformed the value to a
+    // DrupalDateTime object at this point. We need to convert it back to the
+    // storage timezone and format.
+    foreach ($values as &$item) {
+      if (!empty($item['value']) && $item['value'] instanceof DrupalDateTime) {
+        /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
+        $start_date = $item['value'];
+        switch ($this->getFieldSetting('daterange_type')) {
+          case DateRangeItem::DATERANGE_TYPE_DATE:
+            // If this is a date-only field, set it to the default time so the
+            // timezone conversion can be reversed.
+            datetime_date_default_time($start_date);
+            $format = DATETIME_DATE_STORAGE_FORMAT;
+            break;
+
+          case DateRangeItem::DATERANGE_TYPE_ALLDAY:
+            // All day field start at midnight on the starting date, but are
+            // stored like datetime fields, so we need to adjust the time.
+            // This function is called twice, so to prevent a double conversion
+            // we need to explicitly set the timezone.
+            $start_date->setTimeZone(timezone_open(drupal_get_user_timezone()));
+            $start_date->setTime(0, 0, 0);
+            $format = DATETIME_DATETIME_STORAGE_FORMAT;
+            break;
+
+          default:
+            $format = DATETIME_DATETIME_STORAGE_FORMAT;
+            break;
+        }
+        // Adjust the date for storage.
+        $start_date->setTimezone(new \DateTimezone(DATETIME_STORAGE_TIMEZONE));
+        $item['value'] = $start_date->format($format);
+      }
+
+      if (!empty($item['value2']) && $item['value2'] instanceof DrupalDateTime) {
+        /** @var \Drupal\Core\Datetime\DrupalDateTime $end_date */
+        $end_date = $item['value2'];
+        switch ($this->getFieldSetting('daterange_type')) {
+          case DateRangeItem::DATERANGE_TYPE_DATE:
+            // If this is a date-only field, set it to the default time so the
+            // timezone conversion can be reversed.
+            datetime_date_default_time($end_date);
+            $format = DATETIME_DATE_STORAGE_FORMAT;
+            break;
+
+          case DateRangeItem::DATERANGE_TYPE_ALLDAY:
+            // All day field end at midnight on the end date, but are
+            // stored like datetime fields, so we need to adjust the time.
+            // This function is called twice, so to prevent a double conversion
+            // we need to explicitly set the timezone.
+            $end_date->setTimeZone(timezone_open(drupal_get_user_timezone()));
+            $end_date->setTime(23, 59, 59);
+            $format = DATETIME_DATETIME_STORAGE_FORMAT;
+            break;
+
+          default:
+            $format = DATETIME_DATETIME_STORAGE_FORMAT;
+            break;
+        }
+        // Adjust the date for storage.
+        $end_date->setTimezone(new \DateTimezone(DATETIME_STORAGE_TIMEZONE));
+        $item['value2'] = $end_date->format($format);
+      }
+    }
+
+    return $values;
+  }
+
+  /**
+   * Validates that the start <= the end date.
+   */
+  public function validateStartEnd($element, FormStateInterface $form_state) {
+    $start_date = $element['value']['#value']['object'];
+    $end_date = $element['value2']['#value']['object'];
+
+    if ($start_date instanceof DrupalDateTime && $end_date instanceof DrupalDateTime) {
+      /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
+      /** @var \Drupal\Core\Datetime\DrupalDateTime $end_date */
+
+      if ($start_date->format('U') !== $end_date->format('U')) {
+        $interval = $start_date->diff($end_date);
+        if ($interval->invert === 1) {
+          $form_state->setError($element, $this->t('The @title end date cannot be before the start date', ['@title' => $element['#title']]));
+        }
+      }
+    }
+  }
+
+}
diff --git a/core/modules/datetime/src/Tests/DateRangeFieldTest.php b/core/modules/datetime/src/Tests/DateRangeFieldTest.php
new file mode 100644
index 0000000..f2e4f90
--- /dev/null
+++ b/core/modules/datetime/src/Tests/DateRangeFieldTest.php
@@ -0,0 +1,1187 @@
+<?php
+
+namespace Drupal\datetime\Tests;
+
+use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\Core\Datetime\Entity\DateFormat;
+use Drupal\Core\Entity\Entity\EntityViewDisplay;
+use Drupal\datetime\Plugin\Field\FieldType\DateRangeItem;
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\node\Entity\Node;
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests Datetime field functionality.
+ *
+ * @group datetime
+ */
+class DateRangeFieldTest extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['node', 'entity_test', 'datetime', 'field_ui'];
+
+  /**
+   * The default display settings to use for the formatters.
+   */
+  protected $defaultSettings;
+
+  /**
+   * An array of display options to pass to entity_get_display()
+   *
+   * @var array
+   */
+  protected $displayOptions;
+
+  /**
+   * A field storage to use in this test class.
+   *
+   * @var \Drupal\field\Entity\FieldStorageConfig
+   */
+  protected $fieldStorage;
+
+  /**
+   * The field used in this test class.
+   *
+   * @var \Drupal\field\Entity\FieldConfig
+   */
+  protected $field;
+
+  /**
+  +   * An array of timezone extremes to test.
+  +   *
+  +   * @var string[]
+  +   */
+  protected static $timezones = [
+  // UTC-12, no DST.
+    'Pacific/Kwajalein',
+    // UTC-11, no DST
+    'Pacific/Midway',
+    // UTC-7, no DST.
+    'America/Phoenix',
+    // UTC.
+    'UTC',
+    // UTC+5:30, no DST.
+    'Asia/Kolkata',
+    // UTC+12, no DST
+    'Pacific/Funafuti',
+    // UTC+13, no DST.
+    'Pacific/Tongatapu',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $web_user = $this->drupalCreateUser([
+      'access content',
+      'view test entity',
+      'administer entity_test content',
+      'administer entity_test form display',
+      'administer content types',
+      'administer node fields',
+    ]);
+    $this->drupalLogin($web_user);
+
+    // Create a field with settings to validate.
+    $field_name = Unicode::strtolower($this->randomMachineName());
+    $this->fieldStorage = FieldStorageConfig::create([
+      'field_name' => $field_name,
+      'entity_type' => 'entity_test',
+      'type' => 'daterange',
+      'settings' => ['daterange_type' => DateRangeItem::DATERANGE_TYPE_DATE],
+    ]);
+    $this->fieldStorage->save();
+    $this->field = FieldConfig::create([
+      'field_storage' => $this->fieldStorage,
+      'bundle' => 'entity_test',
+      'required' => TRUE,
+    ]);
+    $this->field->save();
+
+    entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
+      ->setComponent($field_name, [
+        'type' => 'daterange_default',
+      ])
+      ->save();
+
+    $this->defaultSettings = [
+      'separator' => '-',
+      'timezone_override' => '',
+    ];
+
+    $this->displayOptions = [
+      'type' => 'daterange_default',
+      'label' => 'hidden',
+      'settings' => ['format_type' => 'medium'] + $this->defaultSettings,
+    ];
+    entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+      ->setComponent($field_name, $this->displayOptions)
+      ->save();
+  }
+
+  /**
+   * Tests date field functionality.
+   */
+  public function testDateRangeField() {
+    $field_name = $this->fieldStorage->getName();
+
+    // Loop through defined timezones to test that date-only fields work at the
+    // extremes.
+    foreach (static::$timezones as $timezone) {
+
+      $this->setSiteTimezone($timezone);
+
+      // Ensure field is set to a date-only field.
+      $this->fieldStorage->setSetting('daterange_type', DateRangeItem::DATERANGE_TYPE_DATE);
+      $this->fieldStorage->save();
+
+      // Display creation form.
+      $this->drupalGet('entity_test/add');
+      $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Start date element found.');
+      $this->assertFieldByName("{$field_name}[0][value2][date]", '', 'End date element found.');
+      $this->assertFieldByXPath('//*[@id="edit-' . $field_name . '-wrapper"]/h4[contains(@class, "js-form-required")]', TRUE, 'Required markup found');
+      $this->assertNoFieldByName("{$field_name}[0][value][time]", '', 'Start time element not found.');
+      $this->assertNoFieldByName("{$field_name}[0][value2][time]", '', 'End time element not found.');
+
+      // Build up dates in the UTC timezone.
+      $value = '2012-12-31 00:00:00';
+      $start_date = new DrupalDateTime($value, 'UTC');
+      $value2 = '2013-06-06 00:00:00';
+      $end_date = new DrupalDateTime($value2, 'UTC');
+
+      // Submit a valid date and ensure it is accepted.
+      $date_format = DateFormat::load('html_date')->getPattern();
+      $time_format = DateFormat::load('html_time')->getPattern();
+
+      $edit = array(
+        "{$field_name}[0][value][date]" => $start_date->format($date_format),
+        "{$field_name}[0][value2][date]" => $end_date->format($date_format),
+      );
+      $this->drupalPostForm(NULL, $edit, t('Save'));
+      preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
+      $id = $match[1];
+      $this->assertText(t('entity_test @id has been created.', array('@id' => $id)));
+      $this->assertRaw($start_date->format($date_format));
+      $this->assertNoRaw($start_date->format($time_format));
+      $this->assertRaw($end_date->format($date_format));
+      $this->assertNoRaw($end_date->format($time_format));
+
+      // Verify the date doesn't change when entity is edited through the form.
+      $entity = EntityTest::load($id);
+      $this->assertEqual('2012-12-31', $entity->{$field_name}->value);
+      $this->assertEqual('2013-06-06', $entity->{$field_name}->value2);
+      $this->drupalGet('entity_test/manage/' . $id . '/edit');
+      $this->drupalPostForm(NULL, [], t('Save'));
+      $this->drupalGet('entity_test/manage/' . $id . '/edit');
+      $this->drupalPostForm(NULL, [], t('Save'));
+      $this->drupalGet('entity_test/manage/' . $id . '/edit');
+      $this->drupalPostForm(NULL, [], t('Save'));
+      $entity = EntityTest::load($id);
+      $this->assertEqual('2012-12-31', $entity->{$field_name}->value);
+      $this->assertEqual('2013-06-06', $entity->{$field_name}->value2);
+
+      // Formats that display a time component for date-only fields will display
+      // the default time, so that is applied before calculating the expected
+      // value.
+      datetime_date_default_time($start_date);
+      datetime_date_default_time($end_date);
+
+      // Reset display options since these get changed below.
+      $this->displayOptions =[
+        'type' => 'daterange_default',
+        'label' => 'hidden',
+        'settings' => [
+          'format_type' => 'long',
+          'separator' => 'THESEPARATOR',
+        ] + $this->defaultSettings,
+      ];
+
+      // Verify that the default formatter works.
+      entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+        ->setComponent($field_name, $this->displayOptions)
+        ->save();
+
+      $start_expected = format_date($start_date->getTimestamp(), 'long', '', DATETIME_STORAGE_TIMEZONE);
+      $start_expected_iso = format_date($start_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', DATETIME_STORAGE_TIMEZONE);
+      $end_expected = format_date($end_date->getTimestamp(), 'long', '', DATETIME_STORAGE_TIMEZONE);
+      $end_expected_iso = format_date($end_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', DATETIME_STORAGE_TIMEZONE);
+      $this->renderTestEntity($id);
+      $this->assertFieldByXPath('//time[@datetime="' . $start_expected_iso . '"]', $start_expected, SafeMarkup::format('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', [
+        '%value' => 'long',
+        '%expected' => $start_expected,
+        '%expected_iso' => $start_expected_iso
+      ]));
+      $this->assertFieldByXPath('//time[@datetime="' . $end_expected_iso . '"]', $end_expected, SafeMarkup::format('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', [
+        '%value' => 'long',
+        '%expected' => $end_expected,
+        '%expected_iso' => $end_expected_iso
+      ]));
+      $this->assertText(' THESEPARATOR ', 'Found proper separator');
+
+      // Verify that the plain formatter works.
+      $this->displayOptions['type'] = 'daterange_plain';
+      $this->displayOptions['settings'] = $this->defaultSettings;
+      entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+        ->setComponent($field_name, $this->displayOptions)
+        ->save();
+      $expected = $start_date->format(DATETIME_DATE_STORAGE_FORMAT) . ' - ' . $end_date->format(DATETIME_DATE_STORAGE_FORMAT);
+      $this->renderTestEntity($id);
+      $this->assertText($expected, SafeMarkup::format('Formatted date field using plain format displayed as %expected.', array('%expected' => $expected)));
+
+      // Verify that the custom formatter works.
+      $this->displayOptions['type'] = 'daterange_custom';
+      $this->displayOptions['settings'] = array('date_format' => 'm/d/Y') + $this->defaultSettings;
+      entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+        ->setComponent($field_name, $this->displayOptions)
+        ->save();
+      $expected = $start_date->format($this->displayOptions['settings']['date_format']) . ' - ' . $end_date->format($this->displayOptions['settings']['date_format']);
+      $this->renderTestEntity($id);
+      $this->assertText($expected, SafeMarkup::format('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected)));
+    }
+  }
+
+  /**
+   * Tests date and time field.
+   */
+  public function testDatetimeRangeField() {
+    $field_name = $this->fieldStorage->getName();
+
+    // Ensure the field to a datetime field.
+    $this->fieldStorage->setSetting('daterange_type', DateRangeItem::DATERANGE_TYPE_DATETIME);
+    $this->fieldStorage->save();
+
+    // Display creation form.
+    $this->drupalGet('entity_test/add');
+    $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Start date element found.');
+    $this->assertFieldByName("{$field_name}[0][value][time]", '', 'Start time element found.');
+    $this->assertFieldByName("{$field_name}[0][value2][date]", '', 'End date element found.');
+    $this->assertFieldByName("{$field_name}[0][value2][time]", '', 'End time element found.');
+
+    // Build up dates in the UTC timezone.
+    $value = '2012-12-31 00:00:00';
+    $start_date = new DrupalDateTime($value, 'UTC');
+    $value2 = '2013-06-06 00:00:00';
+    $end_date = new DrupalDateTime($value2, 'UTC');
+
+    // Update the timezone to the system default.
+    $start_date->setTimezone(timezone_open(drupal_get_user_timezone()));
+    $end_date->setTimezone(timezone_open(drupal_get_user_timezone()));
+
+    // Submit a valid date and ensure it is accepted.
+    $date_format = DateFormat::load('html_date')->getPattern();
+    $time_format = DateFormat::load('html_time')->getPattern();
+
+    $edit = array(
+      "{$field_name}[0][value][date]" => $start_date->format($date_format),
+      "{$field_name}[0][value][time]" => $start_date->format($time_format),
+      "{$field_name}[0][value2][date]" => $end_date->format($date_format),
+      "{$field_name}[0][value2][time]" => $end_date->format($time_format),
+    );
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
+    $id = $match[1];
+    $this->assertText(t('entity_test @id has been created.', array('@id' => $id)));
+    $this->assertRaw($start_date->format($date_format));
+    $this->assertRaw($start_date->format($time_format));
+    $this->assertRaw($end_date->format($date_format));
+    $this->assertRaw($end_date->format($time_format));
+
+    // Verify that the default formatter works.
+    $this->displayOptions['settings'] = [
+        'format_type' => 'long',
+        'separator' => 'THESEPARATOR',
+      ] + $this->defaultSettings;
+    entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+      ->setComponent($field_name, $this->displayOptions)
+      ->save();
+
+    $start_expected = format_date($start_date->getTimestamp(), 'long');
+    $start_expected_iso = format_date($start_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC');
+    $end_expected = format_date($end_date->getTimestamp(), 'long');
+    $end_expected_iso = format_date($end_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC');
+    $this->renderTestEntity($id);
+    $this->assertFieldByXPath('//time[@datetime="' . $start_expected_iso . '"]', $start_expected, SafeMarkup::format('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => 'long', '%expected' => $start_expected, '%expected_iso' => $start_expected_iso]));
+    $this->assertFieldByXPath('//time[@datetime="' . $end_expected_iso . '"]', $end_expected, SafeMarkup::format('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => 'long', '%expected' => $end_expected, '%expected_iso' => $end_expected_iso]));
+    $this->assertText(' THESEPARATOR ', 'Found proper separator');
+
+    // Verify that the plain formatter works.
+    $this->displayOptions['type'] = 'daterange_plain';
+    $this->displayOptions['settings'] = $this->defaultSettings;
+    entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+      ->setComponent($field_name, $this->displayOptions)
+      ->save();
+    $expected = $start_date->format(DATETIME_DATETIME_STORAGE_FORMAT) . ' - ' . $end_date->format(DATETIME_DATETIME_STORAGE_FORMAT);
+    $this->renderTestEntity($id);
+    $this->assertText($expected, SafeMarkup::format('Formatted date field using plain format displayed as %expected.', array('%expected' => $expected)));
+
+    // Verify that the 'datetime_custom' formatter works.
+    $this->displayOptions['type'] = 'daterange_custom';
+    $this->displayOptions['settings'] = ['date_format' => 'm/d/Y g:i:s A'] + $this->defaultSettings;
+    entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+      ->setComponent($field_name, $this->displayOptions)
+      ->save();
+    $expected = $start_date->format($this->displayOptions['settings']['date_format']) . ' - ' . $end_date->format($this->displayOptions['settings']['date_format']);
+    $this->renderTestEntity($id);
+    $this->assertText($expected, SafeMarkup::format('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected)));
+
+    // Verify that the 'timezone_override' setting works.
+    $this->displayOptions['type'] = 'daterange_custom';
+    $this->displayOptions['settings'] = ['date_format' => 'm/d/Y g:i:s A', 'timezone_override' => 'America/New_York'] + $this->defaultSettings;
+    entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+      ->setComponent($field_name, $this->displayOptions)
+      ->save();
+    $expected = $start_date->format($this->displayOptions['settings']['date_format'], ['timezone' => 'America/New_York']);
+    $expected .= ' - ' . $end_date->format($this->displayOptions['settings']['date_format'], ['timezone' => 'America/New_York']);
+    $this->renderTestEntity($id);
+    $this->assertText($expected, SafeMarkup::format('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected)));
+  }
+
+  /**
+   * Tests all-day field.
+   */
+  public function testAlldayRangeField() {
+    $field_name = $this->fieldStorage->getName();
+
+    // Ensure field is set to a all-day field.
+    $this->fieldStorage->setSetting('daterange_type', DateRangeItem::DATERANGE_TYPE_ALLDAY);
+    $this->fieldStorage->save();
+
+    // Display creation form.
+    $this->drupalGet('entity_test/add');
+    $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Start date element found.');
+    $this->assertFieldByName("{$field_name}[0][value2][date]", '', 'End date element found.');
+    $this->assertFieldByXPath('//*[@id="edit-' . $field_name . '-wrapper"]/h4[contains(@class, "js-form-required")]', TRUE, 'Required markup found');
+    $this->assertNoFieldByName("{$field_name}[0][value][time]", '', 'Start time element not found.');
+    $this->assertNoFieldByName("{$field_name}[0][value2][time]", '', 'End time element not found.');
+
+    // Build up dates in the proper timezone.
+    $value = '2012-12-31 00:00:00';
+    $start_date = new DrupalDateTime($value, timezone_open(drupal_get_user_timezone()));
+    $value2 = '2013-06-06 23:59:59';
+    $end_date = new DrupalDateTime($value2, timezone_open(drupal_get_user_timezone()));
+
+    // Submit a valid date and ensure it is accepted.
+    $date_format = DateFormat::load('html_date')->getPattern();
+    $time_format = DateFormat::load('html_time')->getPattern();
+
+    $edit = array(
+      "{$field_name}[0][value][date]" => $start_date->format($date_format),
+      "{$field_name}[0][value2][date]" => $end_date->format($date_format),
+    );
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
+    $id = $match[1];
+    $this->assertText(t('entity_test @id has been created.', array('@id' => $id)));
+    $this->assertRaw($start_date->format($date_format));
+    $this->assertNoRaw($start_date->format($time_format));
+    $this->assertRaw($end_date->format($date_format));
+    $this->assertNoRaw($end_date->format($time_format));
+
+    // Verify that the default formatter works.
+    $this->displayOptions['settings'] = [
+        'format_type' => 'long',
+        'separator' => 'THESEPARATOR',
+      ] + $this->defaultSettings;
+    entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+      ->setComponent($field_name, $this->displayOptions)
+      ->save();
+
+    $start_expected = format_date($start_date->getTimestamp(), 'long');
+    $start_expected_iso = format_date($start_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC');
+    $end_expected = format_date($end_date->getTimestamp(), 'long');
+    $end_expected_iso = format_date($end_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC');
+    $this->renderTestEntity($id);
+    $this->assertFieldByXPath('//time[@datetime="' . $start_expected_iso . '"]', $start_expected, SafeMarkup::format('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => 'long', '%expected' => $start_expected, '%expected_iso' => $start_expected_iso]));
+    $this->assertFieldByXPath('//time[@datetime="' . $end_expected_iso . '"]', $end_expected, SafeMarkup::format('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => 'long', '%expected' => $end_expected, '%expected_iso' => $end_expected_iso]));
+    $this->assertText(' THESEPARATOR ', 'Found proper separator');
+
+    // Verify that the plain formatter works.
+    $this->displayOptions['type'] = 'daterange_plain';
+    $this->displayOptions['settings'] = $this->defaultSettings;
+    entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+      ->setComponent($field_name, $this->displayOptions)
+      ->save();
+    $expected = $start_date->format(DATETIME_DATETIME_STORAGE_FORMAT) . ' - ' . $end_date->format(DATETIME_DATETIME_STORAGE_FORMAT);
+    $this->renderTestEntity($id);
+    $this->assertText($expected, SafeMarkup::format('Formatted date field using plain format displayed as %expected.', array('%expected' => $expected)));
+
+    // Verify that the custom formatter works.
+    $this->displayOptions['type'] = 'daterange_custom';
+    $this->displayOptions['settings'] = array('date_format' => 'm/d/Y') + $this->defaultSettings;
+    entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+      ->setComponent($field_name, $this->displayOptions)
+      ->save();
+    $expected = $start_date->format($this->displayOptions['settings']['date_format']) . ' - ' . $end_date->format($this->displayOptions['settings']['date_format']);
+    $this->renderTestEntity($id);
+    $this->assertText($expected, SafeMarkup::format('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected)));
+
+    // Verify that the 'timezone_override' setting works.
+    $this->displayOptions['type'] = 'daterange_custom';
+    $this->displayOptions['settings'] = ['date_format' => 'm/d/Y g:i:s A', 'timezone_override' => 'America/New_York'] + $this->defaultSettings;
+    entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+      ->setComponent($field_name, $this->displayOptions)
+      ->save();
+    $expected = $start_date->format($this->displayOptions['settings']['date_format'], ['timezone' => 'America/New_York']);
+    $expected .= ' - ' . $end_date->format($this->displayOptions['settings']['date_format'], ['timezone' => 'America/New_York']);
+    $this->renderTestEntity($id);
+    $this->assertText($expected, SafeMarkup::format('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected)));
+  }
+
+  /**
+   * Tests Date Range List Widget functionality.
+   */
+  public function testDatelistWidget() {
+    $field_name = $this->fieldStorage->getName();
+
+    // Ensure field is set to a date only field.
+    $this->fieldStorage->setSetting('daterange_type', DateRangeItem::DATERANGE_TYPE_DATE);
+    $this->fieldStorage->save();
+
+    // Change the widget to a datelist widget.
+    entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
+      ->setComponent($field_name, [
+        'type' => 'daterange_datelist',
+        'settings' => [
+          'date_order' => 'YMD',
+        ],
+      ])
+      ->save();
+    \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+
+    // Display creation form.
+    $this->drupalGet('entity_test/add');
+
+    // Assert that Hour and Minute Elements do not appear on Date Only
+    $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-hour\"]", NULL, 'Hour element not found on Date Only.');
+    $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-minute\"]", NULL, 'Minute element not found on Date Only.');
+    $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value2-hour\"]", NULL, 'Hour element not found on Date Only.');
+    $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value2-minute\"]", NULL, 'Minute element not found on Date Only.');
+
+    // Go to the form display page to assert that increment option does not appear on Date Only
+    $fieldEditUrl = 'entity_test/structure/entity_test/form-display';
+    $this->drupalGet($fieldEditUrl);
+
+    // Click on the widget settings button to open the widget settings form.
+    $this->drupalPostAjaxForm(NULL, [], $field_name . "_settings_edit");
+    $xpathIncr = "//select[starts-with(@id, \"edit-fields-$field_name-settings-edit-form-settings-increment\")]";
+    $this->assertNoFieldByXPath($xpathIncr, NULL, 'Increment element not found for Date Only.');
+
+    // Change the field is set to an all day field.
+    $this->fieldStorage->setSetting('daterange_type', DateRangeItem::DATERANGE_TYPE_ALLDAY);
+    $this->fieldStorage->save();
+
+    // Change the widget to a datelist widget.
+    entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
+      ->setComponent($field_name, [
+        'type' => 'daterange_datelist',
+        'settings' => [
+          'date_order' => 'YMD',
+        ],
+      ])
+      ->save();
+    \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+
+    // Display creation form.
+    $this->drupalGet('entity_test/add');
+
+    // Assert that Hour and Minute Elements do not appear on Date Only
+    $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-hour\"]", NULL, 'Hour element not found on Date Only.');
+    $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-minute\"]", NULL, 'Minute element not found on Date Only.');
+    $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value2-hour\"]", NULL, 'Hour element not found on Date Only.');
+    $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value2-minute\"]", NULL, 'Minute element not found on Date Only.');
+
+    // Go to the form display page to assert that increment option does not appear on Date Only
+    $fieldEditUrl = 'entity_test/structure/entity_test/form-display';
+    $this->drupalGet($fieldEditUrl);
+
+    // Click on the widget settings button to open the widget settings form.
+    $this->drupalPostAjaxForm(NULL, [], $field_name . "_settings_edit");
+    $xpathIncr = "//select[starts-with(@id, \"edit-fields-$field_name-settings-edit-form-settings-increment\")]";
+    $this->assertNoFieldByXPath($xpathIncr, NULL, 'Increment element not found for Date Only.');
+
+    // Change the field to a datetime field.
+    $this->fieldStorage->setSetting('daterange_type', DateRangeItem::DATERANGE_TYPE_DATETIME);
+    $this->fieldStorage->save();
+
+    // Change the widget to a datelist widget.
+    entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
+      ->setComponent($field_name, [
+        'type' => 'daterange_datelist',
+        'settings' => [
+          'increment' => 1,
+          'date_order' => 'YMD',
+          'time_type' => '12',
+        ],
+      ])
+      ->save();
+    \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+
+    // Go to the form display page to assert that increment option does appear on Date Time
+    $fieldEditUrl = 'entity_test/structure/entity_test/form-display';
+    $this->drupalGet($fieldEditUrl);
+
+    // Click on the widget settings button to open the widget settings form.
+    $this->drupalPostAjaxForm(NULL, [], $field_name . "_settings_edit");
+    $this->assertFieldByXPath($xpathIncr, NULL, 'Increment element found for Date and time.');
+
+    // Display creation form.
+    $this->drupalGet('entity_test/add');
+
+    foreach (['value','value2'] as $column) {
+      foreach (['year', 'month', 'day', 'hour', 'minute', 'ampm'] as $element) {
+        $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-$column-$element\"]", NULL, $element . ' element found.');
+        $this->assertOptionSelected("edit-$field_name-0-$column-$element", '', 'No ' . $element . ' selected.');
+      }
+    }
+
+    // Submit a valid date and ensure it is accepted.
+    $start_date_value = ['year' => 2012, 'month' => 12, 'day' => 31, 'hour' => 5, 'minute' => 15];
+    $end_date_value = ['year' => 2013, 'month' => 1, 'day' => 15, 'hour' => 3, 'minute' => 30];
+
+    $edit = [];
+    // Add the ampm indicator since we are testing 12 hour time.
+    $start_date_value['ampm'] = 'am';
+    $end_date_value['ampm'] = 'pm';
+    foreach ($start_date_value as $part => $value) {
+      $edit["{$field_name}[0][value][$part]"] = $value;
+    }
+    foreach ($end_date_value as $part => $value) {
+      $edit["{$field_name}[0][value2][$part]"] = $value;
+    }
+
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
+    $id = $match[1];
+    $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
+
+    $this->assertOptionSelected("edit-$field_name-0-value-year", '2012', 'Correct year selected.');
+    $this->assertOptionSelected("edit-$field_name-0-value-month", '12', 'Correct month selected.');
+    $this->assertOptionSelected("edit-$field_name-0-value-day", '31', 'Correct day selected.');
+    $this->assertOptionSelected("edit-$field_name-0-value-hour", '5', 'Correct hour selected.');
+    $this->assertOptionSelected("edit-$field_name-0-value-minute", '15', 'Correct minute selected.');
+    $this->assertOptionSelected("edit-$field_name-0-value-ampm", 'am', 'Correct ampm selected.');
+
+    $this->assertOptionSelected("edit-$field_name-0-value2-year", '2013', 'Correct year selected.');
+    $this->assertOptionSelected("edit-$field_name-0-value2-month", '1', 'Correct month selected.');
+    $this->assertOptionSelected("edit-$field_name-0-value2-day", '15', 'Correct day selected.');
+    $this->assertOptionSelected("edit-$field_name-0-value2-hour", '3', 'Correct hour selected.');
+    $this->assertOptionSelected("edit-$field_name-0-value2-minute", '30', 'Correct minute selected.');
+    $this->assertOptionSelected("edit-$field_name-0-value2-ampm", 'pm', 'Correct ampm selected.');
+
+    // Test the widget using increment other than 1 and 24 hour mode.
+    entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
+      ->setComponent($field_name, [
+        'type' => 'daterange_datelist',
+        'settings' => [
+          'increment' => 15,
+          'date_order' => 'YMD',
+          'time_type' => '24',
+        ],
+      ])
+      ->save();
+    \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+
+    // Display creation form.
+    $this->drupalGet('entity_test/add');
+
+    // Other elements are unaffected by the changed settings.
+    $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-hour\"]", NULL, 'Hour element found.');
+    $this->assertOptionSelected("edit-$field_name-0-value-hour", '', 'No hour selected.');
+    $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-ampm\"]", NULL, 'AMPM element not found.');
+    $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value2-hour\"]", NULL, 'Hour element found.');
+    $this->assertOptionSelected("edit-$field_name-0-value2-hour", '', 'No hour selected.');
+    $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value2-ampm\"]", NULL, 'AMPM element not found.');
+
+    // Submit a valid date and ensure it is accepted.
+    $start_date_value = ['year' => 2012, 'month' => 12, 'day' => 31, 'hour' => 17, 'minute' => 15];
+    $end_date_value = ['year' => 2013, 'month' => 1, 'day' => 15, 'hour' => 3, 'minute' => 30];
+
+    $edit = [];
+    foreach ($start_date_value as $part => $value) {
+      $edit["{$field_name}[0][value][$part]"] = $value;
+    }
+    foreach ($end_date_value as $part => $value) {
+      $edit["{$field_name}[0][value2][$part]"] = $value;
+    }
+
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
+    $id = $match[1];
+    $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
+
+    $this->assertOptionSelected("edit-$field_name-0-value-year", '2012', 'Correct year selected.');
+    $this->assertOptionSelected("edit-$field_name-0-value-month", '12', 'Correct month selected.');
+    $this->assertOptionSelected("edit-$field_name-0-value-day", '31', 'Correct day selected.');
+    $this->assertOptionSelected("edit-$field_name-0-value-hour", '17', 'Correct hour selected.');
+    $this->assertOptionSelected("edit-$field_name-0-value-minute", '15', 'Correct minute selected.');
+
+    $this->assertOptionSelected("edit-$field_name-0-value2-year", '2013', 'Correct year selected.');
+    $this->assertOptionSelected("edit-$field_name-0-value2-month", '1', 'Correct month selected.');
+    $this->assertOptionSelected("edit-$field_name-0-value2-day", '15', 'Correct day selected.');
+    $this->assertOptionSelected("edit-$field_name-0-value2-hour", '3', 'Correct hour selected.');
+    $this->assertOptionSelected("edit-$field_name-0-value2-minute", '30', 'Correct minute selected.');
+
+    // Test the widget for partial completion of fields.
+    entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
+      ->setComponent($field_name, [
+        'type' => 'daterange_datelist',
+        'settings' => [
+          'increment' => 1,
+          'date_order' => 'YMD',
+          'time_type' => '24',
+        ],
+      ])
+      ->save();
+    \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+
+    // Test the widget for validation notifications.
+    foreach ($this->datelistDataProvider() as $data) {
+      list($start_date_value, $end_date_value, $expected) = $data;
+
+      // Display creation form.
+      $this->drupalGet('entity_test/add');
+
+      // Submit a partial date and ensure and error message is provided.
+      $edit = [];
+      foreach ($start_date_value as $part => $value) {
+        $edit["{$field_name}[0][value][$part]"] = $value;
+      }
+      foreach ($end_date_value as $part => $value) {
+        $edit["{$field_name}[0][value2][$part]"] = $value;
+      }
+
+      $this->drupalPostForm(NULL, $edit, t('Save'));
+      $this->assertResponse(200);
+      foreach ($expected as $expected_text) {
+        $this->assertText(t($expected_text));
+      }
+    }
+
+    // Test the widget for complete input with zeros as part of selections.
+    $this->drupalGet('entity_test/add');
+
+    $start_date_value = ['year' => 2012, 'month' => 12, 'day' => 31, 'hour' => 0, 'minute' => 0];
+    $end_date_value = ['year' => 2013, 'month' => 1, 'day' => 15, 'hour' => 3, 'minute' => 30];
+    $edit = [];
+    foreach ($start_date_value as $part => $value) {
+      $edit["{$field_name}[0][value][$part]"] = $value;
+    }
+    foreach ($end_date_value as $part => $value) {
+      $edit["{$field_name}[0][value2][$part]"] = $value;
+    }
+
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertResponse(200);
+    preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
+    $id = $match[1];
+    $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
+
+    // Test the widget to ensure zeros are not deselected on validation.
+    $this->drupalGet('entity_test/add');
+
+    $start_date_value = ['year' => 2012, 'month' => 12, 'day' => 31, 'hour' => 0, 'minute' => 0];
+    $end_date_value = ['year' => 2013, 'month' => 1, 'day' => 15, 'hour' => 3, 'minute' => 0];
+    $edit = [];
+    foreach ($start_date_value as $part => $value) {
+      $edit["{$field_name}[0][value][$part]"] = $value;
+    }
+    foreach ($end_date_value as $part => $value) {
+      $edit["{$field_name}[0][value2][$part]"] = $value;
+    }
+
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertResponse(200);
+    $this->assertOptionSelected("edit-$field_name-0-value-minute", '0', 'Correct minute selected.');
+    $this->assertOptionSelected("edit-$field_name-0-value2-minute", '0', 'Correct minute selected.');
+  }
+
+  /**
+   * The data provider for testing the validation of the datelist widget.
+   *
+   * @return array
+   *   An array of datelist input permutations to test.
+   */
+  protected function datelistDataProvider() {
+    return [
+      // Year only selected, validation error on Month, Day, Hour, Minute.
+      [['year' => 2012, 'month' => '', 'day' => '', 'hour' => '', 'minute' => ''],
+       ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '3', 'minute' => '30'], [
+        'A value must be selected for month.',
+        'A value must be selected for day.',
+        'A value must be selected for hour.',
+        'A value must be selected for minute.',
+      ]],
+      // Year and Month selected, validation error on Day, Hour, Minute.
+      [['year' => 2012, 'month' => '12', 'day' => '', 'hour' => '', 'minute' => ''],
+        ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '3', 'minute' => '30'], [
+        'A value must be selected for day.',
+        'A value must be selected for hour.',
+        'A value must be selected for minute.',
+      ]],
+      // Year, Month and Day selected, validation error on Hour, Minute.
+      [['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '', 'minute' => ''],
+        ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '3', 'minute' => '30'], [
+        'A value must be selected for hour.',
+        'A value must be selected for minute.',
+      ]],
+      // Year, Month, Day and Hour selected, validation error on Minute only.
+      [['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => ''],
+        ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '3', 'minute' => '30'], [
+        'A value must be selected for minute.',
+      ]],
+      [['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => '0'],
+        ['year' => 2013, 'month' => '', 'day' => '', 'hour' => '', 'minute' => ''], [
+        'A value must be selected for month.',
+        'A value must be selected for day.',
+        'A value must be selected for hour.',
+        'A value must be selected for minute.',
+      ]],
+      // Year and Month selected, validation error on Day, Hour, Minute.
+      [['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => '0'],
+        ['year' => 2013, 'month' => '1', 'day' => '', 'hour' => '', 'minute' => ''], [
+        'A value must be selected for day.',
+        'A value must be selected for hour.',
+        'A value must be selected for minute.',
+      ]],
+      // Year, Month and Day selected, validation error on Hour, Minute.
+      [['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => '0'],
+        ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '', 'minute' => ''], [
+        'A value must be selected for hour.',
+        'A value must be selected for minute.',
+      ]],
+      // Year, Month, Day and Hour selected, validation error on Minute only.
+      [['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => '0'],
+        ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '3', 'minute' => ''], [
+        'A value must be selected for minute.',
+      ]],
+    ];
+  }
+
+  /**
+   * Test default value functionality.
+   */
+  public function testDefaultValue() {
+    // Create a test content type.
+    $this->drupalCreateContentType(['type' => 'date_content']);
+
+    // Create a field storage with settings to validate.
+    $field_name = Unicode::strtolower($this->randomMachineName());
+    $field_storage = FieldStorageConfig::create([
+      'field_name' => $field_name,
+      'entity_type' => 'node',
+      'type' => 'daterange',
+      'settings' => ['daterange_type' => DateRangeItem::DATERANGE_TYPE_DATE],
+    ]);
+    $field_storage->save();
+
+    $field = FieldConfig::create([
+      'field_storage' => $field_storage,
+      'bundle' => 'date_content',
+    ]);
+    $field->save();
+
+    // Set now as default_value.
+    $field_edit = [
+      'default_value_input[default_start_date_type]' => 'now',
+      'default_value_input[default_end_date_type]' => 'now',
+    ];
+    $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
+
+    // Check that default value is selected in default value form.
+    $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name);
+    $this->assertOptionSelected('edit-default-value-input-default-start-date-type', 'now', 'The default start value is selected in instance settings page');
+    $this->assertFieldByName('default_value_input[default_start_date]', '', 'The relative start default value is empty in instance settings page');
+    $this->assertOptionSelected('edit-default-value-input-default-end-date-type', 'now', 'The default end value is selected in instance settings page');
+    $this->assertFieldByName('default_value_input[default_end_date]', '', 'The relative end default value is empty in instance settings page');
+
+    // Check if default_date has been stored successfully.
+    $config_entity = $this->config('field.field.node.date_content.' . $field_name)->get();
+    $this->assertEqual($config_entity['default_value'][0], [
+      'default_start_date_type' => 'now',
+      'default_start_date' => 'now',
+      'default_end_date_type' => 'now',
+      'default_end_date' => 'now'
+    ], 'Default value has been stored successfully');
+
+    // Clear field cache in order to avoid stale cache values.
+    \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+
+    // Create a new node to check that datetime field default value is today.
+    $new_node = Node::create(['type' => 'date_content']);
+    $expected_date = new DrupalDateTime('now', DATETIME_STORAGE_TIMEZONE);
+    $this->assertEqual($new_node->get($field_name)->offsetGet(0)->value, $expected_date->format(DATETIME_DATE_STORAGE_FORMAT));
+    $this->assertEqual($new_node->get($field_name)->offsetGet(0)->value2, $expected_date->format(DATETIME_DATE_STORAGE_FORMAT));
+
+    // Set an invalid relative default_value to test validation.
+    $field_edit = [
+      'default_value_input[default_start_date_type]' => 'relative',
+      'default_value_input[default_start_date]' => 'invalid date',
+      'default_value_input[default_end_date_type]' => 'relative',
+      'default_value_input[default_end_date]' => '+1 day',
+    ];
+    $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
+    $this->assertText('The relative start date value entered is invalid.');
+
+    $field_edit = [
+      'default_value_input[default_start_date_type]' => 'relative',
+      'default_value_input[default_start_date]' => '+1 day',
+      'default_value_input[default_end_date_type]' => 'relative',
+      'default_value_input[default_end_date]' => 'invalid date',
+    ];
+    $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
+    $this->assertText('The relative end date value entered is invalid.');
+
+    // Set a relative default_value.
+    $field_edit = [
+      'default_value_input[default_start_date_type]' => 'relative',
+      'default_value_input[default_start_date]' => '+45 days',
+      'default_value_input[default_end_date_type]' => 'relative',
+      'default_value_input[default_end_date]' => '+90 days',
+    ];
+    $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
+
+    // Check that default value is selected in default value form.
+    $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name);
+    $this->assertOptionSelected('edit-default-value-input-default-start-date-type', 'relative', 'The default start value is selected in instance settings page');
+    $this->assertFieldByName('default_value_input[default_start_date]', '+45 days', 'The relative default start value is displayed in instance settings page');
+    $this->assertOptionSelected('edit-default-value-input-default-end-date-type', 'relative', 'The default end value is selected in instance settings page');
+    $this->assertFieldByName('default_value_input[default_end_date]', '+90 days', 'The relative default end value is displayed in instance settings page');
+
+    // Check if default_date has been stored successfully.
+    $config_entity = $this->config('field.field.node.date_content.' . $field_name)->get();
+    $this->assertEqual($config_entity['default_value'][0], [
+      'default_start_date_type' => 'relative',
+      'default_start_date' => '+45 days',
+      'default_end_date_type' => 'relative',
+      'default_end_date' => '+90 days',
+    ], 'Default value has been stored successfully');
+
+    // Clear field cache in order to avoid stale cache values.
+    \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+
+    // Create a new node to check that datetime field default value is +90 days.
+    $new_node = Node::create(['type' => 'date_content']);
+    $expected_start_date = new DrupalDateTime('+45 days', DATETIME_STORAGE_TIMEZONE);
+    $expected_end_date = new DrupalDateTime('+90 days', DATETIME_STORAGE_TIMEZONE);
+    $this->assertEqual($new_node->get($field_name)->offsetGet(0)->value, $expected_start_date->format(DATETIME_DATE_STORAGE_FORMAT));
+    $this->assertEqual($new_node->get($field_name)->offsetGet(0)->value2, $expected_end_date->format(DATETIME_DATE_STORAGE_FORMAT));
+
+    // Remove default value.
+    $field_edit = [
+      'default_value_input[default_start_date_type]' => '',
+      'default_value_input[default_end_date_type]' => '',
+    ];
+    $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
+
+    // Check that default value is selected in default value form.
+    $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name);
+    $this->assertOptionSelected('edit-default-value-input-default-start-date-type', '', 'The default start value is selected in instance settings page');
+    $this->assertFieldByName('default_value_input[default_start_date]', '', 'The relative default start value is empty in instance settings page');
+    $this->assertOptionSelected('edit-default-value-input-default-end-date-type', '', 'The default end value is selected in instance settings page');
+    $this->assertFieldByName('default_value_input[default_end_date]', '', 'The relative default end value is empty in instance settings page');
+
+    // Check if default_date has been stored successfully.
+    $config_entity = $this->config('field.field.node.date_content.' . $field_name)->get();
+    $this->assertTrue(empty($config_entity['default_value']), 'Empty default value has been stored successfully');
+
+    // Clear field cache in order to avoid stale cache values.
+    \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+
+    // Create a new node to check that datetime field default value is not set.
+    $new_node = Node::create(['type' => 'date_content']);
+    $this->assertNull($new_node->get($field_name)->value, 'Default value is not set');
+  }
+
+  /**
+   * Test that invalid values are caught and marked as invalid.
+   */
+  public function testInvalidField() {
+    // Change the field to a datetime field.
+    $this->fieldStorage->setSetting('daterange_type', DateRangeItem::DATERANGE_TYPE_DATETIME);
+    $this->fieldStorage->save();
+    $field_name = $this->fieldStorage->getName();
+
+    // Display creation form.
+    $this->drupalGet('entity_test/add');
+    $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Start date element found.');
+    $this->assertFieldByName("{$field_name}[0][value][time]", '', 'Start time element found.');
+    $this->assertFieldByName("{$field_name}[0][value2][date]", '', 'End date element found.');
+    $this->assertFieldByName("{$field_name}[0][value2][time]", '', 'End time element found.');
+
+    // Submit invalid start dates and ensure they is not accepted.
+    $date_value = '';
+    $edit = [
+      "{$field_name}[0][value][date]" => $date_value,
+      "{$field_name}[0][value][time]" => '12:00:00',
+      "{$field_name}[0][value2][date]" => '2012-12-01',
+      "{$field_name}[0][value2][time]" => '12:00:00',
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', 'Empty start date value has been caught.');
+
+    $date_value = 'aaaa-12-01';
+    $edit = [
+      "{$field_name}[0][value][date]" => $date_value,
+      "{$field_name}[0][value][time]" => '00:00:00',
+      "{$field_name}[0][value2][date]" => '2012-12-01',
+      "{$field_name}[0][value2][time]" => '12:00:00',
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', format_string('Invalid start year value %date has been caught.', ['%date' => $date_value]));
+
+    $date_value = '2012-75-01';
+    $edit = [
+      "{$field_name}[0][value][date]" => $date_value,
+      "{$field_name}[0][value][time]" => '00:00:00',
+      "{$field_name}[0][value2][date]" => '2012-12-01',
+      "{$field_name}[0][value2][time]" => '12:00:00',
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', format_string('Invalid start month value %date has been caught.', ['%date' => $date_value]));
+
+    $date_value = '2012-12-99';
+    $edit = [
+      "{$field_name}[0][value][date]" => $date_value,
+      "{$field_name}[0][value][time]" => '00:00:00',
+      "{$field_name}[0][value2][date]" => '2012-12-01',
+      "{$field_name}[0][value2][time]" => '12:00:00',
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', format_string('Invalid start day value %date has been caught.', ['%date' => $date_value]));
+
+    // Submit invalid start times and ensure they is not accepted.
+    $time_value = '';
+    $edit = [
+      "{$field_name}[0][value][date]" => '2012-12-01',
+      "{$field_name}[0][value][time]" => $time_value,
+      "{$field_name}[0][value2][date]" => '2012-12-01',
+      "{$field_name}[0][value2][time]" => '12:00:00',
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', 'Empty start time value has been caught.');
+
+    $time_value = '49:00:00';
+    $edit = [
+      "{$field_name}[0][value][date]" => '2012-12-01',
+      "{$field_name}[0][value][time]" => $time_value,
+      "{$field_name}[0][value2][date]" => '2012-12-01',
+      "{$field_name}[0][value2][time]" => '12:00:00',
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', format_string('Invalid start hour value %time has been caught.', ['%time' => $time_value]));
+
+    $time_value = '12:99:00';
+    $edit = [
+      "{$field_name}[0][value][date]" => '2012-12-01',
+      "{$field_name}[0][value][time]" => $time_value,
+      "{$field_name}[0][value2][date]" => '2012-12-01',
+      "{$field_name}[0][value2][time]" => '12:00:00',
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', format_string('Invalid start minute value %time has been caught.', ['%time' => $time_value]));
+
+    $time_value = '12:15:99';
+    $edit = [
+      "{$field_name}[0][value][date]" => '2012-12-01',
+      "{$field_name}[0][value][time]" => $time_value,
+      "{$field_name}[0][value2][date]" => '2012-12-01',
+      "{$field_name}[0][value2][time]" => '12:00:00',
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', format_string('Invalid start second value %time has been caught.', ['%time' => $time_value]));
+
+    // Submit invalid end dates and ensure they is not accepted.
+    $date_value = '';
+    $edit = [
+      "{$field_name}[0][value][date]" => '2012-12-01',
+      "{$field_name}[0][value][time]" => '12:00:00',
+      "{$field_name}[0][value2][date]" => $date_value,
+      "{$field_name}[0][value2][time]" => '12:00:00',
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', 'Empty end date value has been caught.');
+
+    $date_value = 'aaaa-12-01';
+    $edit = [
+      "{$field_name}[0][value][date]" => '2012-12-01',
+      "{$field_name}[0][value][time]" => '12:00:00',
+      "{$field_name}[0][value2][date]" => $date_value,
+      "{$field_name}[0][value2][time]" => '00:00:00',
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', format_string('Invalid end year value %date has been caught.', ['%date' => $date_value]));
+
+    $date_value = '2012-75-01';
+    $edit = [
+      "{$field_name}[0][value][date]" => '2012-12-01',
+      "{$field_name}[0][value][time]" => '12:00:00',
+      "{$field_name}[0][value2][date]" => $date_value,
+      "{$field_name}[0][value2][time]" => '00:00:00',
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', format_string('Invalid end month value %date has been caught.', ['%date' => $date_value]));
+
+    $date_value = '2012-12-99';
+    $edit = [
+      "{$field_name}[0][value][date]" => '2012-12-01',
+      "{$field_name}[0][value][time]" => '12:00:00',
+      "{$field_name}[0][value2][date]" => $date_value,
+      "{$field_name}[0][value2][time]" => '00:00:00',
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', format_string('Invalid end day value %date has been caught.', ['%date' => $date_value]));
+
+    // Submit invalid start times and ensure they is not accepted.
+    $time_value = '';
+    $edit = [
+      "{$field_name}[0][value][date]" => '2012-12-01',
+      "{$field_name}[0][value][time]" => '12:00:00',
+      "{$field_name}[0][value2][date]" => '2012-12-01',
+      "{$field_name}[0][value2][time]" => $time_value,
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', 'Empty end time value has been caught.');
+
+    $time_value = '49:00:00';
+    $edit = [
+      "{$field_name}[0][value][date]" => '2012-12-01',
+      "{$field_name}[0][value][time]" => '12:00:00',
+      "{$field_name}[0][value2][date]" => '2012-12-01',
+      "{$field_name}[0][value2][time]" => $time_value,
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', format_string('Invalid end hour value %time has been caught.', ['%time' => $time_value]));
+
+    $time_value = '12:99:00';
+    $edit = [
+      "{$field_name}[0][value][date]" => '2012-12-01',
+      "{$field_name}[0][value][time]" => '12:00:00',
+      "{$field_name}[0][value2][date]" => '2012-12-01',
+      "{$field_name}[0][value2][time]" => $time_value,
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', format_string('Invalid end minute value %time has been caught.', ['%time' => $time_value]));
+
+    $time_value = '12:15:99';
+    $edit = [
+      "{$field_name}[0][value][date]" => '2012-12-01',
+      "{$field_name}[0][value][time]" => '12:00:00',
+      "{$field_name}[0][value2][date]" => '2012-12-01',
+      "{$field_name}[0][value2][time]" => $time_value,
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', format_string('Invalid end second value %time has been caught.', ['%time' => $time_value]));
+
+    $edit = [
+      "{$field_name}[0][value][date]" => '2012-12-01',
+      "{$field_name}[0][value][time]" => '12:00:00',
+      "{$field_name}[0][value2][date]" => '2010-12-01',
+      "{$field_name}[0][value2][time]" => '12:00:00',
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText(new FormattableMarkup('The @title end date cannot be before the start date', ['@title' => $field_name]), 'End date before start date has been caught.');
+
+    $edit = [
+      "{$field_name}[0][value][date]" => '2012-12-01',
+      "{$field_name}[0][value][time]" => '12:00:00',
+      "{$field_name}[0][value2][date]" => '2012-12-01',
+      "{$field_name}[0][value2][time]" => '11:00:00',
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText(new FormattableMarkup('The @title end date cannot be before the start date', ['@title' => $field_name]), 'End time before start time has been caught.');
+  }
+
+  /**
+   * Tests that 'Date' field storage setting form is disabled if field has data.
+   */
+  public function testDateStorageSettings() {
+    // Create a test content type.
+    $this->drupalCreateContentType(['type' => 'date_content']);
+
+    // Create a field storage with settings to validate.
+    $field_name = Unicode::strtolower($this->randomMachineName());
+    $field_storage = FieldStorageConfig::create([
+      'field_name' => $field_name,
+      'entity_type' => 'node',
+      'type' => 'daterange',
+      'settings' => [
+        'daterange_type' => DateRangeItem::DATERANGE_TYPE_DATE,
+      ],
+    ]);
+    $field_storage->save();
+    $field = FieldConfig::create([
+      'field_storage' => $field_storage,
+      'field_name' => $field_name,
+      'bundle' => 'date_content',
+    ]);
+    $field->save();
+
+    entity_get_form_display('node', 'date_content', 'default')
+      ->setComponent($field_name, [
+        'type' => 'datetime_default',
+      ])
+      ->save();
+    $edit = [
+      'title[0][value]' => $this->randomString(),
+      'body[0][value]' => $this->randomString(),
+      $field_name . '[0][value][date]' => '2016-04-01',
+      $field_name . '[0][value2][date]' => '2016-04-02',
+    ];
+    $this->drupalPostForm('node/add/date_content', $edit, t('Save'));
+    $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name . '/storage');
+    $result = $this->xpath("//*[@id='edit-settings-daterange-type' and contains(@disabled, 'disabled')]");
+    $this->assertEqual(count($result), 1, "Changing daterange setting is disabled.");
+    $this->assertText('There is data for this field in the database. The field settings can no longer be changed.');
+  }
+
+
+  /**
+   * Renders a entity_test and sets the output in the internal browser.
+   *
+   * @param int $id
+   *   The entity_test ID to render.
+   * @param string $view_mode
+   *   (optional) The view mode to use for rendering. Defaults to 'full'.
+   * @param bool $reset
+   *   (optional) Whether to reset the entity_test controller cache. Defaults to
+   *   TRUE to simplify testing.
+   */
+  protected function renderTestEntity($id, $view_mode = 'full', $reset = TRUE) {
+    if ($reset) {
+      \Drupal::service('entity_type.manager')->getStorage('entity_test')->resetCache([$id]);
+    }
+    $entity = EntityTest::load($id);
+    $display = EntityViewDisplay::collectRenderDisplay($entity, $view_mode);
+    $build = $display->build($entity);
+    $output = \Drupal::service('renderer')->renderRoot($build);
+    $this->setRawContent($output);
+    $this->verbose($output);
+  }
+
+  /**
+   * Sets the site timezone to a given timezone.
+   *
+   * @param string $timezone
+   *   The timezone identifier to set.
+   */
+  protected function setSiteTimezone($timezone) {
+    // Set an explicit site timezone, and disallow per-user timezones.
+    $this->config('system.date')
+      ->set('timezone.user.configurable', 0)
+      // A timezone with an offset greater than UTC+12 is used.
+      ->set('timezone.default', $timezone)
+      ->save();
+  }
+
+}
