diff --git a/controlled_access_terms.module b/controlled_access_terms.module
index a5fb060..50372ef 100644
--- a/controlled_access_terms.module
+++ b/controlled_access_terms.module
@@ -73,6 +73,39 @@ function controlled_access_terms_jsonld_alter_normalized_array(EntityInterface $
}
}
+/**
+ * Update EDTF fields from the 2012 draft to match the 2018 spec.
+ */
+function controlled_access_terms_update_8003() {
+ $db = \Drupal::database();
+
+ // Find all the fields using edtf.
+ $config_factory = \Drupal::configFactory();
+ foreach ($config_factory->listAll('field.storage.') as $field_storage_config_name) {
+ $field_storage_config = $config_factory->get($field_storage_config_name);
+ if ($field_storage_config->get('type') === 'edtf') {
+
+ // Run through each update. Make sure 'unknown' is updated before 'u'.
+ $updates = [
+ 'open' => '..',
+ 'unknown' => '',
+ 'y' => 'Y',
+ 'u' => 'X',
+ '?~' => '%',
+ '~?' => '%',
+ ];
+ foreach ($updates as $old => $new) {
+ $db->update($field_storage_config->get('entity_type') . '__' . $field_storage_config->get('field_name'))
+ ->expression($field_storage_config->get('field_name') . '_value', 'replace(' . $field_storage_config->get('field_name') . '_value, :old, :new)', [
+ ':old' => $old,
+ ':new' => $new,
+ ])
+ ->execute();
+ }
+ }
+ }
+}
+
/**
* Change fields using the EDTF Widget to the new EDTF Field Type.
*/
diff --git a/modules/controlled_access_terms_default_configuration/config/install/core.entity_view_display.taxonomy_term.corporate_body.default.yml b/modules/controlled_access_terms_default_configuration/config/install/core.entity_view_display.taxonomy_term.corporate_body.default.yml
index 3133cd5..ae89e1a 100644
--- a/modules/controlled_access_terms_default_configuration/config/install/core.entity_view_display.taxonomy_term.corporate_body.default.yml
+++ b/modules/controlled_access_terms_default_configuration/config/install/core.entity_view_display.taxonomy_term.corporate_body.default.yml
@@ -45,7 +45,6 @@ content:
date_order: big_endian
month_format: mm
day_format: dd
- season_hemisphere: north
third_party_settings: { }
type: edtf_default
region: content
@@ -57,7 +56,6 @@ content:
date_order: big_endian
month_format: mm
day_format: dd
- season_hemisphere: north
third_party_settings: { }
type: edtf_default
region: content
diff --git a/modules/controlled_access_terms_default_configuration/config/install/core.entity_view_display.taxonomy_term.family.default.yml b/modules/controlled_access_terms_default_configuration/config/install/core.entity_view_display.taxonomy_term.family.default.yml
index 05d4f07..5d90208 100644
--- a/modules/controlled_access_terms_default_configuration/config/install/core.entity_view_display.taxonomy_term.family.default.yml
+++ b/modules/controlled_access_terms_default_configuration/config/install/core.entity_view_display.taxonomy_term.family.default.yml
@@ -42,7 +42,6 @@ content:
date_order: big_endian
month_format: mm
day_format: dd
- season_hemisphere: north
third_party_settings: { }
type: edtf_default
region: content
@@ -54,7 +53,6 @@ content:
date_order: big_endian
month_format: mm
day_format: dd
- season_hemisphere: north
third_party_settings: { }
type: edtf_default
region: content
diff --git a/modules/controlled_access_terms_default_configuration/config/install/core.entity_view_display.taxonomy_term.person.default.yml b/modules/controlled_access_terms_default_configuration/config/install/core.entity_view_display.taxonomy_term.person.default.yml
index 5f7245f..c71da8d 100644
--- a/modules/controlled_access_terms_default_configuration/config/install/core.entity_view_display.taxonomy_term.person.default.yml
+++ b/modules/controlled_access_terms_default_configuration/config/install/core.entity_view_display.taxonomy_term.person.default.yml
@@ -45,7 +45,6 @@ content:
date_order: little_endian
month_format: mmm
day_format: dd
- season_hemisphere: north
third_party_settings: { }
type: edtf_default
region: content
@@ -57,7 +56,6 @@ content:
date_order: little_endian
month_format: mmm
day_format: dd
- season_hemisphere: north
third_party_settings: { }
type: edtf_default
region: content
diff --git a/src/EDTFConverter.php b/src/EDTFConverter.php
index 85a278e..11db6d2 100644
--- a/src/EDTFConverter.php
+++ b/src/EDTFConverter.php
@@ -9,40 +9,6 @@
*/
class EDTFConverter extends CommonDataConverter {
- /**
- * Northern hemisphere season map.
- *
- * @var array
- */
- private $seasonMapNorth = [
- // Spring => March.
- '21' => '03',
- // Summer => June.
- '22' => '06',
- // Autumn => September.
- '23' => '09',
- // Winter => December.
- '24' => '12',
- ];
-
- /**
- * Southern hemisphere season map.
- *
- * (Currently unused until a config for this is established.)
- *
- * @var array
- */
- private $seasonMapSouth = [
- // Spring => September.
- '21' => '03',
- // Summer => December.
- '22' => '06',
- // Autumn => March.
- '23' => '09',
- // Winter => June.
- '24' => '12',
- ];
-
/**
* Converts an EDTF text field into an ISO 8601 timestamp string.
*
@@ -55,19 +21,11 @@ class EDTFConverter extends CommonDataConverter {
* Returns the ISO 8601 timestamp.
*/
public static function datetimeIso8601Value(array $data) {
- $date = explode('/', $data['value'])[0];
- // Strip approximations/uncertainty.
- $date = str_replace(['?', '~'], '', $date);
+ // Take first possible date.
+ $date = preg_split('/(,|\.\.|\/)/', trim($data['value'], '{}[]'))[0];
- // Replace unspecified.
- // Month/day.
- $date = str_replace('-uu', '-01', $date);
- // Zero-Year in decade/century.
- $date = str_replace('u', '0', $date);
-
- // Seasons map.
- return EDTFConverter::seasonsMap($date) . 'T00:00:00';
+ return EDTFUtils::iso8601Value($date);
}
@@ -83,43 +41,8 @@ public static function datetimeIso8601Value(array $data) {
* Returns the ISO 8601 date.
*/
public static function dateIso8601Value(array $data) {
- $date = explode('/', $data['value'])[0];
-
- // Strip approximations/uncertainty.
- $date = str_replace(['?', '~'], '', $date);
-
- // Remove unspecified.
- // Month/day.
- $date = str_replace('-uu', '', $date);
- // Zero-Year in decade/century.
- $date = str_replace('u', '0', $date);
-
- // Seasons map.
- return EDTFConverter::seasonsMap($date);
-
- }
-
- /**
- * Converts a numeric season into a numeric month.
- *
- * @param string $date
- * The date string to convert.
- *
- * @return string
- * Returns the ISO 8601 date with the correct month.
- */
- protected static function seasonsMap(string $date) {
- $date_parts[] = explode('-', $date, 3);
- // Digit Seasons.
- if ((count($date_parts) > 1) &&
- in_array($date_parts[1], ['21', '22', '23', '24'])) {
- // TODO: Make hemisphere seasons configurable.
- $season_mapping = $seasonMapNorth;
- $date_parts[1] = $season_mapping[$date_parts[1]];
- $date = implode('-', array_filter($date_parts));
- }
- return $date;
+ return explode('T', EDTFConverter::datetimeIso8601Value($data))[0];
}
diff --git a/src/EDTFUtils.php b/src/EDTFUtils.php
new file mode 100644
index 0000000..b6113db
--- /dev/null
+++ b/src/EDTFUtils.php
@@ -0,0 +1,358 @@
+ ['mmm' => 'Jan', 'mmmm' => 'January'],
+ '02' => ['mmm' => 'Feb', 'mmmm' => 'February'],
+ '03' => ['mmm' => 'Mar', 'mmmm' => 'March'],
+ '04' => ['mmm' => 'Apr', 'mmmm' => 'April'],
+ '05' => ['mmm' => 'May', 'mmmm' => 'May'],
+ '06' => ['mmm' => 'Jun', 'mmmm' => 'June'],
+ '07' => ['mmm' => 'Jul', 'mmmm' => 'July'],
+ '08' => ['mmm' => 'Aug', 'mmmm' => 'August'],
+ '09' => ['mmm' => 'Sep', 'mmmm' => 'September'],
+ '10' => ['mmm' => 'Oct', 'mmmm' => 'October'],
+ '11' => ['mmm' => 'Nov', 'mmmm' => 'November'],
+ '12' => ['mmm' => 'Dec', 'mmmm' => 'December'],
+ '21' => ['mmm' => 'Spr', 'mmmm' => 'Spring'],
+ '22' => ['mmm' => 'Sum', 'mmmm' => 'Summer'],
+ '23' => ['mmm' => 'Aut', 'mmmm' => 'Autumn'],
+ '24' => ['mmm' => 'Win', 'mmmm' => 'Winter'],
+ '25' => ['mmm' => 'Spr', 'mmmm' => 'Spring - Northern Hemisphere'],
+ '26' => ['mmm' => 'Sum', 'mmmm' => 'Summer - Northern Hemisphere'],
+ '27' => ['mmm' => 'Aut', 'mmmm' => 'Autumn - Northern Hemisphere'],
+ '28' => ['mmm' => 'Win', 'mmmm' => 'Winter - Northern Hemisphere'],
+ '29' => ['mmm' => 'Spr', 'mmmm' => 'Spring - Southern Hemisphere'],
+ '30' => ['mmm' => 'Sum', 'mmmm' => 'Summer - Southern Hemisphere'],
+ '31' => ['mmm' => 'Aut', 'mmmm' => 'Autumn - Southern Hemisphere'],
+ '32' => ['mmm' => 'Win', 'mmmm' => 'Winter - Southern Hemisphere'],
+ '33' => ['mmm' => 'Q1', 'mmmm' => 'Quarter 1'],
+ '34' => ['mmm' => 'Q2', 'mmmm' => 'Quarter 2'],
+ '35' => ['mmm' => 'Q3', 'mmmm' => 'Quarter 3'],
+ '36' => ['mmm' => 'Q4', 'mmmm' => 'Quarter 4'],
+ // I'm making up the rest of these abbreviations
+ // because I can't find standardized ones.
+ '37' => ['mmm' => 'Quad1', 'mmmm' => 'Quadrimester 1'],
+ '38' => ['mmm' => 'Quad2', 'mmmm' => 'Quadrimester 2'],
+ '39' => ['mmm' => 'Quad3', 'mmmm' => 'Quadrimester 3'],
+ '40' => ['mmm' => 'Sem1', 'mmmm' => 'Semestral 1'],
+ '41' => ['mmm' => 'Sem2', 'mmmm' => 'Semestral 2'],
+ ];
+
+ const SEASONS_MAP = [
+ // Northern Hemisphere bias for 21-24.
+ '21' => '03',
+ '22' => '06',
+ '23' => '09',
+ '24' => '12',
+ // Northern seasons.
+ '25' => '03',
+ '26' => '06',
+ '27' => '09',
+ '28' => '12',
+ // Southern seasons.
+ '29' => '09',
+ '30' => '12',
+ '31' => '03',
+ '32' => '06',
+ // Quarters.
+ '33' => '01',
+ '34' => '04',
+ '35' => '07',
+ '36' => '10',
+ // Quadrimesters.
+ '37' => '01',
+ '38' => '05',
+ '39' => '09',
+ // Semesters.
+ '40' => '01',
+ '41' => '07',
+ ];
+
+ /**
+ * Southern hemisphere season map.
+ *
+ * (Currently unused until a config for this is established.)
+ *
+ * @var array
+ */
+ private $seasonMapSouth = [
+ // Spring => September.
+ '21' => '03',
+ // Summer => December.
+ '22' => '06',
+ // Autumn => March.
+ '23' => '09',
+ // Winter => June.
+ '24' => '12',
+ ];
+
+ /**
+ * Validate an EDTF expression.
+ *
+ * @param string $edtf_text
+ * The datetime string.
+ * @param bool $intervals
+ * Are interval expressions permitted?
+ * @param bool $sets
+ * Are set expressions permitted?
+ * @param bool $strict
+ * Are only valid calendar dates permitted?
+ *
+ * @return array
+ * Array of error messages. Valid if empty.
+ */
+ public static function validate($edtf_text, $intervals = TRUE, $sets = TRUE, $strict = FALSE) {
+ $msgs = [];
+ // Sets.
+ if ($sets) {
+ if (strpos($edtf_text, '[') !== FALSE || strpos($edtf_text, '{') !== FALSE) {
+ // Test for valid enclosing characters and valid characters inside.
+ $match = preg_match('/^([\[,\{])[\d,\-,X,Y,E,S,.]*([\],\}])$/', $edtf_text);
+ if (!$match || $match[1] !== $match[2]) {
+ $msgs[] = "The set is improperly encoded.";
+ }
+ // Test each date in set.
+ foreach (preg_split('/(,|\.\.)/', trim($edtf_text, '{}[]')) as $date) {
+ if (!empty($date)) {
+ $msgs = array_merge($msgs, self::validateDate($date, $strict));
+ }
+ }
+ return $msgs;
+ }
+ }
+ // Intervals.
+ if ($intervals) {
+ if (strpos($edtf_text, 'T') !== FALSE) {
+ $msgs[] = "Date intervals cannot include times.";
+ }
+ foreach (explode('/', $$edtf_text) as $date) {
+ if (!empty($date) && !$date === '..') {
+ $msgs = array_merge($msgs, self::validateDate($date, $strict));
+ }
+ }
+ return $msgs;
+ }
+ // Single date (we assume at this point).
+ return self::validateDate($edtf_text, $strict);
+ }
+
+ /**
+ * Validate a single date.
+ *
+ * @param string $datetime_str
+ * The datetime string.
+ * @param bool $strict
+ * Are only valid calendar dates permitted?
+ *
+ * @return array
+ * Array of error messages. Valid if empty.
+ */
+ public static function validateDate($datetime_str, $strict = FALSE) {
+ $msgs = [];
+
+ list($date, $time) = explode('T', $datetime_str);
+
+ preg_match(self::DATE_PARSE_REGEX, $date, $parsed_date);
+
+ // "Something" is wrong with the provided date if it doesn't round-trip.
+ // Includes (non-exhaustive):
+ // - no invalid characters present,
+ // - two-digit months and days, and
+ // - capturing group qualifiers.
+ if ($date !== $parsed_date[self::FULL_MATCH]) {
+ $msgs[] = "Could not parse the date '$date'";
+ }
+
+ // Year.
+ if (strpos($parsed_date[self::YEAR_FULL], 'Y') === 0) {
+ if ($strict) {
+ $msgs[] = "Extended years are not supported with the 'strict dates' option enabled.";
+ }
+ // Expand exponents.
+ if (!empty($parsed_date[self::YEAR_EXPONENT])) {
+ $exponent = intval(substr($parsed_date[self::YEAR_EXPONENT], 1));
+ $parsed_date[self::YEAR_BASE] = strval((10 ** $exponent) * intval($parsed_date[self::YEAR_BASE]));
+ $parsed_date[self::YEAR_BASE] = self::expandYear($parsed_date[self::YEAR_FULL], $parsed_date[self::YEAR_BASE], $parsed_date[self::YEAR_EXPONENT]);
+ }
+ }
+ elseif (strlen(ltrim($parsed_date[self::YEAR_BASE], '-')) > 4) {
+ $msgs[] = "Years longer than 4 digits must be prefixed with a 'Y'.";
+ }
+ elseif (strlen($parsed_date[self::YEAR_BASE]) < 4) {
+ $msgs[] = "Years must be at least 4 characters long.";
+ }
+ $strict_pattern = 'Y';
+
+ // Month.
+ if (!array_key_exists(self::MONTH, $parsed_date) && !empty($parsed_date[self::MONTH])) {
+ // Valid month values?
+ if (!array_key_exists($parsed_date[self::MONTH], self::MONTHS_MAP) &&
+ strpos($parsed_date[self::MONTH], 'X') === FALSE) {
+ $msgs[] = "Provided month value '" . $parsed_date[self::MONTH] . "' is not valid.";
+ }
+ $strict_pattern = 'Y-m';
+ }
+
+ // Day.
+ if (!array_key_exists(self::DAY) && !empty($parsed_date[self::DAY])) {
+ // Valid day values?
+ if (strpos($parsed_date[self::DAY], 'X') === FALSE &&
+ !in_array(intval($parsed_date[self::DAY]), range(1, 31))) {
+ $msgs[] = "Provided day value '" . $parsed_date[self::DAY] . "' is not valid.";
+ }
+ $strict_pattern = 'Y-m-d';
+ }
+ // Time.
+ if (strpos($datetime_str, 'T') !== FALSE && empty($time)) {
+ $msgs[] = "Time not provided with time seperator (T).";
+ }
+
+ if ($time) {
+ if (!preg_match('/^-?(\d{4})(-\d{2}){2}T\d{2}(:\d{2}){2}(Z|(\+|-)\d{2}:\d{2})?$/', $datetime_str, $matches)) {
+ $msgs[] = "The date/time '$datetime_str' is invalid.";
+ }
+ $strict_pattern = 'Y-m-d\TH:i:s';
+ if (count($matches) > 4) {
+ if ($matches[4] === 'Z') {
+ $strict_pattern .= '\Z';
+ }
+ else {
+ $strict_pattern .= 'P';
+ }
+ }
+ }
+
+ if ($strict) {
+ // Assemble the parts again.
+ if ($time) {
+ $cleaned_datetime = $datetime_str;
+ }
+ else {
+ $cleaned_datetime = implode('-', [
+ $parsed_date[self::YEAR_BASE],
+ $parsed_date[self::MONTH],
+ $parsed_date[self::DAY],
+ ]);
+ }
+ $datetime_obj = DateTime::createFromFormat('!' . $strict_pattern, $cleaned_datetime);
+ $errors = DateTime::getLastErrors();
+ if (!$datetime_obj ||
+ !empty($errors['warning_count']) ||
+ // DateTime will create valid dates from Y-m without warning,
+ // so validate we still have what it was given.
+ !($cleaned_datetime === $datetime_obj->format($strict_pattern))
+ ) {
+ $msgs[] = "Strictly speaking, the date (and/or time) '$datetime_str' is invalid.";
+ }
+ }
+
+ return $msgs;
+ }
+
+ /**
+ * Expand an exponent year.
+ *
+ * @param string $year_full
+ * The full year expression from the EDTF string.
+ * @param string $year_base
+ * The base expression from the EDTF string.
+ * @param string $year_exponent
+ * The exponent expression from the EDTF string.
+ *
+ * @return string
+ * The expanded year value.
+ */
+ public static function expandYear($year_full, $year_base, $year_exponent) {
+ if (!empty($year_exponent)) {
+ $exponent = intval(substr($year_exponent, 1));
+ return strval((10 ** $exponent) * intval($year_base));
+ }
+ else {
+ return $year_base;
+ }
+ }
+
+ /**
+ * Converts an EDTF string into an ISO 8601 timestamp string.
+ *
+ * @param string $edtf
+ * The array containing the 'value' element.
+ *
+ * @return string
+ * Returns the ISO 8601 timestamp.
+ */
+ public static function iso8601Value(string $edtf) {
+
+ $date_time = explode('T', $edtf);
+
+ preg_match(EDTFUtils::DATE_PARSE_REGEX, $date_time[0], $parsed_date);
+
+ $year = '';
+ $month = '';
+ $day = '';
+
+ $parsed_date[EDTFUtils::YEAR_BASE] = EDTFUtils::expandYear($parsed_date[EDTFUtils::YEAR_FULL], $parsed_date[EDTFUtils::YEAR_BASE], $parsed_date[EDTFUtils::YEAR_EXPONENT]);
+
+ // Clean-up unspecified year/decade.
+ $year = str_replace('X', '0', $parsed_date[EDTFUtils::YEAR_BASE]);
+
+ if (array_key_exists(EDTFUtils::MONTH, $parsed_date)) {
+ $month = str_replace('XX', '01', $parsed_date[EDTFUtils::MONTH]);
+ $month = str_replace('X', '0', $month);
+
+ // ISO 8601 doesn't support seasonal notation yet. Swap them out.
+ if (array_key_exists($month, EDTFUtils::SEASONS_MAP)) {
+ $month = EDTFUtils::SEASONS_MAP[$month];
+ }
+ }
+
+ if (array_key_exists(EDTFUtils::DAY, $parsed_date)) {
+ $day = str_replace('XX', '01', $parsed_date[EDTFUtils::DAY]);
+ $day = str_replace('X', '0', $day);
+ }
+
+ $formatted_date = implode('-', array_filter([$year, $month, $day]));
+
+ // Time.
+ if (array_key_exists(1, $date_time) && !empty($date_time[1])) {
+ $formatted_date .= 'T' . $date_time[1];
+ }
+ else {
+ $formatted_date .= 'T00:00:00';
+ }
+
+ return $formatted_date;
+
+ }
+
+}
diff --git a/src/Plugin/Field/FieldFormatter/EDTFFormatter.php b/src/Plugin/Field/FieldFormatter/EDTFFormatter.php
index 0720c36..c46912b 100644
--- a/src/Plugin/Field/FieldFormatter/EDTFFormatter.php
+++ b/src/Plugin/Field/FieldFormatter/EDTFFormatter.php
@@ -5,6 +5,7 @@
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
+use Drupal\controlled_access_terms\EDTFUtils;
/**
* Plugin implementation of the 'TextEDTFFormatter'.
@@ -21,30 +22,6 @@
*/
class EDTFFormatter extends FormatterBase {
- /**
- * Month/Season to text map.
- *
- * @var array
- */
- private $MONTHS = [
- '01' => ['mmm' => 'Jan', 'mmmm' => 'January'],
- '02' => ['mmm' => 'Feb', 'mmmm' => 'February'],
- '03' => ['mmm' => 'Mar', 'mmmm' => 'March'],
- '04' => ['mmm' => 'Apr', 'mmmm' => 'April'],
- '05' => ['mmm' => 'May', 'mmmm' => 'May'],
- '06' => ['mmm' => 'Jun', 'mmmm' => 'June'],
- '07' => ['mmm' => 'Jul', 'mmmm' => 'July'],
- '08' => ['mmm' => 'Aug', 'mmmm' => 'August'],
- '09' => ['mmm' => 'Sep', 'mmmm' => 'September'],
- '10' => ['mmm' => 'Oct', 'mmmm' => 'October'],
- '11' => ['mmm' => 'Nov', 'mmmm' => 'November'],
- '12' => ['mmm' => 'Dec', 'mmmm' => 'December'],
- '21' => ['mmm' => 'Spr', 'mmmm' => 'Spring'],
- '22' => ['mmm' => 'Sum', 'mmmm' => 'Summer'],
- '23' => ['mmm' => 'Aut', 'mmmm' => 'Autumn'],
- '24' => ['mmm' => 'Win', 'mmmm' => 'Winter'],
- ];
-
/**
* Various delimiters.
*
@@ -57,38 +34,6 @@ class EDTFFormatter extends FormatterBase {
'space' => ' ',
];
- /**
- * Northern hemisphere season map.
- *
- * @var array
- */
- private $seasonMapNorth = [
- // Spring => March.
- '21' => '03',
- // Summer => June.
- '22' => '06',
- // Autumn => September.
- '23' => '09',
- // Winter => December.
- '24' => '12',
- ];
-
- /**
- * Southern hemisphere season map.
- *
- * @var array
- */
- private $seasonMapSouth = [
- // Spring => September.
- '21' => '03',
- // Summer => December.
- '22' => '06',
- // Autumn => March.
- '23' => '09',
- // Winter => June.
- '24' => '12',
- ];
-
/**
* {@inheritdoc}
*/
@@ -96,14 +41,9 @@ public static function defaultSettings() {
return [
// ISO 8601 bias.
'date_separator' => 'dash',
- // ISO 8601 bias.
'date_order' => 'big_endian',
- // ISO 8601 bias.
'month_format' => 'mm',
- // ISO 8601 bias.
'day_format' => 'dd',
- // Northern bias, sorry.
- 'season_hemisphere' => 'north',
] + parent::defaultSettings();
}
@@ -154,18 +94,6 @@ public function settingsForm(array $form, FormStateInterface $form_state) {
'd' => t('one-digit day of the month for days below 10, e.g. 2'),
],
];
- $form['season_hemisphere'] = [
- '#title' => t('Hemisphere Seasons'),
- '#type' => 'select',
- '#default_value' => $this->getSetting('season_hemisphere'),
- '#description' => t("Seasons don't have digit months so we map them
- to their respective equinox and solstice months.
- Select a hemisphere to use for the mapping."),
- '#options' => [
- 'north' => t('Northern Hemisphere'),
- 'south' => t('Southern Hemisphere'),
- ],
- ];
return $form;
}
@@ -187,32 +115,73 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
foreach ($items as $delta => $item) {
// Interval.
- list($begin, $end) = explode('/', $item->value);
+ if (strpos($item->value, '/') !== FALSE) {
+ list($begin, $end) = explode('/', $item->value);
- $formatted_begin = $this->formatDate($begin);
+ if (empty($begin) || $begin === '..') {
+ $formatted_begin = "open start";
+ }
+ else {
+ $formatted_begin = $this->formatDate($begin);
+ }
+
+ if (empty($end) || $end === '..') {
+ $formatted_end = "open end";
+ }
+ else {
+ $formatted_end = $this->formatDate($end);
+ }
- // End either empty or valid extended interval values (5.2.3.)
- if (empty($end)) {
- $element[$delta] = ['#markup' => $formatted_begin];
- }
- elseif ($end === 'unknown' || $end === 'open') {
$element[$delta] = [
'#markup' => t('@begin to @end', [
'@begin' => $formatted_begin,
- '@end' => $end,
+ '@end' => $formatted_end,
]),
];
+ continue;
}
- else {
- $formatted_end = $this->formatDate($end);
+ // Sets.
+ if (strpos($item->value, '[') !== FALSE || strpos($item->value, '{') !== FALSE) {
+ $set_qualifier = (strpos($item->value, '[') !== FALSE) ? t('one of the dates:') : t('all of the dates:');
+ foreach (explode(',', trim($item->value, '{}[] ')) as $date) {
+ $date_range = explode('..', $date);
+ switch (count($date_range)) {
+ case 1:
+ $formatted_dates[] = $this->formatDate($date);
+ break;
+
+ case 2:
+ if (empty($date_range[0])) {
+ $formatted_dates[] = t('@date or some earlier date', [
+ '@date' => $this->formatDate($date_range[1]),
+ ]);
+ }
+ elseif (empty($date_range[1])) {
+ $formatted_dates[] = t('@date or some later date', [
+ '@date' => $this->formatDate($date_range[0]),
+ ]);
+ }
+ else {
+ $formatted_dates[] = t('@date_begin until @date_end', [
+ '@date_begin' => $this->formatDate($date_range[0]),
+ '@date_end' => $this->formatDate($date_range[1]),
+ ]);
+ }
+ break;
+ }
+ }
$element[$delta] = [
- '#markup' => t('@begin to @end', [
- '@begin' => $formatted_begin,
- '@end' => $formatted_end,
+ '#markup' => t('@qualifier @list', [
+ '@qualifier' => $set_qualifier,
+ '@list' => implode(', ', $formatted_dates),
]),
];
+ continue;
}
+ $element[$delta] = [
+ '#markup' => $this->formatDate($item->value),
+ ];
}
return $element;
}
@@ -227,95 +196,250 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
* The date in EDTF format.
*/
protected function formatDate($edtf_text) {
- $settings = $this->getSettings();
- $cleaned_datetime = $edtf_text;
- // TODO: Time?
- $qualifiers_format = '%s';
- // Uncertainty.
- if (!(strpos($edtf_text, '~') === FALSE)) {
- $qualifiers_format = t('approximately');
- $qualifiers_format .= ' %s';
- }
- if (!(strpos($edtf_text, '?') === FALSE)) {
- $qualifiers_format = '%s ';
- $qualifiers_format .= t('(uncertain)');
- }
- $cleaned_datetime = str_replace(['?', '~'], '', $cleaned_datetime);
- list($year, $month, $day) = explode('-', $cleaned_datetime, 3);
+ $date_time = explode('T', $edtf_text);
- // Which unspecified, if any?
- $which_unspecified = '';
- if (!(strpos($year, 'uu') === FALSE)) {
- $which_unspecified = t('decade');
- }
- if (!(strpos($year, 'u') === FALSE)) {
- $which_unspecified = t('year');
+ // Formatted versions of the date elements.
+ $year = '';
+ $month = '';
+ $day = '';
+
+ preg_match(EDTFUtils::DATE_PARSE_REGEX, $date_time[0], $parsed_date);
+
+ $parsed_date[EDTFUtils::YEAR_BASE] = EDTFUtils::expandYear($parsed_date[EDTFUtils::YEAR_FULL], $parsed_date[EDTFUtils::YEAR_BASE], $parsed_date[EDTFUtils::YEAR_EXPONENT]);
+ $settings = $this->getSettings();
+
+ // Unspecified.
+ $unspecified = [];
+ if (strpos($parsed_date[EDTFUtils::YEAR_BASE], 'XXXX') !== FALSE) {
+ $unspecified[] = t('year');
}
- if (!(strpos($month, 'u') === FALSE)) {
- $which_unspecified = t('month');
- // No partial months.
- $month = '';
+ elseif (strpos($parsed_date[EDTFUtils::YEAR_BASE], 'XXX') !== FALSE) {
+ $unspecified[] = t('century');
}
- if (!(strpos($day, 'u') === FALSE)) {
- $which_unspecified = t('day');
- // No partial days.
- $day = '';
+ elseif (strpos($parsed_date[EDTFUtils::YEAR_BASE], 'XX') !== FALSE) {
+ $unspecified[] = t('decade');
}
- // Add unspecified formatting if needed.
- if (!empty($which_unspecified)) {
- $qualifiers_format = t('an unspecified @part in', ['@part' => $which_unspecified]) . ' ' . $qualifiers_format;
+ elseif (strpos($parsed_date[EDTFUtils::YEAR_BASE], 'X') !== FALSE) {
+ $unspecified[] = t('year');
}
-
// Clean-up unspecified year/decade.
- if (!(strpos($year, 'u') === FALSE)) {
- $year = str_replace('u', '0', $year);
- $year = t("the @year's", ['@year' => $year]);
- }
+ $year = str_replace('X', '0', $parsed_date[EDTFUtils::YEAR_BASE]);
- // Format the month.
- if (!empty($month)) {
- // IF 'mm', do nothing, it is already in this format.
- if ($settings['month_format'] === 'mmm' || $settings['month_format'] === 'mmmm') {
- $month = $this->MONTHS[$month][$settings['month_format']];
+ if (array_key_exists(EDTFUtils::MONTH, $parsed_date)) {
+ if (strpos($parsed_date[EDTFUtils::MONTH], 'X') !== FALSE) {
+ $unspecified[] = t('month');
+ // Month remains blank for output.
}
- // Digit Seasons.
- elseif (in_array($month, ['21', '22', '23', '24'])) {
- $season_mapping = ($settings['season_hemisphere'] === 'north' ? $this->seasonMapNorth : $this->seasonMapSouth);
- $month = $season_mapping[$month];
+ elseif ($settings['month_format'] === 'mmm' || $settings['month_format'] === 'mmmm') {
+ $month = EDTFUtils::MONTHS_MAP[$parsed_date[EDTFUtils::MONTH]][$settings['month_format']];
}
-
- if ($settings['month_format'] === 'm') {
- $month = ltrim($month, ' 0');
+ elseif ($settings['month_format'] === 'm') {
+ $month = ltrim($parsed_date[EDTFUtils::MONTH], ' 0');
+ }
+ // IF 'mm', do nothing, it is already in this format.
+ else {
+ $month = $parsed_date[EDTFUtils::MONTH];
}
}
- // Format the day.
- if (!empty($day)) {
- if ($settings['day_format'] === 'd') {
- $day = ltrim($day, ' 0');
+ if (array_key_exists(EDTFUtils::DAY, $parsed_date)) {
+ if (strpos($parsed_date[EDTFUtils::DAY], 'X') !== FALSE) {
+ $unspecified[] = t('day');
+ }
+ elseif ($settings['day_format'] === 'd') {
+ $day = ltrim($parsed_date[EDTFUtils::DAY], ' 0');
+ }
+ else {
+ $day = $parsed_date[EDTFUtils::DAY];
}
}
- // Put the parts back together
- // Big Endian by default.
- $parts_in_order = [$year, $month, $day];
-
+ // Put the parts back together.
if ($settings['date_order'] === 'little_endian') {
$parts_in_order = [$day, $month, $year];
}
elseif ($settings['date_order'] === 'middle_endian') {
$parts_in_order = [$month, $day, $year];
- } // Big Endian by default
+ }
+ else {
+ // Big Endian by default.
+ $parts_in_order = [$year, $month, $day];
+ }
if ($settings['date_order'] === 'middle_endian' && !preg_match('/\d/', $month) && !empty(array_filter([$month, $day]))) {
- $cleaned_datetime = "$month $day, $year";
+ $formatted_date = "$month $day, $year";
}
else {
- $cleaned_datetime = implode($this->DELIMITERS[$settings['date_separator']], array_filter($parts_in_order));
+ $formatted_date = implode($this->DELIMITERS[$settings['date_separator']], array_filter($parts_in_order));
+ }
+
+ // Time.
+ // TODO: Add time formatting options.
+ if (array_key_exists(1, $date_time) && !empty($date_time[1])) {
+ $formatted_date .= ' ' . $date_time[1];
}
- return sprintf($qualifiers_format, $cleaned_datetime);
+ // Unspecified.
+ // Year = 1, Month = 2, Day = 4.
+ switch (count($unspecified)) {
+ case 1:
+ $formatted_date = t('unspecified @time_unit in @date', [
+ '@time_unit' => $unspecified[0],
+ '@date' => $formatted_date,
+ ]);
+ break;
+
+ case 2:
+ $formatted_date = t('unspecified @time_unit1 and @time_unit2 in @date', [
+ '@time_unit1' => $unspecified[0],
+ '@time_unit2' => $unspecified[1],
+ '@date' => $formatted_date,
+ ]);
+ break;
+
+ case 3:
+ $formatted_date = t('unspecified @time_unit1, @time_unit2, and @time_unit3 in @date', [
+ '@time_unit1' => $unspecified[0],
+ '@time_unit2' => $unspecified[1],
+ '@time_unit2' => $unspecified[2],
+ '@date' => $formatted_date,
+ ]);
+ break;
+ }
+
+ // Qualified.
+ // This is ugly and terrible, but I'm out of ideas for simplifying it.
+ $qualifiers = [
+ 'uncertain' => [],
+ 'approximate' => [],
+ ];
+ if (array_key_exists(EDTFUtils::QUALIFIER_YEAR, $parsed_date) && !empty($parsed_date[EDTFUtils::QUALIFIER_YEAR])) {
+ switch ($parsed_date[EDTFUtils::QUALIFIER_YEAR]) {
+ case '?':
+ $qualifiers['uncertain']['year'] = TRUE;
+ break;
+
+ case '~':
+ $qualifiers['approximate']['year'] = TRUE;
+ break;
+
+ case '%':
+ $qualifiers['uncertain']['year'] = TRUE;
+ $qualifiers['approximate']['year'] = TRUE;
+ break;
+ }
+ }
+ if (array_key_exists(EDTFUtils::QUALIFIER_YEAR_ONLY, $parsed_date) && !empty($parsed_date[EDTFUtils::QUALIFIER_YEAR_ONLY])) {
+ switch ($parsed_date[EDTFUtils::QUALIFIER_YEAR_ONLY]) {
+ case '?':
+ $qualifiers['uncertain']['year'] = TRUE;
+ break;
+
+ case '~':
+ $qualifiers['approximate']['year'] = TRUE;
+ break;
+
+ case '%':
+ $qualifiers['uncertain']['year'] = TRUE;
+ $qualifiers['approximate']['year'] = TRUE;
+ break;
+ }
+ }
+ if (array_key_exists(EDTFUtils::QUALIFIER_MONTH, $parsed_date) && !empty($parsed_date[EDTFUtils::QUALIFIER_MONTH])) {
+ switch ($parsed_date[EDTFUtils::QUALIFIER_MONTH]) {
+ case '?':
+ $qualifiers['uncertain']['year'] = TRUE;
+ $qualifiers['uncertain']['month'] = TRUE;
+ break;
+
+ case '~':
+ $qualifiers['approximate']['year'] = TRUE;
+ $qualifiers['approximate']['month'] = TRUE;
+ break;
+
+ case '%':
+ $qualifiers['uncertain']['year'] = TRUE;
+ $qualifiers['uncertain']['month'] = TRUE;
+ $qualifiers['approximate']['year'] = TRUE;
+ $qualifiers['approximate']['month'] = TRUE;
+ break;
+ }
+ }
+ if (array_key_exists(EDTFUtils::QUALIFIER_MONTH_ONLY, $parsed_date) && !empty($parsed_date[EDTFUtils::QUALIFIER_MONTH_ONLY])) {
+ switch ($parsed_date[EDTFUtils::QUALIFIER_MONTH_ONLY]) {
+ case '?':
+ $qualifiers['uncertain']['month'] = TRUE;
+ break;
+
+ case '~':
+ $qualifiers['approximate']['month'] = TRUE;
+ break;
+
+ case '%':
+ $qualifiers['uncertain']['month'] = TRUE;
+ $qualifiers['approximate']['month'] = TRUE;
+ break;
+ }
+ }
+ if (array_key_exists(EDTFUtils::QUALIFIER_DAY, $parsed_date) && !empty($parsed_date[EDTFUtils::QUALIFIER_DAY])) {
+ switch ($parsed_date[EDTFUtils::QUALIFIER_DAY]) {
+ case '?':
+ $qualifiers['uncertain']['year'] = TRUE;
+ $qualifiers['uncertain']['month'] = TRUE;
+ $qualifiers['uncertain']['day'] = TRUE;
+ break;
+
+ case '~':
+ $qualifiers['approximate']['year'] = TRUE;
+ $qualifiers['approximate']['month'] = TRUE;
+ $qualifiers['approximate']['day'] = TRUE;
+ break;
+
+ case '%':
+ $qualifiers['uncertain']['year'] = TRUE;
+ $qualifiers['uncertain']['month'] = TRUE;
+ $qualifiers['uncertain']['day'] = TRUE;
+ $qualifiers['approximate']['year'] = TRUE;
+ $qualifiers['approximate']['month'] = TRUE;
+ $qualifiers['approximate']['day'] = TRUE;
+ break;
+ }
+ }
+ if (array_key_exists(EDTFUtils::QUALIFIER_DAY_ONLY, $parsed_date) && !empty($parsed_date[EDTFUtils::QUALIFIER_DAY_ONLY])) {
+ switch ($parsed_date[EDTFUtils::QUALIFIER_DAY_ONLY]) {
+ case '?':
+ $qualifiers['uncertain']['day'] = TRUE;
+ break;
+
+ case '~':
+ $qualifiers['approximate']['day'] = TRUE;
+ break;
+
+ case '%':
+ $qualifiers['uncertain']['day'] = TRUE;
+ $qualifiers['approximate']['day'] = TRUE;
+ break;
+ }
+ }
+ $qualifier_parts = [];
+ foreach ($qualifiers as $qualifier => $parts) {
+ $keys = array_keys($parts);
+ switch (count($keys)) {
+ case 1:
+ case 2:
+ $qualifier_parts[] = implode(' ' . t('and') . ' ', $keys) . ' ' . $qualifier;
+ break;
+
+ case 3:
+ $qualifier_parts[] = $qualifier;
+ break;
+ }
+ }
+ if (count($qualifier_parts) > 0) {
+ return $formatted_date . ' (' . implode('; ', $qualifier_parts) . ')';
+ }
+ return $formatted_date;
}
}
diff --git a/src/Plugin/Field/FieldType/ExtendedDateTimeFormat.php b/src/Plugin/Field/FieldType/ExtendedDateTimeFormat.php
index 900e4d2..643021a 100644
--- a/src/Plugin/Field/FieldType/ExtendedDateTimeFormat.php
+++ b/src/Plugin/Field/FieldType/ExtendedDateTimeFormat.php
@@ -9,7 +9,7 @@
*
* @FieldType(
* id = "edtf",
- * label = @Translation("EDTF, level 1"),
+ * label = @Translation("EDTF"),
* module = "controlled_access_terms",
* description = @Translation("Extended Date Time Format field"),
* default_formatter = "edtf_default",
diff --git a/src/Plugin/Field/FieldWidget/EDTFWidget.php b/src/Plugin/Field/FieldWidget/EDTFWidget.php
index 8d59cca..2d5d914 100644
--- a/src/Plugin/Field/FieldWidget/EDTFWidget.php
+++ b/src/Plugin/Field/FieldWidget/EDTFWidget.php
@@ -2,18 +2,16 @@
namespace Drupal\controlled_access_terms\Plugin\Field\FieldWidget;
-use Datetime;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
+use Drupal\controlled_access_terms\EDTFUtils;
/**
* Plugin implementation of the 'edtf' widget.
*
- * Validates text values for compliance with EDTF 1.0, level 1.
- * http://www.loc.gov/standards/datetime/pre-submission.html.
- *
- * // TODO: maybe some day support level 2.
+ * Validates text values for compliance with EDTF (2018).
+ * https://www.loc.gov/standards/datetime/edtf.html.
*
* @FieldWidget(
* id = "edtf_default",
@@ -32,6 +30,7 @@ public static function defaultSettings() {
return [
'strict_dates' => FALSE,
'intervals' => FALSE,
+ 'sets' => FALSE,
] + parent::defaultSettings();
}
@@ -40,21 +39,19 @@ public static function defaultSettings() {
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$description_string = $this->t(
- 'Negative dates, and the level 1 features unspecified dates,
- extended years, and seasons
- are not supported with strict date checking.'
+ 'Most level 1 and 2 features are not supported with strict date checking.'
);
$description_string .= '
';
$description_string .= $this->t(
'Uncertain/Approximate dates will have their markers removed before
- checking. (For example, "1984~?" will be checked as "1984".)'
+ checking. (For example, "1984?", "1984~", and "1984%" will be checked as "1984".)'
);
$element = parent::settingsForm($form, $form_state);
$element['description'] = [
'#type' => 'markup',
'#prefix' => '