From 2b985815279512a318acd891b488236516782b85 Mon Sep 17 00:00:00 2001 From: eileen Date: Tue, 2 Jan 2024 14:48:03 +1300 Subject: [PATCH] Fix time handling for us-style Add consistent time-format support for US formats, add test for our magic year-handling --- CRM/Utils/Date.php | 102 ++++++++++++++++++++------- tests/phpunit/CRM/Utils/DateTest.php | 31 ++++---- 2 files changed, 92 insertions(+), 41 deletions(-) diff --git a/CRM/Utils/Date.php b/CRM/Utils/Date.php index 3ed75e4eb3..8df899b827 100644 --- a/CRM/Utils/Date.php +++ b/CRM/Utils/Date.php @@ -2197,7 +2197,14 @@ class CRM_Utils_Date { if (empty($date) || !self::validateDateInput($date, $dateType)) { return NULL; } - if ($dateType === self::DATE_yyyy_mm_dd) { + if ($dateType === self::DATE_mm_dd_yy || $dateType === self::DATE_mm_dd_yyyy) { + // PHP interprets slashes as American and dots/dashes as European/other. + // The only thing we support for mm_dd_yy that differs from strtotime is + // the use of dashes - so here we replace then we can use strtotime. + $date = str_replace('-', '/', $date); + $date = self::replaceShortYear($date, '/', 3); + } + if (in_array($dateType, [self::DATE_yyyy_mm_dd, self::DATE_mm_dd_yy, self::DATE_mm_dd_yyyy], TRUE)) { $timestamp = strtotime($date); return $timestamp ? date('YmdHis', $timestamp) : NULL; } @@ -2205,21 +2212,6 @@ class CRM_Utils_Date { $now = getdate(); // suppress hh:mm or hh:mm:ss if it exists CRM-7957 $value = preg_replace(self::getTimeRegex(), "", $date); - - if ($dateType === self::DATE_mm_dd_yy || $dateType === self::DATE_mm_dd_yyyy) { - $formattedDate = explode("/", $value); - if (count($formattedDate) != 3) { - $formattedDate = explode("-", $value); - } - if (count($formattedDate) == 3) { - $year = (int) $formattedDate[2]; - $month = (int) $formattedDate[0]; - $day = (int) $formattedDate[1]; - } - else { - return NULL; - } - } if ($dateType === self::DATE_Month_dd_yyyy) { $dateArray = explode(' ', $value); // ignore comma(,) @@ -2297,16 +2289,7 @@ class CRM_Utils_Date { $month = ($month < 10) ? "0" . "$month" : $month; $day = ($day < 10) ? "0" . "$day" : $day; - $year = (int) $year; - if ($year < 100) { - $year = substr($now['year'], 0, 2) * 100 + $year; - if ($year > ($now['year'] + 5)) { - $year = $year - 100; - } - elseif ($year <= ($now['year'] - 95)) { - $year = $year + 100; - } - } + $year = self::getYear($year, $now['year']); $newDate = "$year$month$day"; // if month is invalid return as error @@ -2435,4 +2418,71 @@ class CRM_Utils_Date { return TRUE; } + /** + * Get the date element from the passed date string. + * + * @param string $date e.g. '20-Oct-2022' + * @param string $separator e.g '-' + * @param int $monthPlacement eg. 2 for the second section of the string + * + * @return string + */ + protected static function getDateElement(string $date, string $separator, int $monthPlacement): string { + $element = explode($separator, $date)[$monthPlacement - 1]; + // This second explosion drops any trailing time string. + return explode(' ', $element)[0]; + } + + /** + * Replace a year in the short year format e.g 22. + * + * Note this differs from standard php strotime as we treat anything less + * than 5 years in the future as being in the past. + * + * The reasons for this are not documented but it is likely that our use cases + * dictated it - eg. importing birth dates would more sanely default to handling 68 + * as 1968. By contrast importing future data is likely rare. + * + * @param string $date + * @param string $separator + * @param int $yearPlacement + * + * @return string + */ + public static function replaceShortYear($date, string $separator, int $yearPlacement): string { + $year = self::getDateElement($date, $separator, $yearPlacement); + if (strlen($year) === 4) { + return $date; + } + $parts = explode($separator, $date); + // Replace the year with the 4-digit-year, re-appending any trailing time string. + $parts[$yearPlacement - 1] = self::getYear($year) . substr($parts[$yearPlacement - 1], 2); + return implode($separator, $parts); + } + + /** + * Get a 4 digit year from a 2 or 4 digit year. + * + * The handling differs from strtotime as a year more than 5 years in the future + * is deemed to be in the past whereas strtotime uses a 1970 cutoff + * https://www.w3schools.com/php/func_date_strtotime.asp + * + * @param int $year + * + * @return int + */ + public static function getYear(int $year) { + $currentYear = date('Y'); + if ($year < 100) { + $year = ((int) substr($currentYear, 0, 2)) * 100 + $year; + if ($year > ($currentYear + 5)) { + $year -= 100; + } + elseif ($year <= ($currentYear - 95)) { + $year += 100; + } + } + return $year; + } + } diff --git a/tests/phpunit/CRM/Utils/DateTest.php b/tests/phpunit/CRM/Utils/DateTest.php index a13b8ca567..98db38d7f0 100644 --- a/tests/phpunit/CRM/Utils/DateTest.php +++ b/tests/phpunit/CRM/Utils/DateTest.php @@ -2692,23 +2692,24 @@ class CRM_Utils_DateTest extends CiviUnitTestCase { '2022-10-01 3:54:56' => ['date' => '2022-10-01 3:54:56', 'format' => CRM_Utils_Date::DATE_yyyy_mm_dd, 'expected' => '20221001035456'], // mm_dd_yy format - eg. US Style 10-01-22 OR 10/01/22 where 10 is the month. 2 digit year. - '10-01-22' => ['date' => '10-01-22', 'format' => CRM_Utils_Date::DATE_mm_dd_yy, 'expected' => '20221001'], - '10-1-22' => ['date' => '10-1-22', 'format' => CRM_Utils_Date::DATE_mm_dd_yy, 'expected' => '20221001'], - '10-01-22 15:54:56' => ['date' => '10-01-22 15:54:56', 'format' => CRM_Utils_Date::DATE_mm_dd_yy, 'expected' => '20221001155456', 'ignore_reason' => 'Time not handled correctly in this instance.'], - '10-1-22 3:54:56' => ['date' => '10-1-22 3:54:56', 'format' => CRM_Utils_Date::DATE_mm_dd_yy, 'expected' => '20221001035456', 'ignore_reason' => 'Time not handled correctly in this instance.'], - '10/01/22' => ['date' => '10/01/22', 'format' => CRM_Utils_Date::DATE_mm_dd_yy, 'expected' => '20221001'], - '10/1/22' => ['date' => '10/1/22', 'format' => CRM_Utils_Date::DATE_mm_dd_yy, 'expected' => '20221001'], - '10/01/22 15:54:56' => ['date' => '10/01/22 15:54:56', 'format' => CRM_Utils_Date::DATE_mm_dd_yy, 'expected' => '20221001155456', 'ignore_reason' => 'Time not handled correctly in this instance.'], - '10/01/22 3:54:56' => ['date' => '10/01/22 3:54:56', 'format' => CRM_Utils_Date::DATE_mm_dd_yy, 'expected' => '20221001035456', 'ignore_reason' => 'Time not handled correctly in this instance.'], + '10-01-30-mapped-to-1934-not-2034-per-strtotime' => ['date' => '10-01-34', 'format' => CRM_Utils_Date::DATE_mm_dd_yy, 'expected' => '19341001000000'], + '10-01-22' => ['date' => '10-01-22', 'format' => CRM_Utils_Date::DATE_mm_dd_yy, 'expected' => '20221001000000'], + '10-1-22' => ['date' => '10-1-22', 'format' => CRM_Utils_Date::DATE_mm_dd_yy, 'expected' => '20221001000000'], + '10-01-22 15:54:56' => ['date' => '10-01-22 15:54:56', 'format' => CRM_Utils_Date::DATE_mm_dd_yy, 'expected' => '20221001155456'], + '10-1-22 3:54:56' => ['date' => '10-1-22 3:54:56', 'format' => CRM_Utils_Date::DATE_mm_dd_yy, 'expected' => '20221001035456'], + '10/01/22' => ['date' => '10/01/22', 'format' => CRM_Utils_Date::DATE_mm_dd_yy, 'expected' => '20221001000000'], + '10/1/22' => ['date' => '10/1/22', 'format' => CRM_Utils_Date::DATE_mm_dd_yy, 'expected' => '20221001000000'], + '10/01/22 15:54:56' => ['date' => '10/01/22 15:54:56', 'format' => CRM_Utils_Date::DATE_mm_dd_yy, 'expected' => '20221001155456'], + '10/01/22 3:54:56' => ['date' => '10/01/22 3:54:56', 'format' => CRM_Utils_Date::DATE_mm_dd_yy, 'expected' => '20221001035456'], // mm_dd_yyyy format - eg. US Style 10-01-2022 OR 10/01/2022 where 10 is the month. 4 digit year. - '10-01-2022' => ['date' => '10-01-2022', 'format' => CRM_Utils_Date::DATE_mm_dd_yyyy, 'expected' => '20221001'], - '10-1-2022' => ['date' => '10-1-2022', 'format' => CRM_Utils_Date::DATE_mm_dd_yyyy, 'expected' => '20221001'], - '10-01-2022 15:54:56' => ['date' => '10-01-2022 15:54:56', 'format' => CRM_Utils_Date::DATE_mm_dd_yyyy, 'expected' => '20221001155456', 'ignore_reason' => 'Time not handled correctly in this instance.'], - '10-01-2022 3:54:56' => ['date' => '10-01-2022 3:54:56', 'format' => CRM_Utils_Date::DATE_mm_dd_yyyy, 'expected' => '20221001035456', 'ignore_reason' => 'Time not handled correctly in this instance.'], - '10/01/2022' => ['date' => '10/01/2022', 'format' => CRM_Utils_Date::DATE_mm_dd_yyyy, 'expected' => '20221001'], - '10/1/2022' => ['date' => '10/1/2022', 'format' => CRM_Utils_Date::DATE_mm_dd_yyyy, 'expected' => '20221001'], - '10/1/2022 15:54:56' => ['date' => '10/1/2022 15:54:56', 'format' => CRM_Utils_Date::DATE_mm_dd_yyyy, 'expected' => '20221001155456', 'ignore_reason' => 'Time not handled correctly in this instance.'], + '10-01-2022' => ['date' => '10-01-2022', 'format' => CRM_Utils_Date::DATE_mm_dd_yyyy, 'expected' => '20221001000000'], + '10-1-2022' => ['date' => '10-1-2022', 'format' => CRM_Utils_Date::DATE_mm_dd_yyyy, 'expected' => '20221001000000'], + '10-01-2022 15:54:56' => ['date' => '10-01-2022 15:54:56', 'format' => CRM_Utils_Date::DATE_mm_dd_yyyy, 'expected' => '20221001155456'], + '10-01-2022 3:54:56' => ['date' => '10-01-2022 3:54:56', 'format' => CRM_Utils_Date::DATE_mm_dd_yyyy, 'expected' => '20221001035456'], + '10/01/2022' => ['date' => '10/01/2022', 'format' => CRM_Utils_Date::DATE_mm_dd_yyyy, 'expected' => '20221001000000'], + '10/1/2022' => ['date' => '10/1/2022', 'format' => CRM_Utils_Date::DATE_mm_dd_yyyy, 'expected' => '20221001000000'], + '10/1/2022 15:54:56' => ['date' => '10/1/2022 15:54:56', 'format' => CRM_Utils_Date::DATE_mm_dd_yyyy, 'expected' => '20221001155456'], // DATE_Month_dd_yyyy ie December, 12 2023 'December, 12 2023' => ['date' => 'December, 12 2023', 'format' => CRM_Utils_Date::DATE_Month_dd_yyyy, 'expected' => '20221001035456', 'ignore_reason' => 'Example syntax is broken. Investigate'], -- 2.25.1