问 HN:你今天遇到任何闰年 bug 了吗?
2024 年的闰日暴露了令人惊讶的大量软件和硬件故障,从支付系统、门禁锁、游戏和手表,到单元测试、计费系统,甚至大型语言模型都把 2 月 29 日判为无效。参与者把许多错误归因于天真的日期运算(比如“减一年”)、自定义时间处理,以及滚动周年、证书和保留窗口等未在闰年中测试过的边缘情况。这些事故再次印证了长期以来的建议:依赖成熟的日期/时间库,为时间制定更清晰的业务规则,并明确测试罕见的日历情况,而不是假定它们会“自动正常工作”。
真实世界的中断和运营问题
- 多个支付系统失效:
- 瑞典一家大型连锁超市和新西兰的自助加油机都无法处理刷卡支付,只能改用现金或基于应用的替代方案。
- 一些信用卡和开票系统在遇到 2 月 29 日时,把到期日或通知安排错了。
- 物理世界系统出现故障:
- 酒店房卡失效,需要专用硬件重置;有人提到 2020 年也发生过类似问题。
- 巴黎和至少一个校园的路灯夜间不亮,可能与基于日期的控制有关。
- 门禁摄像头和一些太阳能/能源监控系统阻止进入或停止记录数据。
- 面向消费者的服务和游戏:
- 几个 EA 游戏以及特定游戏(例如赛车和节奏类标题)崩溃,或把玩家锁在外面,直到把系统时钟改到 3 月 1 日才恢复。
- 航空公司和交通应用打印出错误日期,或把日程标错,有时还会通过横幅免责声明作为临时处理办法。
- Cloudflare 的计费在一次更大的计费事件中生成了日期为“1970-01-01”的发票。
编程中关于闰年的陷阱
- 许多 bug 都来自天真的“加/减一年”逻辑:
- 使用
replace(year=year±1)或假设 365 天的代码会抛错,或者产生错误日期(例如 DB2 中的 1894‑02‑29)。 - 滚动年度和 YTD 计算会失败,因为比较年份里没有 2 月 29 日。
- 依赖“今天”或假设固定年份长度的单元测试会在 CI 中坏掉。
- 使用
- 讨论了 Python、Java 和其他语言:
- 标准的
timedelta/Duration只支持天/秒;“年”是有歧义的。 - 提到 dateutil 的
relativedelta、Arrow、Carbon、date-fns 和 .NET 的AddYears等库更安全,但边缘情况仍然需要明确决策。
- 标准的
- 关于定义的争论:
- “减一年”可以指 365/365.25 天、相同的日历日期,或者相同的“语义”日期(例如一月的最后一个星期一)。
- 一些人认为,只要行为一致且不会崩溃,任何结果都可以接受;另一些人强调法律和业务规则通常期望的是“12 个日历月”。
用户体验上的怪事与生日
- 许多手表、应用和表单会直接跳过 2 月 29 日,显示 3 月 1 日,或者把 29 日设为无效的二月日期;有些设备会故意把二月硬编码为 28 天。
- 闰日生日暴露了边缘情况:
- 系统会把它们映射到 2 月 28 日或 3 月 1 日,用于年龄检查、证件和法律门槛,有时还会错误地阻止操作。
- 有人报告表单缺少 2 月 29 日,或者前端接受了它但保存成 2 月 28 日。
LLMs 与元层面的教训
- 有多条报告称 ChatGPT 和其他 LLM:
- 最初声称 2024 不是闰年,或者拒绝把 2024‑02‑29 视为有效日期,随后又在解释过程中自行纠正。
- 在与分词相关的任务上表现吃力(例如统计一个单词里的字母数)。
- 一些人认为这证明 LLM 不应承担关键逻辑;另一些人则认为,只要置于沙盒中,且用于非关键、有人在环的工作流,它们是没问题的。
- 广泛共识是:时间处理出人意料地难;不要自己造日期逻辑,也不要忽略 2 月 29 日/世纪规则,并且要让测试明确覆盖这些边缘情况。