公元0年2月的一个故障
A glitch in February of the year 0

原始链接: https://28times.com/blog/2026-06-26-february-of-the-year-0

Lukas Gelbmann 报告了一个在增加对公元 0 年历史时间戳支持时发现的隐蔽 Bug。团队观察到,公元 0 年 1 月末和 2 月的某些时间戳总是会产生一天的误差。 最初怀疑是自身逻辑的问题,团队深入调查了将 Unix 时间戳转换为 PHP `DateTimeImmutable` 对象的过程。他们发现,尽管多种实例化方法通常会产生相同的结果,但 `new DateTimeImmutable('@-62164356180')` 却为公元 0 年产生了错误的日期。 根本原因在于 `timelib`,即驱动 PHP 日期/时间功能的内部库。该库在时间戳转换时使用了两种不同的算法;其中一种算法对世纪闰年的范围检查有误,导致了这一天误差。 团队通过改用另一种实例化方法(`DateTimeImmutable::createFromFormat('U', ...)`)解决了该问题。此外,Gelbmann 向 `timelib` 提交了一个合并请求,旨在标准化转换逻辑并修复根本缺陷,从而确保未来版本的 PHP 能够正确处理这些古老的时间戳。

抱歉。
相关文章

原文

Recently, as we were adding support for timestamps in the distant past, a team member noticed during testing that some timestamps weren’t handled correctly. The issue could be easily reproduced with the timestamp 0000-02-03 04:00 Europe/Oslo.

A first investigation showed that the problem affected all time zones, but only in February of the year 0 (as well as the last few days of January).

Most time series don’t have timestamps that are two thousand years in the past. But, of course, we want to parse all timestamps in the supported range correctly, even the rare cases dating back to antiquity.

Time for bug hunting

We started looking for the bug, assuming that, surely, it would be in our own code. We use the calendar logic provided by the PHP runtime (the DateTimeImmutable class), but we still have some non-trivial processing on timestamps. To deal with timestamps that are ambiguous because of a time zone transition, we compute a Unix time stamp internally, and then convert it to a PHP DateTimeImmutable.

The year 0 is a bit of an outlier in two ways. First, it doesn’t exist in the traditional Julian calendar that historians use (they would call it 1 BC instead). Second, in the proleptic Gregorian calendar with astronomical year numbering (which is the calendar that we use on 28times), it’s a century leap year. Century leap years are the exception to an exception: years divisible by 100 are not leap years, unless – like the year 0 – they are divisible by 400.

This gave us a vague idea of why the year 0 might be impacted by a bug. But since other century leap years (such as 2000) were unaffected, this couldn’t be a full explanation.

So we went through our code step by step and found the problem. To our surprise, it wasn’t in our code. The problem was related to the idiom we used to convert a Unix time stamp to a DateTimeImmutable object.

The root cause

Below are three ways of converting a Unix time stamp to a DateTimeImmutable in PHP.

  1. \DateTimeImmutable::createFromFormat('U', '-62164356180')
  2. (new \DateTimeImmutable('@0'))->setTimestamp(-62164356180)
  3. new \DateTimeImmutable('@-62164356180') // incorrect return value

These three should be completely equivalent – and for most timestamps, they are. But unlike the first two, the last variant gives a result that’s off by one day for February of the year 0. At time of writing, this happens in all recent PHP releases. As luck would have it, the last method is also the one we used in our code.

(You can also substitute DateTime for DateTimeImmutable in these three snippets. DateTime has the same problem with the last variant.)

Fixing the issue

For our own purposes, the fix was simply to use one of the first two methods. This is also what I would recommend to other PHP programmers using DateTime or DateTimeImmutable.

I also opened a pull request to fix the bug in the library timelib, which provides date/time functionality. PHP’s DateTimeImmutable uses this library internally.

The problem ended up being that timelib has two implementations for converting a Unix timestamp to a date in the proleptic Gregorian calendar. One of them has a range check that uses the wrong date – a date that falls in January of the year 0, around a month before the century leap day instead of after it. This causes all results before the century leap day to be off by one day. I proposed fixing it by making all callers use the correct algorithm.

This issue will hopefully be fixed in upcoming releases of timelib and PHP. It certainly made for a satisfying bug fix – we have a clean workaround, and improving timelib and PHP is a nice bonus.

联系我们 contact @ memedata.com