From 1a992f144b411d339f540392062442e8b745ea75 Mon Sep 17 00:00:00 2001 From: Rowan Tommins Date: Sun, 10 Apr 2022 23:04:46 +0100 Subject: [PATCH] Account for different handling of historical dates by ICU and Posix The Gregorian calendar was introduced in much of Europe in October 1582, with 4th October followed by the 15th October. ICU interprets historical dates based on this transition by default, but Posix and timelib do not. --- src/php-8.1-strftime.php | 14 +++++++++++++- tests/strftimeTest.php | 26 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/php-8.1-strftime.php b/src/php-8.1-strftime.php index 5f21c46..4d37fe6 100644 --- a/src/php-8.1-strftime.php +++ b/src/php-8.1-strftime.php @@ -6,6 +6,7 @@ use DateTimeInterface; use Exception; use IntlDateFormatter; + use IntlGregorianCalendar; use InvalidArgumentException; /** @@ -85,7 +86,18 @@ function strftime (string $format, $timestamp = null, ?string $locale = null) : $pattern = $intl_formats[$format]; } - return (new IntlDateFormatter($locale, $date_type, $time_type, $tz, null, $pattern))->format($timestamp); + // In October 1582, the Gregorian calendar replaced the Julian in much of Europe, and + // the 4th October was followed by the 15th October. + // ICU (including IntlDateFormattter) interprets and formats dates based on this cutover. + // Posix (including strftime) and timelib (including DateTimeImmutable) instead use + // a "proleptic Gregorian calendar" - they pretend the Gregorian calendar has existed forever. + // This leads to the same instants in time, as expressed in Unix time, having different representations + // in formatted strings. + // To adjust for this, a custom calendar can be supplied with a cutover date arbitrarily far in the past. + $calendar = IntlGregorianCalendar::createInstance(); + $calendar->setGregorianChange(PHP_INT_MIN); + + return (new IntlDateFormatter($locale, $date_type, $time_type, $tz, $calendar, $pattern))->format($timestamp); }; // Same order as https://www.php.net/manual/en/function.strftime.php diff --git a/tests/strftimeTest.php b/tests/strftimeTest.php index 831e4d3..0db3e0f 100644 --- a/tests/strftimeTest.php +++ b/tests/strftimeTest.php @@ -176,4 +176,30 @@ public function testLocale () { $result = strftime('%B', '20220306 13:02:03', 'eu'); $this->assertEquals('martxoa', $result, '%B: Full month name, based on the locale'); } + + /** + * In October 1582, the Gregorian calendar replaced the Julian in much of Europe, and + * the 4th October was followed by the 15th October. + * ICU (including IntlDateFormattter) interprets and formats dates based on this cutover. + * Posix (including strftime) and timelib (including DateTimeImmutable) instead use + * a "proleptic Gregorian calendar" - they pretend the Gregorian calendar has existed forever. + * This leads to the same instants in time, as expressed in Unix time, having different representations + * in formatted strings. + */ + public function testJulianCutover () { + // 1st October 1582 in proleptic Gregorian is the same date as 21st September 1582 Julian + $prolepticTimestamp = DateTimeImmutable::createFromFormat('Y-m-d|', '1582-10-01')->getTimestamp(); + $result = strftime('%x', $prolepticTimestamp, 'eu'); + $this->assertEquals('82/10/1', $result); + + // In much of Europe, the 10th October 1582 never existed + $prolepticTimestamp = DateTimeImmutable::createFromFormat('Y-m-d|', '1582-10-10')->getTimestamp(); + $result = strftime('%x', $prolepticTimestamp, 'eu'); + $this->assertEquals('82/10/10', $result); + + // The 15th October was the first day after the cutover, after which both systems agree + $prolepticTimestamp = DateTimeImmutable::createFromFormat('Y-m-d|', '1582-10-15')->getTimestamp(); + $result = strftime('%x', $prolepticTimestamp, 'eu'); + $this->assertEquals('82/10/15', $result); + } }