Skip to content

Commit

Permalink
Merge pull request #5 from IMSoP/julian-calendar-fix
Browse files Browse the repository at this point in the history
Account for different handling of historical dates by ICU and Posix
  • Loading branch information
alphp authored Apr 10, 2022
2 parents 041de67 + 1a992f1 commit b747539
Show file tree
Hide file tree
Showing 2 changed files with 39 additions and 1 deletion.
14 changes: 13 additions & 1 deletion src/php-8.1-strftime.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use DateTimeInterface;
use Exception;
use IntlDateFormatter;
use IntlGregorianCalendar;
use InvalidArgumentException;

/**
Expand Down Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions tests/strftimeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

0 comments on commit b747539

Please sign in to comment.