在Excel中输入"1900-02-29",按下回车。程序不会报错,不会提示无效日期,而是平静地接受了这个输入,将它格式化为"1900/2/29"或"29-Feb-00"。
问题在于,1900年2月29日从未存在过。这一天在历史上从未发生过。
这不是Excel的某个冷门功能,而是全球十几亿用户每天都在使用的电子表格软件核心功能中的一个故意保留的错误。这个错误已经存在了四十二年,从1983年延续至今,而且Microsoft明确表示不会修复。
一个软件bug能存活四十二年,必然有不同寻常的原因。这个原因涉及计算机内存的物理限制、商业竞争的残酷法则、以及软件兼容性这个被低估的工程困境。
1900年为何不是闰年
要理解这个bug,首先需要理解闰年的规则——比大多数人知道的要复杂。
儒略历(Julian Calendar)由凯撒在公元前45年引入,规则简单:每四年一个闰年。这个规则导致平均每年365.25天,比实际的太阳年(约365.2422天)长了约11分钟。看起来微不足道,但累积到1582年,历法已经比太阳年落后了约10天。
1582年,教皇格里高利十三世颁布了格里高利历(Gregorian Calendar),改革了闰年规则:
- 能被4整除的年份是闰年
- 能被100整除的年份不是闰年
- 能被400整除的年份仍然是闰年
这条规则的数学本质是:平均每年 = 365 + 1/4 - 1/100 + 1/400 = 365.2425天,与太阳年非常接近。
根据这条规则:
- 1896年是闰年(能被4整除)
- 1900年不是闰年(能被100整除,但不能被400整除)
- 2000年是闰年(能被400整除)
- 2100年不是闰年
这就是为什么1900年只有28天,而不是29天。Excel接受"1900-02-29"这个日期,本质上是在接受一个历史上从未存在的幽灵日期。
Lotus 1-2-3的工程权衡
时间回到1983年1月26日。Lotus Development Corporation发布了Lotus 1-2-3,这是一个注定改变个人电脑产业的产品。
Lotus 1-2-3由Mitch Kapor和Jonathan Sachs开发,是第一个真正意义上的"杀手应用"(Killer App)。它将电子表格、数据库和图表功能集成在一个程序中,运行在IBM PC上,速度比竞争对手VisiCalc快得多。在发布后的第一年,Lotus 1-2-3就为公司带来了5300万美元的收入,到1985年,它占据了电子表格市场80%的份额。
在这个辉煌的商业成功背后,是严苛的工程约束。1983年的IBM PC运行在4.77 MHz的处理器上,内存只有640KB——不是640MB,是640KB。每字节内存都珍贵无比。
处理日期是电子表格的核心功能之一。要计算两个日期之间的天数,程序需要知道每个月有多少天,每年有多少天。最简单的实现方式是将日期存储为从某个起点开始的天数——这就是日期序列号的概念。
判断闰年需要检查三个条件。正确的实现伪代码如下:
function isLeapYear(year):
if year mod 4 != 0:
return false
if year mod 100 != 0:
return true
if year mod 400 == 0:
return true
return false
这在现代计算机上毫秒级完成,但在1983年的硬件上,每个除法操作都是昂贵的。Lotus 1-2-3的开发者做了一个简化的假设:只要能被4整除就是闰年。
为什么这样做?因为检查"能被4整除"只需要一次位运算:
if ((year & 3) == 0) // 等价于 year % 4 == 0
位运算& 3只需要检查最后两位是否为零,比除法快得多,在8位和16位处理器上尤其明显。这个优化跳过了世纪年的特殊情况,导致1800年、1900年、2100年都被错误地判断为闰年。
但Lotus的开发者可能这样想:谁会在电子表格里计算1900年之前的日期?那时的用户主要处理的是财务报表、销售数据,起点基本都是1900年以后。对于1900年之后到2100年之间的所有年份,这个简化规则完全正确——因为2000年确实是闰年。错误的1900年对当时的用户来说无关紧要。
这是一个典型的工程权衡:牺牲边界情况正确性,换取性能和简洁性。
Microsoft的兼容性困局
Microsoft进入电子表格市场的时机晚于Lotus。Multiplan于1982年发布,但市场反响平平。真正的转折点是Excel,它最初在1985年为Macintosh开发,1987年发布Windows版本。
Excel面临一个关键决策:如何处理日期序列号?
当时,Lotus 1-2-3是市场主导者。无数企业的电子表格文件存储在Lotus格式中。如果Excel要获得市场份额,必须能够无缝导入这些文件。
问题来了:Lotus 1-2-3的日期系统假设1900年是闰年。如果Excel正确地不将1900年视为闰年,那么从Lotus导入的文件中,所有日期都会偏移一天。
Microsoft做出了一个决定:故意复制Lotus 1-2-3的错误。
Microsoft官方文档这样解释:
“When Lotus 1-2-3 was first released, the program assumed that the year 1900 was a leap year, even though it actually was not a leap year. This made it easier for the program to handle leap years and caused no harm to almost all date calculations in Lotus 1-2-3. When Microsoft Multiplan and Microsoft Excel were released, they also assumed that 1900 was a leap year. This assumption allowed Microsoft Multiplan and Microsoft Excel to use the same serial date system used by Lotus 1-2-3 and provide greater compatibility with Lotus 1-2-3.”
这不是无能的表现,而是清醒的商业决策。正确性很重要,但兼容性决定了产品的生死。
Joel Spolsky与Bill Gates的对话
1991年6月17日,一个名叫Joel Spolsky的年轻人加入Microsoft Excel团队,担任程序经理(Program Manager)。他的任务是改进Excel的宏语言。
Spolsky后来成为著名的软件博主和企业家,但当时只是一个初入行业的程序员。在准备与Bill Gates的评审会议时,他发现了一个奇怪的现象。
Excel工作表的日期序列号以1900年1月1日为第1天。但VBA(Visual Basic for Applications)的日期系统以1899年12月31日为第1天。两个系统对同一天给出了相同的序列号,这怎么可能?
他找到资深的Excel开发者Ed Fries询问。
“检查1900年2月28日的序列号,“Ed提示道。
“是59,“Spolsky回答。
“现在看1900年3月1日。”
“是61!”
“60去哪了?”
“1900年2月29日。1900年是闰年!它能被4整除!”
“不错的猜测,但不正确,“Ed说。
Spolsky去查了资料。能被100整除的年份不是闰年,除非也能被400整除。1900年不是闰年。
“这是Excel的bug!“他惊呼。
“不完全是,“Ed解释,“我们必须这样做,因为需要能导入Lotus 1-2-3的工作表。”
“所以这是Lotus 1-2-3的bug?”
“是的,但可能是故意的。Lotus必须塞进640K内存。那不是很多内存。如果你忽略1900年这个特例,判断闰年只需要检查最低两位是否为零。那非常快且简单。Lotus的人可能觉得,对于那两个月的过去日期,错误并不重要。”
第二天,Spolsky参加了与Bill Gates的评审会议。Gates翻阅着他的规格文档,每页都有批注——他读完了整个文档。
最后,Gates抛出了终极问题:“有人真的在研究所有这些细节吗?比如所有那些日期时间函数。Excel有那么多日期时间函数。Basic会有相同的函数吗?它们会以相同方式工作吗?”
“是的,“Spolsky回答,“除了1900年1月和2月。”
沉默。会议室里的人面面相觑。
“好吧,干得好,“Gates说完,带着他批注过的文档离开了。
Spolsky后来解释,Gates的评审风格是问越来越难的问题,直到对方承认不知道答案,然后他就可以批评你准备不足。Spolsky是第一个答对了最难题目的人。
一个错误的三重影响
这个错误对Excel的影响远比想象中复杂。
第一重影响:WEEKDAY函数的偏差
在Excel中,=WEEKDAY("1900-01-01")返回2(星期一),但1900年1月1日实际上是星期一。等等,这好像是对的?
问题在于序列号。由于Excel认为1900年2月29日存在,所有从1900年3月1日开始的日期序列号都比正确值多1。例如,1900年3月1日的序列号是61而不是60。
如果这个偏差持续存在,WEEKDAY函数应该在所有1900年3月1日之后的日期都返回错误值。但Excel在1900年3月1日之后做了"修正”——序列号多1,但星期计算正确。这是通过在内部计算中补偿那个额外的"幽灵日"实现的。
唯一受影响的是1900年1月1日到2月28日之间的日期。这些日期的WEEKDAY返回值比实际多1。例如,1900年1月1日实际上是星期一,但Excel返回2(星期二)。
第二重影响:VBA与工作表的分歧
VBA的日期系统基准日期是1899年12月30日。这个奇怪的日期选择是为了同时兼容:
- Excel工作表的日期(从1900年1月1日为第1天)
- 那个幽灵的2月29日
结果是:VBA的日期序列号1代表1899年12月31日,而Excel工作表的日期序列号1代表1900年1月1日。两者差了1天,加上幽灵日的存在,形成了复杂的对应关系。
第三重影响:跨软件数据交换
当你在Python pandas中读取Excel文件,或者在Google Sheets中打开Excel文件时,日期可能会偏移一天。
Python的xlrd库文档明确指出:必须调整1900闰年bug。Stack Overflow上无数开发者困惑于为什么同样的日期在Excel和Python中差一天。
Google Sheets正确地知道1900年不是闰年,但为了兼容Excel格式,它也必须在导入时处理这个幽灵日。这导致Google Sheets和Excel在某些情况下显示不同的日期。
为什么Microsoft至今不修复
Microsoft完全有能力修复这个bug。他们为什么不?
官方文档列出了三个原因:
-
现有文件破坏:几乎所有Excel工作表中的日期都会偏移一天。需要逐一检查每个公式、每个条件格式、每个数据验证规则。
-
函数行为改变:WEEKDAY等函数在1900年初的返回值会改变,可能破坏依赖这些值的公式。
-
跨软件兼容性断裂:Excel与其他使用日期序列号的程序(如旧版Lotus 1-2-3文件)之间的兼容性将完全丧失。
第四个原因更根本:修复的成本是确定的、巨大的;而收益是模糊的、微小的。
有多少用户会用到1900年1月和2月的日期?历史研究者?家谱研究者?这个群体小到可以忽略不计。相比之下,修复这个bug可能影响的文件数量以亿计。
这是软件工程中一个残酷的现实:有时错误的价值在于它已经存在太久。
更广泛的日期系统困境
Excel的日期困境不是孤立的。几乎每个软件系统在处理日期时都面临类似的选择。
SQL Server的1753年起点
SQL Server的datetime数据类型最早只能存储1753年1月1日的日期。为什么是1753年?
这要追溯到1582年。当教皇格里高利十三世推行新历时,天主教国家立即采用,但新教国家拒绝接受"教皇的日历”。英国及其殖民地(包括美洲)一直沿用儒略历到1752年。
1752年9月,英国议会通过法案采用格里高利历。为了修正历法偏差,1752年9月2日之后直接跳到9月14日——这12天在英国历史上从未存在过。
SQL Server的Sybase祖先选择了1753年作为起点,正好跳过这个混乱的过渡期。这是另一个工程权衡:牺牲1753年之前的历史日期支持,避免处理历法转换的复杂性。
Unix Epoch的1970年起点
Unix系统将1970年1月1日00:00:00 UTC作为时间零点(Unix Epoch)。这个选择同样基于工程考量:
- 1970年代是Unix诞生的时代,选择一个"当前时代"的起点简化了32位整数的范围计算
- 32位有符号整数可以表示约68年的秒数,1970年到2038年正好覆盖当时预期的系统寿命
2038年问题(当Unix时间戳溢出32位有符号整数范围时)是这个选择的延续。
Mac的1904年起点
Excel for Mac最初使用1904年作为日期系统的起点,比Windows版本晚了四年。这个选择是为了与Macintosh系统的内部时间格式兼容。
直到今天,Excel中仍然存在一个选项:“使用1904日期系统”。如果你在Mac上创建的Excel文件传到Windows上打开,日期可能会偏移1462天(四年加一个闰日)。
一个幽灵日期的现代回响
四十二年后的今天,这个幽灵日期仍然在实际工作中制造麻烦。
一个常见场景:用Excel分析历史数据。假设你在研究1900年之前的出生和死亡日期,Excel无法直接处理。日期变成文本,无法进行日期计算。
另一个场景:跨平台数据管道。Python脚本读取Excel文件,日期序列号转换时必须考虑幽灵日。忘记这一步,数据就会偏移一天。
更隐蔽的问题是依赖WEEKDAY函数的历史计算。如果你用Excel计算1900年1月某天是星期几,结果会是错的。
一位开发者在Stack Overflow上分享了一个案例:他们公司的财务模型依赖日期序列号来计算债券利息。在跨越1900年边界的某些计算中,结果出现了1天的偏差。排查这个问题花了一周时间,因为没人会怀疑Excel的基本日期功能有错。
软件兼容性的代价
Excel的幽灵日期揭示了一个更深层的软件工程困境:兼容性是一种沉重的债务。
当你选择保留一个错误而不是修复它,你实际上是在借钱。这笔债务需要持续支付利息——每一个需要处理这个错误的新代码、每一个因这个错误而困惑的用户、每一次跨软件数据交换时的额外检查。
但修复债务的成本可能更高。Microsoft估计,修复这个bug将影响数十亿个现有的Excel文件。这些文件中的日期公式、条件格式、数据透视表都可能依赖当前的错误行为。修复这个bug的成本将由所有Excel用户承担,而收益只惠及极少数处理1900年初日期的人。
这是软件工程中一个反复出现的模式:早期的设计决策会无限期地影响后续发展。Lotus 1-2-3在1983年做出的简化假设,四十二年后仍然在塑造数十亿用户使用的软件行为。
这不仅仅是技术债务。这是生态债务——当一个产品成为某个生态系统的基础设施时,它的任何变化都会在整个生态系统中引起连锁反应。
结语
Excel接受1900年2月29日这个幽灵日期,不是bug,而是一份签署于1983年的兼容性协议。
Lotus 1-2-3为了节省内存,简化了闰年算法。Microsoft为了赢得市场,复制了这个错误。今天,Excel为了保护数十亿现有文件,继续保留着这个错误。
这不是一个孤立的事件。从SQL Server的1753年起点,到Unix epoch的1970年起点,再到Mac的1904年起点,每个软件系统在处理日期时都面临着类似的权衡。
日期看起来简单——年、月、日,三个数字。但当这三个数字需要跨越历法改革、跨越操作系统、跨越编程语言、跨越四十年的时间时,它们承载的是人类文明史、计算机发展史和商业竞争史的全部复杂性。
下次当你在Excel中看到1900年2月29日时,记住:这不是一个bug,这是历史。
参考资料:
- Microsoft Learn. “Excel incorrectly assumes that the year 1900 is a leap year.” Microsoft 365 Apps documentation.
- Joel Spolsky. “My First BillG Review.” Joel on Software, June 16, 2006.
- Wikipedia. “Lotus 1-2-3.”
- Wikipedia. “Gregorian calendar.”
- Wikipedia. “Century leap year.”
- Conor Cunningham. “1753, datetime, and you.” SQLskills, March 17, 2008.
- Stack Overflow. “What is the significance of 1/1/1753 in SQL Server?”
- Britannica. “Ten Days That Vanished: The Switch to the Gregorian Calendar.”
- Hacker News. “A leap year check in three instructions.”
- Wikipedia. “Unix time.”
- Microsoft Support. “Date systems in Excel.”
- David Turner. “Identifying leap years.” davecturner.github.io, August 7, 2020.
- Level Up Coding. “A Date That Never Existed.” January 6, 2026.
- Wikipedia. “VisiCalc.”
- WIRED. “Jan. 26, 1983: Spreadsheet as Easy as 1-2-3.”
- Wikipedia. “Leap year.”
- Wikipedia. “Adoption of the Gregorian calendar.”
- Stack Overflow. “How to convert a python datetime.datetime to excel serial date number.”
- Reddit r/excel. “Who do you blame for February 29, 1900?”
- Reddit r/IsItBullshit. “Microsoft had to replicate a bug in excel to make…”
- The Register. “I watched Excel meet 1-2-3, and beat it fair and square.” January 31, 2013.
- Wikipedia. “Macintosh 128K.”
- Stack Overflow. “Why does the date returns ‘31-12-1899’ when 1 is passed to it?”
- Baeldung. “Why Was 1 January 1970 Used as the Epoch Time?”
- Wikipedia. “Adoption of the Gregorian calendar.”
- Super User. “Why are Excel weekdays wrong for 1900?”
- GitHub Gist. “Convert between Excel serialdate and Python datetime with adjustment for 1900 leap year bug.”
- Stack Overflow. “How to find leap year programmatically in C.”
- Wikipedia. “Mitchell Kapor.”
- ExcelUser.com. “How to Work with Dates Before 1900 in Excel.”
- Stack Overflow. “Why is 1899-12-30 the zero date in Access / SQL Server instead of 12/31?”
- Medium. “The 100-Year Bug Microsoft Never Fixed.” July 27, 2025.
- Dev.to. “Fixing a 40-year-old Software Bug.” March 16, 2021.
- Wikipedia. “Leap year problem.”
- BetterSolutions.com. “1904 Date System - Excel.”
- Fenying.net. “Two datetime systems in Excel.”
- Wikipedia. “Leap year.”
- Wikipedia. “Year 1900 problem.”
- Wikipedia. “Adoption of the Gregorian calendar.”
- Microsoft Learn. “Date & Time Functions - LibreOffice Help.”