跳转到主要内容
技术

代码中的夏令时 - 常见 Bug 及规避方法

间隙与重叠 - 夏令时的两种异常

夏令时转换每年两次打破时间连续性。在春季拨快时 (例如美国东部时间凌晨 2:00 跳至 3:00),2:00-2:59 的时间根本不存在 (间隙)。在秋季拨回时 (凌晨 2:00 退回到 1:00),1:00-1:59 的时间出现两次 (重叠)。这两种异常产生了数量惊人的软件 bug。

如果用户输入了一个处于间隙中的时间 (比如春季转换日的凌晨 2:30 EST),正确的行为并不明确。作为验证错误拒绝它?向前取整到 3:30 AM EDT?向后取整到 1:30 AM EST?每种选择都有道理。风险在于许多库默默地选择了一种,让期望另一种行为的开发者感到意外。

时长计算 - 24 小时后不等于明天同一时间

在夏令时转换日,一个日历日是 23 小时或 25 小时。“在当前时间上加 24 小时”和“给我明天的同一时间”通常一致,但在转换日它们不同。在春季拨快日凌晨 1 点加 24 小时得到次日凌晨 2 点,而“明天同一时间”是凌晨 1 点。

这种区别对循环调度 (“每天上午 9 点发送邮件”) 和截止时间 (“24 小时内取消”) 有实际影响。对于“每天相同的挂钟时间”,加一个日历日;对于精确的 24 小时物理时间,在 UTC 时间戳上加 86400 秒。设计应该明确区分这些意图,而不是将它们视为等同。

重复事件 - 日历应用的挑战

一个重复的“周二 14:00”约会应该在夏令时转换后仍然保持在当地时间 14:00。用户的意图是本地挂钟时间,而非固定的 UTC 时刻。因此重复事件应存储为 (时区, 本地时间) 对,每次出现时动态计算实际 UTC 时间。

RFC 5545 (iCalendar) 通过 DTSTART 和 RRULE 解决了这个问题。DTSTART 指定一个时区感知的本地时间,RRULE 描述重复模式。每次出现的 UTC 时间根据时区规则 (包括夏令时) 计算得出。采用这种设计,“本地 14:00”在转换后仍然是本地 14:00,避免了“我周一早上 9 点的定期会议在 3 月移到了 10 点”的尴尬投诉。

数据库存储 - TIMESTAMP 与 TIMESTAMPTZ

PostgreSQL 的 TIMESTAMP (WITHOUT TIME ZONE) 按原样存储值,不进行任何时区转换。TIMESTAMPTZ (WITH TIME ZONE) 在存储时将输入转换为 UTC,在读取时转换回会话的时区。要正确处理夏令时,TIMESTAMPTZ 是正确的默认选择。

如果你还需要记住用户表达的原始本地时间 (例如用户预约的时间),仅靠 TIMESTAMPTZ 会在转换时丢失该信息。模式是存储 TIMESTAMPTZ 加上原始时区名称在单独的列中,在显示时恢复用户的意图。这样既保留了绝对时刻,也保留了用户特定于区域的意图。

夏令时测试 - 捕获一年两次的 Bug

夏令时 bug 一年只出现两次,因此很容易在常规测试中漏掉。测试代码必须明确包含转换前后的时刻:输入间隙中的时间、解决重叠中的重复时间、跨转换日的时长计算,以及跨越转换的重复事件。没有这些特定的测试用例,bug 会在生产环境中暴露。

固定测试环境的时区也很重要。如果开发者在 UTC+9 (无夏令时) 上运行,但生产环境运行在 America/New_York (有夏令时),测试在本地通过但在生产中失败。要么将时区作为参数传递给被测函数,要么在测试运行期间显式设置 TZ 环境变量,以消除结果对环境的依赖。

XB!LINE

这篇文章对您有帮助吗?

相关文章