问 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 日/世纪规则,并且要让测试明确覆盖这些边缘情况。