diff --git a/src/EDTFConverter.php b/src/EDTFConverter.php index 7ae3401..558e0f7 100644 --- a/src/EDTFConverter.php +++ b/src/EDTFConverter.php @@ -30,7 +30,7 @@ public static function datetimeIso8601Value($data) { } /** - * Converts an EDTF text field into an ISO 8601 timestamp string. + * Converts an EDTF text field into an ISO 8601 date. * * It assumes the earliest valid date for approximations and intervals. * diff --git a/src/EDTFUtils.php b/src/EDTFUtils.php index 0ae1eb9..c8bd6c5 100644 --- a/src/EDTFUtils.php +++ b/src/EDTFUtils.php @@ -183,7 +183,7 @@ public static function validateDate($datetime_str, $strict = FALSE) { $msgs = []; if (strpos($datetime_str, 'T') > -1) { - list($date, $time) = explode('T', $datetime_str); + [$date, $time] = explode('T', $datetime_str); } else { $date = (string) $datetime_str; @@ -216,22 +216,29 @@ public static function validateDate($datetime_str, $strict = FALSE) { 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 ( - // Month doesn't exist in mapping or does exist in mapping, but is > 12 - // and there is a day part. - (!array_key_exists($parsed_date[self::MONTH], self::MONTHS_MAP) || - (array_key_exists($parsed_date[self::MONTH], self::MONTHS_MAP) && - array_key_exists(self::DAY, $parsed_date) && - $parsed_date[self::MONTH] > 12)) && - strpos($parsed_date[self::MONTH], 'X') === FALSE) { + // Month doesn't exist in mapping + // and isn't a valid unspecified month value. + ( + !array_key_exists($parsed_date[self::MONTH], self::MONTHS_MAP) && + (intval(str_replace('X', '1', $parsed_date[self::MONTH])) > 12) + ) || + // Sub-year groupings with day values. + ( + array_key_exists($parsed_date[self::MONTH], self::MONTHS_MAP) && + array_key_exists(self::DAY, $parsed_date) && + $parsed_date[self::MONTH] > 12 + ) || + // Unspecifed character comes before number. + ( + preg_match('/X+\d/', $parsed_date[self::MONTH]) + ) + ) { $msgs[] = "Provided month value '" . $parsed_date[self::MONTH] . "' is not valid."; } $strict_pattern = 'Y-m'; @@ -327,8 +334,28 @@ public static function expandYear($year_full, $year_base, $year_exponent) { */ public static function iso8601Value(string $edtf) { + if (count(self::validate($edtf)) > 0) { + return ''; + } + // Sets. + if (strpos($edtf, '[') !== FALSE || strpos($edtf, '{') !== FALSE) { + // Use first in set. + $dates = preg_split('/(,|\.\.)/', trim($edtf, '{}[]')); + return self::iso8601Value(array_shift($dates)); + } + // Intervals. + if (str_contains($edtf, '/')) { + $dates = explode('/', $edtf); + return self::iso8601Value(array_shift($dates)); + } + $date_time = explode('T', $edtf); + // Valid EDTF values with time portions are already ISO 8601 timestamps. + if (array_key_exists(1, $date_time) && !empty($date_time[1])) { + return $edtf; + } + preg_match(EDTFUtils::DATE_PARSE_REGEX, $date_time[0], $parsed_date); $year = ''; @@ -358,17 +385,7 @@ public static function iso8601Value(string $edtf) { $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; + return implode('-', array_filter([$year, $month, $day])); } diff --git a/tests/src/Kernel/EdtfUtilsTest.php b/tests/src/Kernel/EdtfUtilsTest.php index 6fe17b3..685428e 100644 --- a/tests/src/Kernel/EdtfUtilsTest.php +++ b/tests/src/Kernel/EdtfUtilsTest.php @@ -19,7 +19,7 @@ class EdtfUtilsTest extends KernelTestBase { * * @var array */ - private $testCases = [ + private $singleDateValidations = [ '1900' => [], '1900-01' => [], '1900-01-02' => [], @@ -27,17 +27,18 @@ class EdtfUtilsTest extends KernelTestBase { '1900-XX' => [], '1900-91' => ['Provided month value \'91\' is not valid.'], '1900-91-01' => ['Provided month value \'91\' is not valid.'], + '1900-X1' => ['Provided month value \'X1\' is not valid.'], // No validation for months with X. - '1900-3X' => [], + '1900-3X' => ['Provided month value \'3X\' is not valid.'], // Month 31 without a day matches summer so it's valid. '1900-31' => [], '1900-31-01' => ['Provided month value \'31\' is not valid.'], - '190X-5X-8X' => [], + '190X-5X-8X' => ['Provided month value \'5X\' is not valid.'], '19000' => ['Years longer than 4 digits must be prefixed with a \'Y\'.'], 'Y19000' => [], '190u' => ['Could not parse the date \'190u\'.'], - '190' => ['Years must be at least 4 characters long.'], - '190-99-52' => ['Years must be at least 4 characters long.', + '190' => [], + '190-99-52' => [ 'Provided month value \'99\' is not valid.', 'Provided day value \'52\' is not valid.', ], @@ -53,9 +54,47 @@ class EdtfUtilsTest extends KernelTestBase { * @covers ::validate */ public function testEdtfValidate() { - foreach ($this->testCases as $input => $expected) { + foreach ($this->singleDateValidations as $input => $expected) { $this->assertEquals($expected, EDTFUtils::validate($input, FALSE, FALSE, FALSE)); } } + /** + * @covers ::iso8601Value + */ + public function testIso8601() { + // EDTF value and ISO 8601 Timestamp results. + // Empty values are invalid dates which return blank. + $tests = [ + '1900' => '1900', + '1900-01' => '1900-01', + '1900-01-02' => '1900-01-02', + '190X' => '1900', + '1900-XX' => '1900-01', + '1900-91' => '', + '1900-91-01' => '', + '1900-3X' => '', + '1900-31' => '1900-03', + '190X-5X-8X' => '', + '19000' => '', + 'Y19000' => '19000', + '190u' => '', + '190' => '190', + '190-99-52' => '', + '1900-01-02T' => '', + '1900-01-02T1:1:1' => '', + '1900-01-02T01:22:33' => '1900-01-02T01:22:33', + '1900-01-02T01:22:33Z' => '1900-01-02T01:22:33Z', + '1900-01-02T01:22:33+' => '', + '1900-01-02T01:22:33+05:00' => '1900-01-02T01:22:33+05:00', + // Intervals and Sets should return the earliest value. + '1900/2023' => '1900', + '[1900,2023]' => '1900', + '[1900,2023}' => '1900', + ]; + foreach ($tests as $date => $iso) { + $this->assertEquals($iso, EDTFUtils::iso8601Value($date)); + } + } + }