我们中的不少人应该都不会对「千年虫」问题有所陌生:它主要指代 2000 年附近发生的一系列计算机系统异常事件,起源于两位年份转换为四位年份的过程中发生的失误:98
可以解析成 1998
,99
可以解析成 1999
,而 00
却解析成了 1900
——「千年虫」问题最早在 1958 年便已被公开提出,但直到 2000 年即将到来之际才引起足够的重视。
从长远的角度来说,「千年虫」问题当然没有结束——我们还有 2100 年,2200 年,等等;但正所谓「一万年太久,只争朝夕」,我们距离下一个世纪的到来还有足足七十余年,哪怕响应中关村某高校「为祖国健康工作六十年」的口号,那也最多只需要考虑到 2082 年。那么在这几十年的时间内,我们是不是便可高枕无忧,「千年虫」问题是不是就暂时离我们而去了呢?
我们不妨想一想这样一个问题,把 00
解析成 1900
固然在某种程度上违反了我们的日常使用习惯,但是把 00
解析成 2000
,把 99
解析成 1999
,真的就那么顺理成章吗?这里自然便会引入一个「截断年份」(Pivot Year)的概念:两位年份倘若大于这个「截断年份」,在转换成四位年份的时候便会视为上一个世纪,倘若小于等于便会视为当前这个世纪。
我们熟知的「千年虫」问题可以视为将「截断年份」设置成了 1999
——当然目前主流的计算机系统和软件早已不是如此。考虑到目前主流的编程语言和不少标准库都支持解析两位年份,并能「正确」地将 00
解析成 2000
,那么我们不由得想问:「截断年份」到底是多少?经过简单的搜索引擎搜索,我发现似乎并没有人系统总结过不同编程语言和标准库是如何实现「截断年份」的——但经过一圈调研我发现,不同的实现简直是五花八门,精彩纷呈。在此仅提取 TIOBE 2022 年 10 月榜单前十名为例。
值得提醒的是:不少编程语言和标准库都支持自主设置「截断年份」,在此以默认值为准。
SQL
不同 SQL 数据库对「截断年份」的实现均有不同:
- Microsoft SQL Server:
2049
(49
/50
分别视为2049
/1950
) - MySQL / MariaDB:
2069
(69
/70
分别视为2069
/1970
) - Oracle:当前所在世纪的
99
年(99
/00
分别视为2099
/2000
) - PostgreSQL:
2069
(69
/70
分别视为2069
/1970
) - SQLite:不支持自动解析两位年份
PHP
PHP 对解析日期(如内置的 strtotime
函数等)有一套共同的准则。准则中规定了对于两位年份的解析范围应处于 1970
和 2069
之间,换言之,PHP 的「截断年份」为 2069
。
ASM / C++ / C
之所以把这三者列在一起是因为它们都不存在一个能够解析日期的标准库函数。不过,对 C/C++ 的 time.h
而言,标准库里定义了用于格式化日期的 strftime
函数,该函数对应 POSIX 的 strftime
。POSIX 同时存在用于解析日期格式的 strptime
,但并未在 C/C++ 标准库里有所对应。POSIX 的 strptime
规定两位年份在 68
/69
处截断(分别对应 2068
/1969
),换言之,「截断年份」为 2068
。
JavaScript
JavaScript 主要使用 Date.parse
函数解析日期,但 Date.parse
函数只保证能够解析 ISO 8601 格式的字符串——这一格式的年份是四位数。同时在 Chrome 107.0.5304.87、Firefox 102.4.0、以及 Node 18.12.0 下尝试解析 01 Jan 49
和 01 Jan 50
得到的实验结果是:49
/50
分别视为 2049
/1950
,亦即「截断年份」为 2049
。
Visual Basic / C
鉴于 TIOBE 目前将 VB.NET 视为 Visual Basic,故将其与 C# 放在一起讨论:两者均基于 .NET 平台。MSDN 上介绍了在 .NET 平台设置「截断年份」的方法。在 2022 年 10 月 23 日前,这个「截断年份」一直是 2029
,但一个 Pull Request(dotnet/runtime#76848
)让 .NET 平台从这一天开始变成了 2049
。Microsoft Excel 等软件继承了 2029
这一设定,而国产软件 WPS 将这一设定照抄了过来。由于这一 Pull Request 的存在,我们仍然无法知晓未来版本(或者干脆在未来时间)的 Microsoft Excel 和 WPS 是否会将其向后调整 20 年。
Java
在某种程度上说,Java 对「截断年份」的实现可能是这篇文章中最有趣的:在 Java 标准库里,「截断年份」被设置成了当前年份后的第 19 年(见 SimpleDateFormat
的文档)。本文撰写于 2022 年,故本文完稿时 41
/42
将分别解析成 2041
/1942
,而如果读者是在 2023 年看到的这篇文章,那么 42
便将会解析成 2042
了。
Python
Python 的 datetime
库内置了 strftime
和 strptime
函数,并明确了用于格式化日期的 strftime
和 C 语言标准库的 strftime
相一致(也因此同 POSIX 相一致)。我们合理推测用于解析日期格式 strptime
函数也和 POSIX 相一致。在 Python 3.10.8(Linux 平台)上的实验结果也证实了「截断日期」位于 2068
处。
总结
在这篇短短的文章中我们已经看到了数种不同的「截断年份」:2029
、2049
、2068
、2069
、2099
……而最早的 2029
已经距离我们不足十年!换言之,「千年虫」问题带来的影响绝非想象中的那样遥远。对于现有的计算机系统和软件而言,它们所使用的「截断年份」越晚,暴露出来问题的时间也就越晚。当然,对于新开发的计算机系统和软件而言,最好的解决方案可能仍然是——使用四位数年份存储时间。
以下是参考链接:
- https://en.wikipedia.org/wiki/Year_2000_problem
- https://www.toutiao.com/article/7071076546601026055/
- https://en.wikipedia.org/wiki/Date_windowing
- https://www.tiobe.com/tiobe-index/
- https://www.php.net/manual/en/datetime.formats.date.php
- https://pubs.opengroup.org/onlinepubs/9699919799/
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse
- https://learn.microsoft.com/en-us/dotnet/api/system.globalization.calendar.twodigityearmax
- https://github.com/dotnet/runtime/pull/76848
- https://docs.oracle.com/en/java/javase/19/docs/api/java.base/java/text/SimpleDateFormat.html