diff --git a/config/install/datetime_extras.date_range_format.medium.yml b/config/install/datetime_extras.date_range_format.medium.yml
new file mode 100644
index 0000000..073e0b2
--- /dev/null
+++ b/config/install/datetime_extras.date_range_format.medium.yml
@@ -0,0 +1,17 @@
+langcode: en
+status: true
+dependencies: { }
+id: medium
+label: Medium
+date_settings:
+ default_pattern: 'j F Y'
+ separator: '–'
+ same_month_start_pattern: j
+ same_month_end_pattern: 'j F Y'
+ same_year_start_pattern: 'j F'
+ same_year_end_pattern: 'j F Y'
+datetime_settings:
+ default_pattern: 'j F Y H:i'
+ separator: '–'
+ same_day_start_pattern: 'j F Y H:i'
+ same_day_end_pattern: 'H:i'
diff --git a/config/schema/datetime_extras.schema.yml b/config/schema/datetime_extras.schema.yml
new file mode 100644
index 0000000..d9dd2e5
--- /dev/null
+++ b/config/schema/datetime_extras.schema.yml
@@ -0,0 +1,58 @@
+datetime_extras.date_range_format.*:
+ type: config_entity
+ label: 'Date range format config'
+ mapping:
+ id:
+ type: string
+ label: 'ID'
+ label:
+ type: label
+ label: 'Label'
+
+ date_settings:
+ type: mapping
+ label: 'Date settings'
+ mapping:
+ default_pattern:
+ type: string
+ label: 'Default pattern'
+ separator:
+ type: string
+ label: 'Separator'
+ same_month_start_pattern:
+ type: string
+ label: 'Same month start pattern'
+ same_month_end_pattern:
+ type: string
+ label: 'Same month end pattern'
+ same_year_start_pattern:
+ type: string
+ label: 'Same year start pattern'
+ same_year_end_pattern:
+ type: string
+ label: 'Same year end pattern'
+
+ datetime_settings:
+ type: mapping
+ label: 'Datetime settings'
+ mapping:
+ default_pattern:
+ type: string
+ label: 'Default pattern'
+ separator:
+ type: string
+ label: 'Separator'
+ same_day_start_pattern:
+ type: string
+ label: 'Same day start pattern'
+ same_day_end_pattern:
+ type: string
+ label: 'Same day end pattern'
+
+field.formatter.settings.daterange_compact:
+ type: mapping
+ label: 'Date/time range compact display format settings'
+ mapping:
+ format_type:
+ type: string
+ label: 'Date/time range format'
diff --git a/datetime_extras.links.action.yml b/datetime_extras.links.action.yml
new file mode 100644
index 0000000..c4b2efa
--- /dev/null
+++ b/datetime_extras.links.action.yml
@@ -0,0 +1,5 @@
+entity.date_range_format.add_form:
+ route_name: 'entity.date_range_format.add_form'
+ title: 'Add format'
+ appears_on:
+ - entity.date_range_format.collection
diff --git a/datetime_extras.links.menu.yml b/datetime_extras.links.menu.yml
new file mode 100644
index 0000000..c01d8e3
--- /dev/null
+++ b/datetime_extras.links.menu.yml
@@ -0,0 +1,6 @@
+entity.date_range_format.collection:
+ title: 'Date and time range formats'
+ route_name: entity.date_range_format.collection
+ description: 'Configure how date and time ranges are displayed.'
+ parent: system.admin_config_regional
+ weight: 0
diff --git a/datetime_extras.services.yml b/datetime_extras.services.yml
new file mode 100644
index 0000000..321b60d
--- /dev/null
+++ b/datetime_extras.services.yml
@@ -0,0 +1,4 @@
+services:
+ datetime_extras.date_range.formatter:
+ class: Drupal\datetime_extras\DateRangeFormatter
+ arguments: ['@entity_type.manager', '@date.formatter']
diff --git a/src/DateRangeFormatListBuilder.php b/src/DateRangeFormatListBuilder.php
new file mode 100644
index 0000000..2fc2a14
--- /dev/null
+++ b/src/DateRangeFormatListBuilder.php
@@ -0,0 +1,151 @@
+dateRangeFormatter = $date_range_formatter;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
+ return new static(
+ $entity_type,
+ $container->get('entity.manager')->getStorage($entity_type->id()),
+ $container->get('datetime_extras.date_range.formatter')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildHeader() {
+ $header['label'] = $this->t('Name');
+ $header['date'] = $this->t('Date examples');
+ $header['datetime'] = $this->t('Date & time examples');
+ return $header + parent::buildHeader();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildRow(EntityInterface $entity) {
+ /** @var \Drupal\datetime_extras\Entity\DateRangeFormatInterface $format */
+ $format = $entity;
+
+ $row['label'] = $format->label();
+ $row['date']['data'] = $this->dateExamples($format);
+ $row['datetime']['data'] = $this->dateTimeExamples($format);
+ return $row + parent::buildRow($entity);
+ }
+
+ /**
+ * Examples of various date ranges shown using the given format.
+ *
+ * @param \Drupal\datetime_extras\Entity\DateRangeFormatInterface $format
+ * The date range format entity.
+ *
+ * @return array
+ * A render array suitable for use within the list builder table.
+ */
+ private function dateExamples(DateRangeFormatInterface $format) {
+ $examples = [];
+
+ // An example range that is a single day.
+ $same_day_timestamp = \DateTime::createFromFormat('Y-m-d', '2017-01-01')->getTimestamp();
+ $examples[] = $this->dateRangeFormatter->formatDateRange(
+ $same_day_timestamp, $same_day_timestamp, $format->id());
+
+ // An example range that spans several days within the same month.
+ $same_month_start_timestamp = \DateTime::createFromFormat('Y-m-d', '2017-01-02')->getTimestamp();
+ $same_month_end_timestamp = \DateTime::createFromFormat('Y-m-d', '2017-01-03')->getTimestamp();
+ $examples[] = $this->dateRangeFormatter->formatDateRange(
+ $same_month_start_timestamp, $same_month_end_timestamp, $format->id());
+
+ // An example range that spans several months within the same year.
+ $same_year_start_timestamp = \DateTime::createFromFormat('Y-m-d', '2017-01-04')->getTimestamp();
+ $same_year_end_timestamp = \DateTime::createFromFormat('Y-m-d', '2017-02-05')->getTimestamp();
+ $examples[] = $this->dateRangeFormatter->formatDateRange(
+ $same_year_start_timestamp, $same_year_end_timestamp, $format->id());
+
+ // An example range that spans multiple years.
+ $fallback_start_timestamp = \DateTime::createFromFormat('Y-m-d', '2017-01-06')->getTimestamp();
+ $fallback_end_timestamp = \DateTime::createFromFormat('Y-m-d', '2018-01-07')->getTimestamp();
+ $examples[] = $this->dateRangeFormatter->formatDateRange(
+ $fallback_start_timestamp, $fallback_end_timestamp, $format->id());
+
+ $output = '';
+ foreach ($examples as $example) {
+ $output .= htmlspecialchars($example) . '
';
+ }
+ return ['#markup' => $output];
+ }
+
+ /**
+ * Examples of various datetime ranges shown using the given format.
+ *
+ * @param \Drupal\datetime_extras\Entity\DateRangeFormatInterface $format
+ * The date range format entity.
+ *
+ * @return array
+ * A render array suitable for use within the list builder table.
+ */
+ private function dateTimeExamples(DateRangeFormatInterface $format) {
+ $examples = [];
+
+ // An example range that is a single date and time.
+ $same_time_timestamp = \DateTime::createFromFormat('Y-m-d H:i', '2017-01-01 09:00')->getTimestamp();
+ $examples[] = $this->dateRangeFormatter->formatDateTimeRange(
+ $same_time_timestamp, $same_time_timestamp, $format->id());
+
+ // An example range that is contained within a single day.
+ $same_day_start_timestamp = \DateTime::createFromFormat('Y-m-d H:i', '2017-01-01 09:00')->getTimestamp();
+ $same_day_end_timestamp = \DateTime::createFromFormat('Y-m-d H:i', '2017-01-01 13:00')->getTimestamp();
+ $examples[] = $this->dateRangeFormatter->formatDateTimeRange(
+ $same_day_start_timestamp, $same_day_end_timestamp, $format->id());
+
+ // An example range that spans multiple days.
+ $fallback_start_timestamp = \DateTime::createFromFormat('Y-m-d H:i', '2017-01-01 09:00')->getTimestamp();
+ $fallback_end_timestamp = \DateTime::createFromFormat('Y-m-d H:i', '2017-01-02 13:00')->getTimestamp();
+ $examples[] = $this->dateRangeFormatter->formatDateTimeRange(
+ $fallback_start_timestamp, $fallback_end_timestamp, $format->id());
+
+ $output = '';
+ foreach ($examples as $example) {
+ $output .= htmlspecialchars($example) . '
';
+ }
+ return ['#markup' => $output];
+ }
+
+}
diff --git a/src/DateRangeFormatter.php b/src/DateRangeFormatter.php
new file mode 100644
index 0000000..b71fb49
--- /dev/null
+++ b/src/DateRangeFormatter.php
@@ -0,0 +1,135 @@
+dateRangeFormatStorage = $entity_type_manager->getStorage('date_range_format');
+ $this->dateFormatter = $date_formatter;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function formatDateRange($start_timestamp, $end_timestamp, $type = 'medium', $timezone = NULL, $langcode = NULL) {
+ $start_date_time = DrupalDateTime::createFromTimestamp($start_timestamp, $timezone);
+ $end_date_time = DrupalDateTime::createFromTimestamp($end_timestamp, $timezone);
+
+ /** @var \Drupal\datetime_extras\Entity\DateRangeFormatInterface $entity */
+ $entity = $this->dateRangeFormatStorage->load($type);
+ $date_settings = $entity->getDateSettings();
+ $default_pattern = $date_settings['default_pattern'];
+ $separator = $date_settings['separator'] ?: '';
+
+ // Strings containing the ISO-8601 representations of the start and end
+ // date can be used to determine if the day, month or year is the same.
+ $start_iso_8601 = $start_date_time->format('Y-m-d');
+ $end_iso_8601 = $end_date_time->format('Y-m-d');
+
+ if ($start_iso_8601 === $end_iso_8601) {
+ // The range is a single day.
+ return $this->dateFormatter->format($start_timestamp, 'custom',
+ $default_pattern, $timezone, $langcode);
+ }
+ elseif (substr($start_iso_8601, 0, 7) === substr($end_iso_8601, 0, 7)) {
+ // The range spans several days within the same month.
+ $start_pattern = isset($date_settings['same_month_start_pattern']) ? $date_settings['same_month_start_pattern'] : '';
+ $end_pattern = isset($date_settings['same_month_end_pattern']) ? $date_settings['same_month_end_pattern'] : '';
+ if ($start_pattern && $end_pattern) {
+ $start_text = $this->dateFormatter->format($start_timestamp, 'custom', $start_pattern, $timezone, $langcode);
+ $end_text = $this->dateFormatter->format($end_timestamp, 'custom', $end_pattern, $timezone, $langcode);
+ return $start_text . $separator . $end_text;
+ }
+ }
+ elseif (substr($start_iso_8601, 0, 4) === substr($end_iso_8601, 0, 4)) {
+ // The range spans several months within the same year.
+ $start_pattern = isset($date_settings['same_year_start_pattern']) ? $date_settings['same_year_start_pattern'] : '';
+ $end_pattern = isset($date_settings['same_year_end_pattern']) ? $date_settings['same_year_end_pattern'] : '';
+ if ($start_pattern && $end_pattern) {
+ $start_text = $this->dateFormatter->format($start_timestamp, 'custom', $start_pattern, $timezone, $langcode);
+ $end_text = $this->dateFormatter->format($end_timestamp, 'custom', $end_pattern, $timezone, $langcode);
+ return $start_text . $separator . $end_text;
+ }
+ }
+
+ // Fallback: show the start and end dates in full using the default
+ // pattern. This is the case if the range spans different years,
+ // or if the other patterns are not specified.
+ $start_text = $this->dateFormatter->format($start_timestamp, 'custom', $default_pattern, $timezone, $langcode);
+ $end_text = $this->dateFormatter->format($end_timestamp, 'custom', $default_pattern, $timezone, $langcode);
+ return $start_text . $separator . $end_text;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function formatDateTimeRange($start_timestamp, $end_timestamp, $type = 'medium', $timezone = NULL, $langcode = NULL) {
+ $start_date_time = DrupalDateTime::createFromTimestamp($start_timestamp, $timezone);
+ $end_date_time = DrupalDateTime::createFromTimestamp($end_timestamp, $timezone);
+
+ /** @var \Drupal\datetime_extras\Entity\DateRangeFormatInterface $entity */
+ $entity = $this->dateRangeFormatStorage->load($type);
+ $datetime_settings = $entity->getDateTimeSettings();
+ $default_pattern = $datetime_settings['default_pattern'];
+ $separator = $datetime_settings['separator'] ?: '';
+
+ // Strings containing the ISO-8601 representations of the start and end
+ // datetime can be used to determine if the date and/or time are the same.
+ $start_iso_8601 = $start_date_time->format('Y-m-d\TH:i:s');
+ $end_iso_8601 = $end_date_time->format('Y-m-d\TH:i:s');
+
+ if ($start_iso_8601 === $end_iso_8601) {
+ // The range is a single date and time.
+ return $this->dateFormatter->format($start_timestamp, 'custom',
+ $default_pattern, $timezone, $langcode);
+ }
+ elseif (substr($start_iso_8601, 0, 10) == substr($end_iso_8601, 0, 10)) {
+ // The range is contained within a single day.
+ $start_pattern = isset($datetime_settings['same_day_start_pattern']) ? $datetime_settings['same_day_start_pattern'] : '';
+ $end_pattern = isset($datetime_settings['same_day_end_pattern']) ? $datetime_settings['same_day_end_pattern'] : '';
+ if ($start_pattern && $end_pattern) {
+ $start_text = $this->dateFormatter->format($start_timestamp, 'custom', $start_pattern, $timezone, $langcode);
+ $end_text = $this->dateFormatter->format($end_timestamp, 'custom', $end_pattern, $timezone, $langcode);
+ return $start_text . $separator . $end_text;
+ }
+ }
+
+ // Fallback: show the start and end datetimes in full using the default
+ // pattern. This is the case if the range spans different days,
+ // or if the other patterns are not specified.
+ $start_text = $this->dateFormatter->format($start_timestamp, 'custom', $default_pattern, $timezone, $langcode);
+ $end_text = $this->dateFormatter->format($end_timestamp, 'custom', $default_pattern, $timezone, $langcode);
+ return $start_text . $separator . $end_text;
+ }
+
+}
diff --git a/src/DateRangeFormatterInterface.php b/src/DateRangeFormatterInterface.php
new file mode 100644
index 0000000..e1711fa
--- /dev/null
+++ b/src/DateRangeFormatterInterface.php
@@ -0,0 +1,66 @@
+get('date_settings');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDateTimeSettings() {
+ return $this->get('datetime_settings');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCacheTagsToInvalidate() {
+ return ['rendered'];
+ }
+
+}
diff --git a/src/Entity/DateRangeFormatInterface.php b/src/Entity/DateRangeFormatInterface.php
new file mode 100644
index 0000000..cf357d0
--- /dev/null
+++ b/src/Entity/DateRangeFormatInterface.php
@@ -0,0 +1,46 @@
+t('Are you sure you want to delete the date range format %name?', ['%name' => $this->entity->label()]);
+ }
+
+}
diff --git a/src/Form/DateRangeFormatForm.php b/src/Form/DateRangeFormatForm.php
new file mode 100644
index 0000000..19c0355
--- /dev/null
+++ b/src/Form/DateRangeFormatForm.php
@@ -0,0 +1,223 @@
+entity;
+
+ $form['label'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Label'),
+ '#maxlength' => 255,
+ '#default_value' => $format->label(),
+ '#description' => $this->t("Name of the date time range format."),
+ '#required' => TRUE,
+ ];
+
+ $form['id'] = [
+ '#type' => 'machine_name',
+ '#default_value' => $format->id(),
+ '#machine_name' => [
+ 'exists' => '\Drupal\datetime_extras\Entity\DateRangeFormat::load',
+ ],
+ '#disabled' => !$format->isNew(),
+ ];
+
+ $date_settings = $format->getDateSettings();
+
+ $form['date'] = [
+ '#type' => 'vertical_tabs',
+ '#title' => $this->t('Date only formats'),
+ '#tree' => FALSE,
+ ];
+
+ $form['date']['basic'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Basic'),
+ '#open' => TRUE,
+ '#weight' => 1,
+ '#group' => 'date',
+ '#description' => $this->t('Basic date format used for single dates, or ranges that cannot be shown in a compact form.'),
+ ];
+
+ $form['date']['basic']['default_pattern'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Pattern'),
+ '#default_value' => $date_settings['default_pattern'] ?: '',
+ '#maxlength' => 100,
+ '#description' => $this->t('A user-defined date format. See the PHP manual for available options.'),
+ '#required' => TRUE,
+ '#parents' => ['date_settings', 'default_pattern'],
+ ];
+
+ $form['date']['basic']['separator'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Separator'),
+ '#default_value' => $date_settings['separator'] ?: '',
+ '#maxlength' => 100,
+ '#size' => 10,
+ '#description' => $this->t('Text between start and end dates.'),
+ '#required' => FALSE,
+ '#parents' => ['date_settings', 'separator'],
+ ];
+
+ $form['date']['same_month'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Same month'),
+ '#open' => TRUE,
+ '#weight' => 2,
+ '#group' => 'date',
+ '#description' => $this->t('Optional formatting of date ranges that span multiple days within the same month.'),
+ ];
+
+ $form['date']['same_month']['same_month_start_pattern'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Start date pattern'),
+ '#default_value' => $date_settings['same_month_start_pattern'] ?: '',
+ '#maxlength' => 100,
+ '#description' => $this->t('A user-defined date format. See the PHP manual for available options.'),
+ '#parents' => ['date_settings', 'same_month_start_pattern'],
+ ];
+
+ $form['date']['same_month']['same_month_end_pattern'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('End date pattern'),
+ '#default_value' => $date_settings['same_month_end_pattern'] ?: '',
+ '#maxlength' => 100,
+ '#description' => $this->t('A user-defined date format. See the PHP manual for available options.'),
+ '#parents' => ['date_settings', 'same_month_end_pattern'],
+ ];
+
+ $form['date']['same_year'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Same year'),
+ '#open' => TRUE,
+ '#weight' => 2,
+ '#group' => 'date',
+ '#description' => $this->t('Optional formatting of date ranges that span multiple months within the same year.'),
+ ];
+
+ $form['date']['same_year']['same_year_start_pattern'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Start date pattern'),
+ '#default_value' => $date_settings['same_year_start_pattern'] ?: '',
+ '#maxlength' => 100,
+ '#description' => $this->t('A user-defined date format. See the PHP manual for available options.'),
+ '#parents' => ['date_settings', 'same_year_start_pattern'],
+ ];
+
+ $form['date']['same_year']['same_year_end_pattern'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('End date pattern'),
+ '#default_value' => $date_settings['same_year_end_pattern'] ?: '',
+ '#maxlength' => 100,
+ '#description' => $this->t('A user-defined date format. See the PHP manual for available options.'),
+ '#parents' => ['date_settings', 'same_year_end_pattern'],
+ ];
+
+ $datetime_settings = $format->getDateTimeSettings();
+
+ $form['datetime'] = [
+ '#type' => 'vertical_tabs',
+ '#title' => $this->t('Date & time formats'),
+ ];
+
+ $form['datetime']['basic'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Basic'),
+ '#open' => TRUE,
+ '#weight' => 1,
+ '#group' => 'datetime',
+ '#description' => $this->t('Basic date and time format used for single date/times, or ranges that cannot be shown in a compact form.'),
+ ];
+
+ $form['datetime']['basic']['default_pattern'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Pattern'),
+ '#default_value' => $datetime_settings['default_pattern'] ?: '',
+ '#maxlength' => 100,
+ '#description' => $this->t('A user-defined date format. See the PHP manual for available options.'),
+ '#required' => TRUE,
+ '#parents' => ['datetime_settings', 'default_pattern'],
+ ];
+
+ $form['datetime']['basic']['separator'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Separator'),
+ '#default_value' => $datetime_settings['separator'] ?: '',
+ '#maxlength' => 100,
+ '#size' => 10,
+ '#description' => $this->t('Text between start and end date/times.'),
+ '#required' => FALSE,
+ '#parents' => ['datetime_settings', 'separator'],
+ ];
+
+ $form['datetime']['same_day'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Same day'),
+ '#open' => TRUE,
+ '#weight' => 2,
+ '#group' => 'datetime',
+ '#description' => $this->t('Optional formatting of time ranges within a single day.'),
+ ];
+
+ $form['datetime']['same_day']['same_day_start_pattern'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Start date/time pattern'),
+ '#default_value' => $datetime_settings['same_day_start_pattern'] ?: '',
+ '#maxlength' => 100,
+ '#description' => $this->t('A user-defined date format. See the PHP manual for available options.'),
+ '#parents' => ['datetime_settings', 'same_day_start_pattern'],
+ ];
+
+ $form['datetime']['same_day']['same_day_end_pattern'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('End date/time pattern'),
+ '#default_value' => $datetime_settings['same_day_end_pattern'] ?: '',
+ '#maxlength' => 100,
+ '#description' => $this->t('A user-defined date format. See the PHP manual for available options.'),
+ '#parents' => ['datetime_settings', 'same_day_end_pattern'],
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save(array $form, FormStateInterface $form_state) {
+ $date_range_format = $this->entity;
+ $status = $date_range_format->save();
+
+ switch ($status) {
+ case SAVED_NEW:
+ drupal_set_message($this->t('Created the %label date range format.', [
+ '%label' => $date_range_format->label(),
+ ]));
+ break;
+
+ default:
+ drupal_set_message($this->t('Updated the %label date range format.', [
+ '%label' => $date_range_format->label(),
+ ]));
+ }
+ $form_state->setRedirectUrl($date_range_format->toUrl('collection'));
+ }
+
+}
diff --git a/src/Plugin/Field/FieldFormatter/DateRangeCompactFormatter.php b/src/Plugin/Field/FieldFormatter/DateRangeCompactFormatter.php
new file mode 100644
index 0000000..d6d9f9e
--- /dev/null
+++ b/src/Plugin/Field/FieldFormatter/DateRangeCompactFormatter.php
@@ -0,0 +1,168 @@
+ elements, with
+ * configurable date formats (from the list of configured formats) and a
+ * separator.
+ *
+ * @FieldFormatter(
+ * id = "daterange_compact",
+ * label = @Translation("Compact"),
+ * field_types = {
+ * "daterange"
+ * }
+ * )
+ */
+class DateRangeCompactFormatter extends FormatterBase implements ContainerFactoryPluginInterface {
+
+ /**
+ * The date range formatter service.
+ *
+ * @var \Drupal\datetime_extras\DateRangeFormatterInterface
+ */
+ protected $dateRangeFormatter;
+
+ /**
+ * The date range format entity storage.
+ *
+ * @var \Drupal\Core\Entity\EntityStorageInterface
+ */
+ protected $dateRangeFormatStorage;
+
+ /**
+ * Constructs a new DateRangeCompactFormatter.
+ *
+ * @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\datetime_extras\DateRangeFormatterInterface $date_range_formatter
+ * The date formatter service.
+ * @param \Drupal\Core\Entity\EntityStorageInterface $date_range_format_storage
+ * The date format entity storage.
+ */
+ public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, DateRangeFormatterInterface $date_range_formatter, EntityStorageInterface $date_range_format_storage) {
+ parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
+
+ $this->dateRangeFormatter = $date_range_formatter;
+ $this->dateRangeFormatStorage = $date_range_format_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['label'],
+ $configuration['view_mode'],
+ $configuration['third_party_settings'],
+ $container->get('datetime_extras.date_range.formatter'),
+ $container->get('entity_type.manager')->getStorage('date_range_format')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function defaultSettings() {
+ return [
+ 'format_type' => 'medium',
+ ] + parent::defaultSettings();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsForm(array $form, FormStateInterface $form_state) {
+ $form = parent::settingsForm($form, $form_state);
+
+ $format_types = $this->dateRangeFormatStorage->loadMultiple();
+ $options = [];
+ foreach ($format_types as $type => $type_info) {
+ $options[$type] = $type_info->label();
+ }
+
+ $form['format_type'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Date and time range format'),
+ '#description' => $this->t("Choose a format for displaying the date and time range."),
+ '#options' => $options,
+ '#default_value' => $this->getSetting('format_type'),
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsSummary() {
+ $summary = parent::settingsSummary();
+ $summary[] = $this->t('Format: @format', ['@format' => $this->getSetting('format_type')]);
+ return $summary;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function viewElements(FieldItemListInterface $items, $langcode) {
+ $elements = [];
+
+ foreach ($items as $delta => $item) {
+ if (!empty($item->start_date) && !empty($item->end_date)) {
+ $start_timestamp = $item->start_date->getTimestamp();
+ $end_timestamp = $item->end_date->getTimestamp();
+ $format = $this->getSetting('format_type');
+
+ if ($this->getFieldSetting('datetime_type') == DateTimeItem::DATETIME_TYPE_DATE) {
+ $timezone = DATETIME_STORAGE_TIMEZONE;
+ $text = $this->dateRangeFormatter->formatDateRange($start_timestamp, $end_timestamp, $format, $timezone);
+ }
+ else {
+ $timezone = drupal_get_user_timezone();
+ $text = $this->dateRangeFormatter->formatDateTimeRange($start_timestamp, $end_timestamp, $format, $timezone);
+ }
+
+ $elements[$delta] = [
+ '#plain_text' => $text,
+ '#cache' => [
+ 'contexts' => [
+ 'timezone',
+ ],
+ ],
+ ];
+ }
+ }
+
+ return $elements;
+ }
+
+}
diff --git a/src/Tests/DateRangeCompactFormatterTest.php b/src/Tests/DateRangeCompactFormatterTest.php
new file mode 100644
index 0000000..c707449
--- /dev/null
+++ b/src/Tests/DateRangeCompactFormatterTest.php
@@ -0,0 +1,369 @@
+installConfig(['system']);
+ $this->installConfig(['field']);
+ $this->installConfig(['datetime_extras']);
+ $this->installEntitySchema('entity_test');
+
+ $this->entityType = 'entity_test';
+ $this->bundle = $this->entityType;
+ $this->dateFieldName = Unicode::strtolower($this->randomMachineName());
+ $this->dateTimeFieldName = Unicode::strtolower($this->randomMachineName());
+
+ // Create a typical format for USA.
+ $usa_format = DateRangeFormat::create([
+ 'id' => 'usa',
+ 'label' => 'USA',
+ 'date_settings' => [
+ 'default_pattern' => 'F jS, Y',
+ 'separator' => ' - ',
+ 'same_month_start_pattern' => 'F jS',
+ 'same_month_end_pattern' => 'jS, Y',
+ 'same_year_start_pattern' => 'F jS',
+ 'same_year_end_pattern' => 'F jS, Y',
+ ],
+ 'datetime_settings' => [
+ 'default_pattern' => 'g:ia \o\n F jS, Y',
+ 'separator' => ' - ',
+ 'same_day_start_pattern' => 'g:ia',
+ 'same_day_end_pattern' => 'g:ia \o\n F jS, Y',
+ ],
+ ]);
+ $usa_format->save();
+
+ // Create a ISO-8601 format without any compact variations.
+ $iso_8601_format = DateRangeFormat::create([
+ 'id' => 'iso_8601',
+ 'label' => 'ISO-8601',
+ 'date_settings' => [
+ 'default_pattern' => 'Y-m-d',
+ 'separator' => ' - ',
+ ],
+ 'datetime_settings' => [
+ 'default_pattern' => 'Y-m-d\TH:i:s',
+ 'separator' => ' - ',
+ ],
+ ]);
+ $iso_8601_format->save();
+
+ $date_field_storage = FieldStorageConfig::create([
+ 'field_name' => $this->dateFieldName,
+ 'entity_type' => $this->entityType,
+ 'type' => 'daterange',
+ 'settings' => [
+ 'datetime_type' => DateTimeItem::DATETIME_TYPE_DATE,
+ ],
+ ]);
+ $date_field_storage->save();
+
+ $date_field_instance = FieldConfig::create([
+ 'field_storage' => $date_field_storage,
+ 'bundle' => $this->bundle,
+ 'label' => $this->randomMachineName(),
+ ]);
+ $date_field_instance->save();
+
+ $date_time_field_storage = FieldStorageConfig::create([
+ 'field_name' => $this->dateTimeFieldName,
+ 'entity_type' => $this->entityType,
+ 'type' => 'daterange',
+ 'settings' => [
+ 'datetime_type' => DateTimeItem::DATETIME_TYPE_DATETIME,
+ ],
+ ]);
+ $date_time_field_storage->save();
+
+ $date_time_field_instance = FieldConfig::create([
+ 'field_storage' => $date_time_field_storage,
+ 'bundle' => $this->bundle,
+ 'label' => $this->randomMachineName(),
+ ]);
+ $date_time_field_instance->save();
+
+ $this->defaultDisplay = EntityViewDisplay::create([
+ 'targetEntityType' => $this->entityType,
+ 'bundle' => $this->bundle,
+ 'mode' => 'default',
+ 'status' => TRUE,
+ ]);
+ $this->defaultDisplay->setComponent($this->dateFieldName, [
+ 'type' => 'daterange_compact',
+ 'settings' => [
+ 'format_type' => 'medium',
+ ],
+ ]);
+ $this->defaultDisplay->setComponent($this->dateTimeFieldName, [
+ 'type' => 'daterange_compact',
+ 'settings' => [
+ 'format_type' => 'medium',
+ ],
+ ]);
+ $this->defaultDisplay->save();
+ }
+
+ /**
+ * Renders fields of a given entity with a given display.
+ *
+ * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
+ * The entity object with attached fields to render.
+ * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display
+ * The display to render the fields in.
+ *
+ * @return string
+ * The rendered entity fields.
+ */
+ protected function renderEntityFields(FieldableEntityInterface $entity, EntityViewDisplayInterface $display) {
+ $content = $display->build($entity);
+ $content = $this->render($content);
+ return $content;
+ }
+
+ /**
+ * Tests the display of date-only range fields.
+ */
+ public function testDateRanges() {
+ $all_data = [];
+
+ // Same day.
+ $all_data[] = [
+ 'start' => '2017-01-01',
+ 'end' => '2017-01-01',
+ 'expected' => [
+ 'default' => '1 January 2017',
+ 'usa' => 'January 1st, 2017',
+ 'iso_8601' => '2017-01-01',
+ ],
+ ];
+
+ // Different days, same month.
+ $all_data[] = [
+ 'start' => '2017-01-02',
+ 'end' => '2017-01-03',
+ 'expected' => [
+ 'default' => '2–3 January 2017',
+ 'usa' => 'January 2nd - 3rd, 2017',
+ 'iso_8601' => '2017-01-02 - 2017-01-03',
+ ],
+ ];
+
+ // Different months, same year.
+ $all_data[] = [
+ 'start' => '2017-01-04',
+ 'end' => '2017-02-05',
+ 'expected' => [
+ 'default' => '4 January–5 February 2017',
+ 'usa' => 'January 4th - February 5th, 2017',
+ 'iso_8601' => '2017-01-04 - 2017-02-05',
+ ],
+ ];
+
+ // Different years.
+ $all_data[] = [
+ 'start' => '2017-01-06',
+ 'end' => '2018-02-07',
+ 'expected' => [
+ 'default' => '6 January 2017–7 February 2018',
+ 'usa' => 'January 6th, 2017 - February 7th, 2018',
+ 'iso_8601' => '2017-01-06 - 2018-02-07',
+ ],
+ ];
+
+ foreach ($all_data as $data) {
+ foreach ($data['expected'] as $format => $expected) {
+ $entity = EntityTest::create([]);
+ $entity->{$this->dateFieldName}->value = $data['start'];
+ $entity->{$this->dateFieldName}->end_value = $data['end'];
+
+ if ($format === 'default') {
+ $this->renderEntityFields($entity, $this->defaultDisplay);
+ }
+ else {
+ // For testing a custom format, don't use the entity's default
+ // display, but instead render the field using a temporary one.
+ $display = $this->buildCustomDisplay($this->dateFieldName, $format);
+ $this->renderEntityFields($entity, $display);
+ }
+
+ $this->assertRaw($expected);
+ }
+ }
+ }
+
+ /**
+ * Tests the display of date and time range fields.
+ */
+ public function testDateTimeRanges() {
+ $all_data = [];
+
+ // Note: the default timezone for unit tests is Australia/Sydney
+ // see https://www.drupal.org/node/2498619 for why
+ // Australia/Sydney is UTC +10:00 (normal) or UTC +11:00 (DST)
+ // DST starts first Sunday in October
+ // DST ends first Sunday in April.
+ // Same day.
+ $all_data[] = [
+ 'start' => '2017-01-01T09:00:00',
+ 'end' => '2017-01-01T12:00:00',
+ 'expected' => [
+ 'default' => '1 January 2017 20:00–23:00',
+ 'usa' => '8:00pm - 11:00pm on January 1st, 2017',
+ 'iso_8601' => '2017-01-01T20:00:00 - 2017-01-01T23:00:00',
+ ],
+ ];
+
+ // Different day in UTC, same day in Australia.
+ $all_data[] = [
+ 'start' => '2017-01-01T23:00:00',
+ 'end' => '2017-01-02T01:00:00',
+ 'expected' => [
+ 'default' => '2 January 2017 10:00–12:00',
+ 'usa' => '10:00am - 12:00pm on January 2nd, 2017',
+ 'iso_8601' => '2017-01-02T10:00:00 - 2017-01-02T12:00:00',
+ ],
+ ];
+
+ // Same day in UTC, different day in Australia.
+ $all_data[] = [
+ 'start' => '2017-01-01T12:00:00',
+ 'end' => '2017-01-01T15:00:00',
+ 'expected' => [
+ 'default' => '1 January 2017 23:00–2 January 2017 02:00',
+ 'usa' => '11:00pm on January 1st, 2017 - 2:00am on January 2nd, 2017',
+ 'iso_8601' => '2017-01-01T23:00:00 - 2017-01-02T02:00:00',
+ ],
+ ];
+
+ // Different days in UTC and Australia, also spans DST change.
+ $all_data[] = [
+ 'start' => '2017-04-01T01:00:00',
+ 'end' => '2017-04-08T01:00:00',
+ 'expected' => [
+ 'default' => '1 April 2017 12:00–8 April 2017 11:00',
+ 'usa' => '12:00pm on April 1st, 2017 - 11:00am on April 8th, 2017',
+ 'iso_8601' => '2017-04-01T12:00:00 - 2017-04-08T11:00:00',
+ ],
+ ];
+
+ foreach ($all_data as $data) {
+ foreach ($data['expected'] as $format => $expected) {
+ $entity = EntityTest::create([]);
+ $entity->{$this->dateTimeFieldName}->value = $data['start'];
+ $entity->{$this->dateTimeFieldName}->end_value = $data['end'];
+
+ if ($format === 'default') {
+ $this->renderEntityFields($entity, $this->defaultDisplay);
+ }
+ else {
+ // For testing a custom format, don't use the entity's default
+ // display, but instead render the field using a temporary one.
+ $display = $this->buildCustomDisplay($this->dateTimeFieldName, $format);
+ $this->renderEntityFields($entity, $display);
+ }
+
+ $this->assertRaw($expected);
+ }
+ }
+ }
+
+ /**
+ * Helper function that creates a display object for the given format.
+ *
+ * @param string $field_name
+ * The name of the field.
+ * @param string $format
+ * The name of the format.
+ *
+ * @return \Drupal\Core\Entity\Display\EntityViewDisplayInterface
+ * A temporary display object.
+ */
+ private function buildCustomDisplay($field_name, $format) {
+ $display = EntityViewDisplay::create([
+ 'targetEntityType' => $this->entityType,
+ 'bundle' => $this->bundle,
+ 'mode' => 'default',
+ 'status' => TRUE,
+ ]);
+ $display->setComponent($field_name, [
+ 'type' => 'daterange_compact',
+ 'settings' => [
+ 'format_type' => $format,
+ ],
+ ]);
+ return $display;
+ }
+
+}