将 "julian dates" 转换为 unixtimestamp 并返回 DateTime 时出现错误
Bug with converting "julian dates" to unixtimestamp and back to DateTime
今天在我的项目上工作 - 发现了一个奇怪的错误。
使用 symfony/form
(5.4.5) 组件,发现日期在转换后(在 Symfony 端)作为 0001-01-01
传递,返回为 0000-12-30
.
所以我做了一些调查(从 symfony/form
获取代码,它正在用日期做这个转换的东西,发现一件非常有趣的事情 - 日期,它在 1582 年之前(从 Julian 日历移动到 Gregorian 日历) 转换不正确。
下面我放了一个代码示例及其输出。
$dateFormat = 2;
$timeFormat = -1;
$timezone = new \DateTimeZone('UTC');
$calendar = 1;
$pattern = 'yyyy-MM-dd';
$dateFormatter = new \IntlDateFormatter(\Locale::getDefault(), $dateFormat, $timeFormat, $timezone, $calendar, $pattern);
$dates = ['0001-01-01', '0002-01-01', '1000-01-01', '1582-01-01', '1583-01-01', '1600-01-01', '1800-01-01'];
foreach ($dates as $dateInput) {
$timestamp = $dateFormatter->parse($dateInput);
$dateTime = new \DateTime(date('Y-m-d', $timestamp), new \DateTimeZone('Europe/Berlin'));
$dateOutput = $dateTime->format('Y-m-d');
echo $dateInput . " " .$dateOutput . PHP_EOL;
}
0001-01-01 0000-12-30 - BAD
0002-01-01 0001-12-30 - BAD
1000-01-01 1000-01-06 - BAD
1582-01-01 1582-01-11 - BAD
1583-01-01 1583-01-01 - GOOD
1600-01-01 1600-01-01 - GOOD
1800-01-01 1800-01-01 - GOOD
我想说这并不完全是错误,只是一种不同的解释,或者是对一个棘手问题的不同解决方案选择:你如何在你使用的日历开始时倒计时?您看到的最终错误是混合选择 不同 解决方案的库的结果,因此误解了彼此的输出。
首先要知道的是,您使用的函数是建立在两个不同的库上的:
- 作为 PHP“intl”扩展的一部分,
IntlDateFormatter
建立在 ICU(Unicode 的国际组件)之上,由 Unicode 联盟维护
date
函数和 DateTime
class 建立在 timelib
之上,由 Derick Rethans 维护
正如您已经指出的那样,接下来要注意的是,他们在首次引入公历的日期开始出现分歧。我们可以比年份更具体:它是在 1582 年 10 月引入的,紧随其后的是儒略历 10 月 4 日和格里高利历 10 月 15 日。重要的是,这意味着世界上有 10 个不存在的日期会立即切换。
最后,让我们看看这两个库生成的实际时间戳,而不是来回格式化。请注意,Unix 时间戳应该表示在公历和 UTC 时区中表示为 1970-01-01 00:00:00.[= 的时间之后(或者,在这种情况下之前)的秒数。 22=]
为了使时间戳更易于阅读,让我们将它们分开以显示他们所说的在给定日期和 1970 年 1 月 1 日之间的天天数。
$dateFormatter = new \IntlDateFormatter(\Locale::getDefault(), 2, -1, new \DateTimeZone('UTC'), 1, 'yyyy-MM-dd');
$dates = [
'1582-10-03', '1582-10-04', '1582-10-05', '1582-10-06', '1582-10-07', '1582-10-08', '1582-10-09',
'1582-10-10', '1582-10-11', '1582-10-12', '1582-10-13', '1582-10-14', '1582-10-15', '1582-10-16'
];
foreach ($dates as $dateInput) {
$icuTimestamp = $dateFormatter->parse($dateInput);
$timelibTimestamp = DateTimeImmutable::createFromFormat('Y-m-d|', $dateInput)->getTimestamp();
$icuDays = abs(intval( $icuTimestamp / 60 / 60 / 24 ));
$timeLibDays = abs(intval( $timelibTimestamp / 60 / 60 / 24 ));
echo "Is {$dateInput} {$icuDays} days or {$timeLibDays} days before 1970?\n";
}
结果如下所示:
Is 1582-10-03 141429 days or 141439 days before 1970?
Is 1582-10-04 141428 days or 141438 days before 1970?
Is 1582-10-05 141427 days or 141437 days before 1970?
Is 1582-10-06 141426 days or 141436 days before 1970?
Is 1582-10-07 141425 days or 141435 days before 1970?
Is 1582-10-08 141424 days or 141434 days before 1970?
Is 1582-10-09 141423 days or 141433 days before 1970?
Is 1582-10-10 141422 days or 141432 days before 1970?
Is 1582-10-11 141421 days or 141431 days before 1970?
Is 1582-10-12 141420 days or 141430 days before 1970?
Is 1582-10-13 141419 days or 141429 days before 1970?
Is 1582-10-14 141418 days or 141428 days before 1970?
Is 1582-10-15 141427 days or 141427 days before 1970?
Is 1582-10-16 141426 days or 141426 days before 1970?
正如预期的那样,两个图书馆一致认为 1582-10-15 是 1970 年之前的 141427 天,但对更早的日期存在分歧:
- timelib 使用的是“proleptic Gregorian calendar”,假设 1582-10-14 比 1582-10-15 早一天,依此类推;这在历史上是不准确的,但使用起来更简单
- ICU 正在尝试按照当时欧洲人的方式来解释日期,因此 1582-10-04 仅比 1582-10-15 早 1 天;这在中间留下了一些模棱两可的日期,根据这种解释,这些日期根本不应该存在,ICU 将其解释为 Julian,奇怪的是 1582-10-05 给出的值与 1582-10-15
The ICU documentation 解释了这种 cut-over 行为,以及如何影响它。尽管没有完全记录,PHP 公开了相关方法,因此可以将 cut-over 设置为任意远的过去以匹配 timelib 行为:
$cal = IntlGregorianCalendar::createInstance();
$cal->setGregorianChange(PHP_INT_MIN);
$dateFormatter->setCalendar($cal);
将其添加到之前的代码中,我们得到匹配的输出:
Is 1582-10-03 141439 days or 141439 days before 1970?
Is 1582-10-04 141438 days or 141438 days before 1970?
Is 1582-10-05 141437 days or 141437 days before 1970?
Is 1582-10-06 141436 days or 141436 days before 1970?
Is 1582-10-07 141435 days or 141435 days before 1970?
Is 1582-10-08 141434 days or 141434 days before 1970?
Is 1582-10-09 141433 days or 141433 days before 1970?
Is 1582-10-10 141432 days or 141432 days before 1970?
Is 1582-10-11 141431 days or 141431 days before 1970?
Is 1582-10-12 141430 days or 141430 days before 1970?
Is 1582-10-13 141429 days or 141429 days before 1970?
Is 1582-10-14 141428 days or 141428 days before 1970?
Is 1582-10-15 141427 days or 141427 days before 1970?
Is 1582-10-16 141426 days or 141426 days before 1970?
注意:我相信 Symfony 跟踪器中的相应错误是 #29610
今天在我的项目上工作 - 发现了一个奇怪的错误。
使用 symfony/form
(5.4.5) 组件,发现日期在转换后(在 Symfony 端)作为 0001-01-01
传递,返回为 0000-12-30
.
所以我做了一些调查(从 symfony/form
获取代码,它正在用日期做这个转换的东西,发现一件非常有趣的事情 - 日期,它在 1582 年之前(从 Julian 日历移动到 Gregorian 日历) 转换不正确。
下面我放了一个代码示例及其输出。
$dateFormat = 2;
$timeFormat = -1;
$timezone = new \DateTimeZone('UTC');
$calendar = 1;
$pattern = 'yyyy-MM-dd';
$dateFormatter = new \IntlDateFormatter(\Locale::getDefault(), $dateFormat, $timeFormat, $timezone, $calendar, $pattern);
$dates = ['0001-01-01', '0002-01-01', '1000-01-01', '1582-01-01', '1583-01-01', '1600-01-01', '1800-01-01'];
foreach ($dates as $dateInput) {
$timestamp = $dateFormatter->parse($dateInput);
$dateTime = new \DateTime(date('Y-m-d', $timestamp), new \DateTimeZone('Europe/Berlin'));
$dateOutput = $dateTime->format('Y-m-d');
echo $dateInput . " " .$dateOutput . PHP_EOL;
}
0001-01-01 0000-12-30 - BAD
0002-01-01 0001-12-30 - BAD
1000-01-01 1000-01-06 - BAD
1582-01-01 1582-01-11 - BAD
1583-01-01 1583-01-01 - GOOD
1600-01-01 1600-01-01 - GOOD
1800-01-01 1800-01-01 - GOOD
我想说这并不完全是错误,只是一种不同的解释,或者是对一个棘手问题的不同解决方案选择:你如何在你使用的日历开始时倒计时?您看到的最终错误是混合选择 不同 解决方案的库的结果,因此误解了彼此的输出。
首先要知道的是,您使用的函数是建立在两个不同的库上的:
- 作为 PHP“intl”扩展的一部分,
IntlDateFormatter
建立在 ICU(Unicode 的国际组件)之上,由 Unicode 联盟维护 date
函数和DateTime
class 建立在timelib
之上,由 Derick Rethans 维护
正如您已经指出的那样,接下来要注意的是,他们在首次引入公历的日期开始出现分歧。我们可以比年份更具体:它是在 1582 年 10 月引入的,紧随其后的是儒略历 10 月 4 日和格里高利历 10 月 15 日。重要的是,这意味着世界上有 10 个不存在的日期会立即切换。
最后,让我们看看这两个库生成的实际时间戳,而不是来回格式化。请注意,Unix 时间戳应该表示在公历和 UTC 时区中表示为 1970-01-01 00:00:00.[= 的时间之后(或者,在这种情况下之前)的秒数。 22=]
为了使时间戳更易于阅读,让我们将它们分开以显示他们所说的在给定日期和 1970 年 1 月 1 日之间的天天数。
$dateFormatter = new \IntlDateFormatter(\Locale::getDefault(), 2, -1, new \DateTimeZone('UTC'), 1, 'yyyy-MM-dd');
$dates = [
'1582-10-03', '1582-10-04', '1582-10-05', '1582-10-06', '1582-10-07', '1582-10-08', '1582-10-09',
'1582-10-10', '1582-10-11', '1582-10-12', '1582-10-13', '1582-10-14', '1582-10-15', '1582-10-16'
];
foreach ($dates as $dateInput) {
$icuTimestamp = $dateFormatter->parse($dateInput);
$timelibTimestamp = DateTimeImmutable::createFromFormat('Y-m-d|', $dateInput)->getTimestamp();
$icuDays = abs(intval( $icuTimestamp / 60 / 60 / 24 ));
$timeLibDays = abs(intval( $timelibTimestamp / 60 / 60 / 24 ));
echo "Is {$dateInput} {$icuDays} days or {$timeLibDays} days before 1970?\n";
}
结果如下所示:
Is 1582-10-03 141429 days or 141439 days before 1970?
Is 1582-10-04 141428 days or 141438 days before 1970?
Is 1582-10-05 141427 days or 141437 days before 1970?
Is 1582-10-06 141426 days or 141436 days before 1970?
Is 1582-10-07 141425 days or 141435 days before 1970?
Is 1582-10-08 141424 days or 141434 days before 1970?
Is 1582-10-09 141423 days or 141433 days before 1970?
Is 1582-10-10 141422 days or 141432 days before 1970?
Is 1582-10-11 141421 days or 141431 days before 1970?
Is 1582-10-12 141420 days or 141430 days before 1970?
Is 1582-10-13 141419 days or 141429 days before 1970?
Is 1582-10-14 141418 days or 141428 days before 1970?
Is 1582-10-15 141427 days or 141427 days before 1970?
Is 1582-10-16 141426 days or 141426 days before 1970?
正如预期的那样,两个图书馆一致认为 1582-10-15 是 1970 年之前的 141427 天,但对更早的日期存在分歧:
- timelib 使用的是“proleptic Gregorian calendar”,假设 1582-10-14 比 1582-10-15 早一天,依此类推;这在历史上是不准确的,但使用起来更简单
- ICU 正在尝试按照当时欧洲人的方式来解释日期,因此 1582-10-04 仅比 1582-10-15 早 1 天;这在中间留下了一些模棱两可的日期,根据这种解释,这些日期根本不应该存在,ICU 将其解释为 Julian,奇怪的是 1582-10-05 给出的值与 1582-10-15
The ICU documentation 解释了这种 cut-over 行为,以及如何影响它。尽管没有完全记录,PHP 公开了相关方法,因此可以将 cut-over 设置为任意远的过去以匹配 timelib 行为:
$cal = IntlGregorianCalendar::createInstance();
$cal->setGregorianChange(PHP_INT_MIN);
$dateFormatter->setCalendar($cal);
将其添加到之前的代码中,我们得到匹配的输出:
Is 1582-10-03 141439 days or 141439 days before 1970?
Is 1582-10-04 141438 days or 141438 days before 1970?
Is 1582-10-05 141437 days or 141437 days before 1970?
Is 1582-10-06 141436 days or 141436 days before 1970?
Is 1582-10-07 141435 days or 141435 days before 1970?
Is 1582-10-08 141434 days or 141434 days before 1970?
Is 1582-10-09 141433 days or 141433 days before 1970?
Is 1582-10-10 141432 days or 141432 days before 1970?
Is 1582-10-11 141431 days or 141431 days before 1970?
Is 1582-10-12 141430 days or 141430 days before 1970?
Is 1582-10-13 141429 days or 141429 days before 1970?
Is 1582-10-14 141428 days or 141428 days before 1970?
Is 1582-10-15 141427 days or 141427 days before 1970?
Is 1582-10-16 141426 days or 141426 days before 1970?
注意:我相信 Symfony 跟踪器中的相应错误是 #29610