题记与说明
课题:时区与日期处理:UTC、ISO 8601、夏令时与日历计算陷阱
报告性质:横纵两轴综合报告(最终稿,已按独立审读意见逐条修订)
时间截面 / 调研日期:2026-11-05
综合自:
- 纵轴章节:《时区与日期处理 —— 纵轴发展史(含跨语言对照手册)》(parts/2026-11-05-时区与日期处理-vertical.md)
- 横轴章节:《时区与日期处理 —— 横轴横向对比(跨语言对照手册)》(parts/2026-11-05-时区与日期处理-UTC-ISO8601-夏令时-跨语言对照-horizontal.md)
综合说明(矛盾与重复的处理):两章重复出现的结论只保留一处主叙述,口径冲突处统一如下——
1. Temporal 状态:纵轴与横轴分别叙述,合并为统一口径:"2026 年 3 月进入 TC39 Stage 4;Firefox 139(2025-05-27)首发、Chrome/Edge 144(2026-01-13)跟进、Node.js 26(2026-05-05)落地,Safari(JavaScriptCore)截至截面日尚未发布"(依据 github.com/tc39/proposal-temporal 2026-11-05 实读、socket.dev)。
2. Rust 生态推荐:纵轴写"新项目 jiff",横轴写"chrono + chrono-tz 起步",统一为:默认 chrono + chrono-tz,jiff 作为观察项(jiff 最新 0.2.39、2026-11-02 更新,1.0 未发布,等 1.0 再评估迁移,见 crates.io API 实测、chrono#1768)。
3. moment.js 维护模式时间:统一为 2020-09(纠正常见的"2020-02"误传,见 moment.js 项目状态)。
4. tzdb 版本口径:以横轴实测为准——截面最新 2026f,2026 年已发 2026a–f 六版;纵轴的哈萨克斯坦案例(tzdata 2024a)保留为历史案例。
5. 跨语言对照:纵轴第八节总表与横轴第二~四节的大表、逐项点评合并去重,收敛为一张能力语义总表 + 一张数据生态表 + 五个试金石任务 + 一张陷阱速查表;DST 缺口/重叠的逐项行为细节只在任务 E 一处给出,2.2 与洞察三只保留判定词与结论。
6. 历史细节凡无引用出处的一律标"(通识)",属未逐条复核的模型既有知识(本次修订已补齐漏标处)。
TL;DR
时区处理之难,在四类问题叠加:天文、政治、历史、语言设计。纵轴约一百八十年(1840s 铁路时间起步,至 2026 Temporal 落地,对应 1.1–1.7 各节起止)模式清晰:制度层先行(1972 UTC 取代 GMT 成为法定时间基准、1988 ISO 8601 发布)→ 数据层(1986 tzdb 创建)作为活的基础设施持续运营 → 语言层以两代 API 偿还设计债——Java 的 Date 等 18 年才等到 java.time,JS 的 Date 等 31 年才等来 Temporal。横轴上,2026 年各生态已收敛到同一模型:时间点(绝对瞬时,Instant)、公民时间(不带时区的墙钟读数,LocalDateTime)、时长(Duration,两时点之间流逝的绝对时间量,不是"起止区间")三类分离,不可变,显式时区;最大残余分野是 DST 歧义处理——显式报错派与静默规范化派共存,同一输入跨语言结果不同。铁律:已发生的时刻存 UTC,未来事件存墙钟加时区 ID,展示才本地化;歧义显式选策略(场景→策略对照见 3.3);tzdb 版本纳入发布清单(查询与告警做法见 3.3)。
一、纵轴:发展脉络
时区与日期处理被戏称为"计算机科学两大难题之一"(另一个是缓存失效与命名),因为它同时叠加了四类性质完全不同的困难:天文物理(地球自转不均匀,需要闰秒这种非均匀补丁缝合原子时与太阳时)、政治法律(时区边界与夏令时开关由各国立法随时改写)、历史(tz 数据库必须能回答"1943 年上海是几点")、语言设计(几乎每门语言的第一代日期 API 都犯了同样的错误,再用二十年补救)。
纵轴上这条课题线呈现非常清晰的模式:制度层(UTC / ISO 8601)先行标准化 → 数据层(tzdb)作为活的基础设施持续运营 → 语言层两代 API 更迭(第一代原生陷阱 + 第二代第三方/重写补救)→ 闰秒与 DST 的政治博弈反过来重塑制度层。
1.1 制度起源(1840s–1972):铁路时间、格林尼治与 UTC 诞生
- 1840s–1883 铁路时间:铁路出现前,每个城镇用自己的地方太阳时。英国铁路率先统一格林尼治时间(1840 年大西部铁路,1847 年 Railway Clearing House 正式推行);美国各铁路公司 1883 年 11 月 18 日("双正午之日",Day of Two Noons)自发建立四大标准时区。这是"时间由主权实体标准化"的第一个现代先例(通识,见 Wikipedia: Railway time)。
- 1884 国际子午线会议:25 国代表在华盛顿确定格林尼治为本初子午线,提出以格林尼治为基准的"通用日",全球协调时间的制度骨架定型(通识,见 Wikipedia: International Meridian Conference)。
- 1960–1963 UTC 诞生:与 GMT 保持在一秒以内的协调时间体系 1960 年前后试用,1963 年 CCIR 第 374 号建议书首次正式采用"Coordinated Universal Time"。UTC 缩写本身是英法妥协的产物(英语 CUT 与法语 TUC 都不得罪)(见 Wikipedia: UTC)。
- 1972 关键转折:UTC 的秒长与原子时(TAI)绑定、与日长脱钩,用闰秒保证 UTC 与 UT1 之差不超过 ±0.9 秒;首个闰秒 1972-06-30 插入,当时 UTC 已落后 TAI 10 秒。多数国家同年以 UTC 取代 GMT 作为法定时间基准(见 NIST: Leap Seconds)。
- 1970 Unix 纪元:Unix 将 1970-01-01T00:00:00Z 定为
time_t零点,为所有软件的"绝对时间"提供公共坐标系——也埋下 32 位系统的 Y2038 隐患(通识)。
驱动因素:跨洋通信与铁路需要统一时间;原子钟成熟使"秒"与"日长"脱钩成为可能;计算机需要简洁连续的计数坐标系。
1.2 标准化与数据奠基(1970s–2002):ISO 8601 与 tz 数据库
- 1988-06-15 ISO 8601 第一版发布,统一取代 1970 年代散落的五项标准:ISO 2014:1976(全数字日历日期)、ISO 2015:1976(周编号)、ISO 2711:1973(序数日期)、ISO 3307:1975(一日内时间)、ISO 4031:1978(时区偏移表示)。
2026-11-05T01:30:00Z这种表示法自此有了单一权威出处(见 Wikipedia: ISO 8601、NIST FIPS PUB 4-1 (1991);委员会工作草案见 ISO TC154 8601-1 WD)。后续修订:2000、2004,2019 年拆分为 ISO 8601-1 与 ISO 8601-2(后者引入扩展日期与不确定性表示)。 - 1986-12-16 tz 数据库诞生:Arthur David Olson(NIH)发布创建帖,开始编纂全球本地时间历史数据库(zoneinfo),后由 Paul Eggert 长期协调,1990 年代后期起由 IANA 托管,成为事实上的全球时区数据源——Linux/macOS 直接使用,Java/Python/PHP/ICU/Windows 也以某种方式映射到它(见 Wikipedia: tz database、ICANN 博客 (2023)、OneZero 对 Eggert 的特稿)。tzdb 的核心哲学:数据是活的——法律一变就发新版本,全球操作系统与运行时跟进。
- Internet 协议层的日期格式:RFC 822 (1982) 邮件日期头 → RFC 1123/2822 修订 → RFC 3339(2002-07)把 ISO 8601 剖面定为互联网标准时间戳格式(见 RFC Editor: RFC 3339);其 2024 年的 IXDTF 扩展(RFC 9557)越出本段时间窗,归入 1.7。
1.3 第一代标准库 API:陷阱被"制度化"(1989–2005)
这一阶段的特点是:各语言几乎独立地、平行地犯下同一批设计错误,并把这些错误固化成标准库。
- C(ANSI C 1989):
time.h提供time_t/struct tm/localtime/gmtime。陷阱:localtime返回静态缓冲区(非线程安全)、DST 靠tm_isdst三态猜测、32 位time_t有 Y2038 溢出、mktime无法表达回拨歧义(通识)。 - Java:JDK 1.0(1996)的
java.util.Date——月份 0 起算、年份带 1900 偏移、对象可变;JDK 1.1(1997)紧急弃用 Date 大部分方法,引入Calendar/SimpleDateFormat,但 Calendar 仍可变、月份仍 0 起算、SimpleDateFormat线程不安全(见 Oracle: java.util.Date javadoc)。 - JavaScript(1995):Brendan Eich 十天写出 Mocha,
Date直接抄自java.util.Date,把月份 0 起算、可变性、解析不一致原封搬进浏览器(通识)。TC39 官方文档至今称 Date 为 "a long-standing pain point"(见 TC39 Temporal 文档)。 - Python 2.3(2003):引入
datetime,确立 naive/aware 二分——概念干净,但标准库长期不带 IANA 时区数据;同年第三方库 pytz 问世,其localize/normalize协议成为此后二十年 Python 时区事故的头号来源(通识)。 - .NET 1.0(2002):
DateTime结构 + 后补的DateTimeKind三态(Local/Utc/Unspecified)。微软官方文档直言:只有 Kind=Utc 全局无歧义,Local 出了本时区就可能多解,Unspecified 连本地都可能歧义(见 Microsoft Learn: Compare date and time types)。
驱动因素:早期 API 只服务"本机、本地时区、近期日期"的单机场景;可变对象契合当时的 OOP 风格;没人预料到软件会跑在历史规则随时变化、跨时区协作的分布式环境里。
1.4 补救一代:第三方库黄金期(2001–2012)
标准库的失误催生了一整代"外挂式"日期库,这一代库反过来定义了后来标准库重写的方向:不可变、显式时区、区分时间点与公民时间(civil time)。
- Joda-Time(2002 起,1.0 于 2005):Stephen Colebourne 发起,成为 Java 生态事实标准;对 DST 缺口抛
IllegalInstantException的显式失败路线见 Joda FAQ(见 JCP JSR-310)。 - Noda Time(2009 起,1.0 于 2012-11):Jon Skeet 主导,直接动机是 BCL 只有 DateTime/DateTimeOffset 两个类型却要覆盖所有场景;其 2011 年博文《What's wrong with DateTime anyway?》系统批判了
DateTimeKind的歧义与"有偏移 ≠ 知道时区"的混用(见 InfoQ 访谈 (2012)、Noda Time 博客 (2011))。 - moment.js(2011,Tim Wood)(通识):填补 JS 生态空白并成为最流行 JS 日期库——但其可变 API 与体积问题也由它自己埋下(API 面见 momentjs.com/docs)。
- Go(2009 设计,1.0 于 2012):标准库
time.Time一开始就把Location作为一等公民、类型不可变,规避了前代多数陷阱;2017 年 Go 1.9 又补上单调时钟(越出本段时间窗,归入 1.5)。 - PHP:5.1(2005)重写日期引擎、5.2(2006)引入
DateTime类(Derick Rethans 的 timelib);生态代表为 Carbon(2012)(通识)。可变DateTime的官方解药DateTimeImmutable要到 PHP 5.5(2013)才补上,同样越出本段时间窗、归入 1.5。
驱动因素:多线程并发使可变性成为致命伤;全球化服务使本地时区默认值成为致命伤;tzdb 数据需要跟着库走。
1.5 规则剧变与基础设施重写(2005–2021,政治剧变案例延续至 2024)
这是本课题的"高压期":政治规则接连突变,迫使全球软件基础设施反复重写,语言层也完成换代。
(1)政治规则剧变——tzdb/OS 厂商疲于奔命
- 美国 2007:《2005 能源政策法案》把夏令时提前到 3 月第二个周日,Windows/Java/各 OS 紧急发补丁——"一国立法、全球软件加班"的教科书案例(通识)。
- 俄罗斯 2011→2014 先永久夏令时再回调永久冬时;土耳其 2016、墨西哥与伊朗 2022 废除、埃及 2023 恢复夏令时(通识,tzdb NEWS 均有记录)。
- 哈萨克斯坦 2024-03-01:全国合并为单一 UTC+5,IANA tzdata 2024a 把 5 个东部 zone 改为指向 Asia/Almaty 的 link(见 timeanddate、Reuters、Microsoft DST 博客)。
- 黎巴嫩 2023-03:政府在改钟前两天突然宣布推迟夏令时一个月,全国出现"一国两时",学校、航班、手机自动更新用户全部遭殃,四天后政府收回成命——tzdb 社区"我们只是数据的搬运工"的极端注脚(见 The Register、Reuters)。
- 欧盟废除夏令时悬案:2018 年欧委会公众咨询收到约 460 万份回应、八成赞成废除;2019-03-26 欧洲议会以 410:192 通过 2021 年前终止双季拨钟;但理事会至今未达成共同立场,2026 年 10 月的报道确认拨钟仍在继续(见 欧洲议会新闻稿、理事会专题页、Euronews (2026-10-22))。
(2)tzdb 社区自身的两次内战
- 2013–2014:tzdb 把 1970 年前历史数据移入
backzone,ICU 维护者不满一度威胁 fork,催生 timezone-integrity 邮件列表(通识)。 - 2021-09:Eggert 提议把 1970 年后规则相同的 zone 合并(如 Europe/Oslo 指向柏林数据),引发"要不要 fork tzdb"的大论战;Colebourne 发文抗议"查 1970 年前的 Oslo 会得到柏林";合并提案最终撤回(见 LWN、Colebourne 博文、The Register)。IANA《Theory》文档的官方立场:1970 年前的数据只保证"命名城市"正确(见 IANA Theory)。
(3)语言层换代
- PHP 5.5(2013-06):
DateTimeImmutable。第一代可变DateTime(1.4 的 timelib 路线)的官方解药,不可变类型自此进入 PHP 标准库(通识)。 - Java 8(2014-03):JSR-310 / java.time。由 Joda-Time 作者 Colebourne 与 Oracle 共同主导、从零重写:不可变、纳秒精度、
Instant/LocalDateTime/ZonedDateTime/ZoneId与ZoneOffset明确分离;配套 ThreeTen-Backport(Java 6/7)与 ThreeTenABP(Android)(见 JCP JSR-310、Oracle: Java SE 8 Date and Time、InfoQ (2014)、Colebourne: Joda→java.time 迁移指南 (2014))。 - Python:PEP 495(3.6,2016)给 aware datetime 加
fold属性,显式消歧 DST 回拨的重复小时;PEP 615(3.9,2020-10)把zoneinfo纳入标准库,数据源优先系统 tzpath、回退 PyPItzdata包——pytz 时代走向终结(见 Python zoneinfo 文档、LWN、PEP 615、PEP 495 fold(backports.zoneinfo 文档))。 - Go 1.9(2017-08)单调时钟:Russ Cox 的提案(issue #12914)把单调时钟读数透明地藏进每个
time.Time值,使Sub计时在 NTP 跳变/闰秒平滑时依然安全——"墙钟时间与流逝时间必须分开"这一教训的最优雅落地(见 Go 1.9 Release Notes、Russ Cox 提案)。 - moment.js 进入维护模式(2020-09,注意不是常被误传的 2020-02):官方宣布"moment 是处于维护模式的遗留项目……它没有死,但确实完成了",理由:可变 API、体积、不支持 tree-shaking;官方推荐 Luxon、Day.js、date-fns、js-Joda 四个继任者(见 moment.js 项目状态、TuxCare 解读)。
- TC39 Temporal 立项:2017 年前后由 Maggie Pint、Matt Johnson-Pint、Brian Terlson 等发起,2021-03 进入 Stage 3(见 Temporal 提案仓库、Bloomberg 工程博客)。
1.6 闰秒:从工程灾难到政治废除(2012–2035)
- 2012-06-30 闰秒之夜:Linux 内核缺陷(主要影响 pre-2.6.29 的 hrtimer 代码)在插入闰秒时让线程陷入活锁、CPU 100%——Reddit(Cassandra)、Mozilla(Hadoop)、LinkedIn、Yelp、澳洲航空值机系统当晚接连瘫痪;临时解法竟是"手工重新设置系统时间"(见 Wired、Wikipedia: Leap second)。2015、2016 两次闰秒各大厂靠"平滑抹秒"(smearing,Google 首创)安然过关。
- 科技行业反攻制度层:Meta 2022 年发表论文主张废除闰秒,Google、Meta、Microsoft 等持续游说(通识)。
- 2022 年第 27 届 CGPM 第 4 号决议:到 2035 年前取消 UT1 与 UTC 之间 ±1 秒的差值上限——闰秒机制实质上被判死刑,部分是为照顾依赖闰秒的俄罗斯 GLONASS 留了缓冲期(见 BIPM 决议、NYT)。
- 2026 年第 28 届 CGPM 第 5 号决议:进一步处理"负闰秒"风险(历史上从未出现),估计到 2035 年出现负闰秒的概率约 30%(见 BIPM 2026 决议)。
驱动因素:闰秒对分布式系统(NTP 同步、数据库时钟、内核调度)的边际成本已远超其天文价值;地球自转近期反而变快,负闰秒风险加剧了废除的紧迫性。
1.7 当下与未来(2022–2026)
- Temporal 终成正果(2026 年最重要节点):TC39 官方仓库确认 Temporal 已进入 Stage 4、即将并入 ECMA-262/ECMA-402;实现时间线:Firefox 139(2025-05-27)首发、Chrome/Edge 144(2026-01-13)跟进、Node.js 26(2026-05-05)落地,Safari 尚未发布(见 github.com/tc39/proposal-temporal(2026-11-05 实读)、Socket (2026-03)、Bryntum (2026-01))。Temporal 的八个日期时间类型(Instant、ZonedDateTime、PlainDate/PlainTime/PlainDateTime、PlainYearMonth、PlainMonthDay、Duration)加
Now命名空间,把"时间点 / 公民时间 / 时长(Duration)"彻底分开。 - Rust:受 Temporal 启发的 jiff(BurntSushi)按计划推进,但实测 crates.io 最新版 0.2.39(2026-11-02),1.0 尚未发布;chrono 仍是事实标准,生态已在讨论迁移(见 crates.io API 实测、chrono#1768 "soft-deprecating chrono")。
- C++:C++20 把 Howard Hinnant 的 date/tz 库收编为
std::chrono的日历与时区部分(tzdb、zoned_time),补齐了 C++11"只有 duration 没有日历"的缺口——标准 2020 年定稿(事件已归入 1.8 总表"剧变与重写"行),本窗口内的进展是编译器矩阵补齐:GCC 13(2023-03)完成 libstdc++ 时区支持,MSVC 更早(2019 16.10+)已就绪(见 cppreference: std::chrono、cppstories: zones_around_world_chrono)。 - .NET:官方指南已把
DateTimeOffset定为应用开发的默认类型,.NET 6 补入DateOnly/TimeOnly(见 Microsoft Learn)。 - Y2038 进入倒计时(约 11 年半):Linux 内核 5.6(2020)为 32 位架构提供 64 位 time_t 系统调用,glibc 需以
-D_TIME_BITS=64显式启用(2.34+);截至 2026 年 2 月 GCC bugzilla 仍在推进 32 位目标默认启用事宜——工具链已就绪,嵌入式长尾(预期寿命 10–15 年的设备)是主要风险面(综述性指引,未逐条复核)。 - 未来变量:欧盟废除夏令时案(悬置)、CGPM 闰秒新框架落地、RFC 9557 IXDTF(2024,在 RFC 3339 之上扩展历法名、语言等附加信息后缀,见 RFC Editor: RFC 9557)在各语言的采用、tzdb 1970 前数据的存废。
1.8 阶段总表
| 阶段 | 时间 | 关键事件/版本节点 | 主要参与者 | 驱动因素 |
|---|---|---|---|---|
| 制度起源 | 1840s–1972 | 铁路时间 → 1884 子午线会议 → 1963 UTC → 1972 闰秒体系 | CCIR/ITU、BIPM、授时台、Bell Labs | 铁路/通信、原子钟 |
| 标准化奠基 | 1970s–2002 | 1986 tzdb 创建;1988 ISO 8601 第一版;RFC 822 (1982)→RFC 3339 (2002) | ISO TC154、Olson、Eggert、IETF | 数据交换无歧义需求 |
| 第一代 API | 1989–2005 | C time.h(ANSI C 1989)、Java Date/Calendar、JS Date、Python datetime、.NET DateTime | 各语言标准库 | 单机本地场景、可变 OOP 风格 |
| 补救一代 | 2001–2012 | Joda-Time、pytz、moment.js、Noda Time、Go time | Colebourne、Skeet、Cox 等个人开发者 | 并发、全球化、tzdb 数据分发 |
| 剧变与重写 | 2005–2021 | 美 2007 DST 变更;欧盟 2018–2019 废除动议;tzdb 2013/2021 两次内战;PHP 5.5 DateTimeImmutable(2013);Java 8(2014)、PEP 495/615、Go 1.9 单调时钟(2017)、C++20 chrono 定稿(2020)、moment 弃维(2020)、Temporal Stage 3(2021) | 各国政府、Eggert、Oracle、CPython、Hinnant、TC39 | 政治规则剧变 + 移动/云全球化 |
| 闰秒废除 | 2012–2035 | 2012 Web 大瘫痪;CGPM 27th (2022) 决议 4;28th (2026) 决议 5 | IERS、BIPM、Google/Meta、GLONASS | 分布式系统成本 > 天文价值 |
| 当下与未来 | 2022–2026 | Temporal Stage 4(2026-03);FF139/Chrome144/Node26 落地(2025–2026);jiff 0.2.39(2026);Y2038 倒计时 | TC39、BurntSushi、glibc/内核 | 补课收尾、长尾设备 |
二、横轴:横向对比
2.1 对比对象与四层结构
横轴在 2026-11-05 截面上把"时区与日期处理"的承担者分为四层(同层互为竞品/替代方案):
| 层 | 对象 |
|---|---|
| L1 语言内建 | JS Date / JS Temporal、Python datetime+zoneinfo、Java java.time、C# DateTime/TimeZoneInfo、Go time、C++20 chrono、Swift Foundation、PHP DateTime、Ruby Time/TZInfo、C time.h |
| L2 事实标准第三方库 | Luxon、date-fns、Day.js、js-joda;Noda Time;chrono+chrono-tz、time crate、jiff;Carbon;tzinfo;Joda-Time(被替代者) |
| L3 数据与标准 | IANA tzdb vs Windows 时区库 vs ICU/CLDR;ISO 8601 vs RFC 3339 vs epoch |
| L4 架构策略 | 存 UTC 瞬时 / 存墙钟+时区 ID / 存固定偏移 / 存 epoch 秒 |
2.2 核心能力总表:建模与 DST 语义
试金石:2026 年美国东部时区 2026-03-08 春季前拨(该日只有 23 小时,02:00 直接跳 03:00)与 2026-11-01 秋季回拨(01:30 出现两次)——后者距本报告截面仅 4 天。
| 方案 | 三类时间建模 | DST 歧义默认(判定词;逐项行为见任务 E) | 日历日 +1 语义 | 判定 |
|---|---|---|---|---|
| JS Date(1995) | 仅 1 种:UTC 毫秒 + 隐式本地 | 无检测 | setDate 墙钟溢出 | ❌ 原罪级缺陷,仅够渲染 |
| JS Temporal(Stage 4,2026-03) | Instant/ZonedDateTime/PlainDateTime 严格分离 | 静默规范化(默认 compatible),可选 reject | add({days:1}) 保证日历日、保留墙钟 | ✅ 新标准 |
| Python datetime+zoneinfo(3.9+,PEP 615) | naive/aware 二分(无独立 Zoned 类型) | 静默规范化(fold 需调用方手动选边) | aware 的 +timedelta(days=1) 是墙钟相加,落缺口会静默漂移 | ⚠️ 可用但要懂 fold |
| Java java.time(Java 8+,JSR-310) | Instant/ZonedDateTime/LocalDateTime 严格分离 | 静默规范化 | plusDays 日历语义;plus(Duration.ofHours(24)) 才是绝对时长 | ✅ 业界模板(javadoc) |
| C# TimeZoneInfo(+DateTimeOffset) | DateTimeOffset + Kind,模型偏弱 | 显式检测(转换抛 ArgumentException) | AddDays 墙钟语义,不校验 | ⚠️ 检测齐但 API 别扭;Noda Time 补齐 |
| Go time | 单一 Time(内含 location),无独立墙钟类型 | 静默规范化(官方明言"选择不保证") | AddDate(0,0,1) 日历语义;Add(24h) 绝对时长 | ⚠️ 实用主义,歧义靠自觉(pkg.go.dev/time) |
| Rust chrono + chrono-tz | DateTime\<Tz\> / NaiveDateTime / Utc 分离 | 显式失败(LocalResult) | checked_add_days(DST 感知,落缺口/重叠返回 None);+ Duration 是绝对时长 | ✅ 最显式(docs.rs/chrono) |
| C++20 chrono | time_point / zoned_time / local_time 分离 | 静默规范化(需 get_info 自查) | year_month_day 纯日历运算 | ⚠️ 设计对,编译器矩阵碎片化(见 2.3) |
| PHP DateTimeImmutable(+Carbon) | 单类型 + 时区参数 | 静默规范化(宽松) | modify('+1 day') 墙钟 | ⚠️ 行为宽松需自测 |
| Ruby TZInfo 2.x(+Rails ActiveSupport) | Time + zone 对象 | 显式失败(抛错) | Rails 1.day 日历语义 | ✅ 默认最保守 |
| Swift Foundation | Date(瞬时,2001 纪元 Double 秒)+ Calendar/TimeZone 分离 | 无显式 API(需自查) | Calendar.date(byAdding:.day) 日历语义,正确跨 DST | ⚠️ 日历强、时区歧义弱 |
| C time.h | time_t/struct tm 单轨 | 无法表达(tm_isdst 三态猜测) | 手工算术,无日历类型 | ❌ 仅够系统层 |
| Elixir/Erlang(额外参照) | DateTime/NaiveDateTime 分离 | 显式(需显式给 zone) | 按函数区分 | ⚠️ 显式但繁琐 |
读表要点:行业在 DST 歧义上分裂为两派——显式失败派(Rust chrono、Ruby TZInfo、C# 检测、Temporal 可选 reject)与静默规范化派(Java、Go、Python、C++、PHP)。默认行为要分场景说:缺口(不存在的时刻)下,静默规范化派的默认结果碰巧都等效于"跳过缺口"(前移到过渡后);重叠(重复的时刻)下,默认则是"取较早瞬时"(Temporal compatible、Java 默认、Python fold=0),并不存在"跳过"一说。这是同一个输入在不同语言里得到不同结果的根源——缺口与重叠的逐项默认值、Temporal 四种 disambiguation 选项的完整对照只在 2.4 任务 E 一处给出,本表不重复。
2.3 数据来源、更新成本与生态
| 方案 | 时区数据来源 | 规则更新方式 | 体积/运行时成本 | 生态与替代库 |
|---|---|---|---|---|
| JS Date | 引擎内嵌(随浏览器/Node 更新) | 随引擎 | 最小 | Moment(维护模式,2020-09 起)、Luxon、date-fns、Day.js(~2KB)、js-joda |
| JS Temporal | 引擎内嵌 ICU tzdata | 随引擎 | 中(重类型) | 官方 polyfill 回填旧环境 |
| Python zoneinfo | 系统 tzdata(Windows 走 tzdata pip 包) | 系统更新 / pip | 极小 | pytz(遗留,勿新用)、dateutil、Arrow、Pendulum;3.12 弃用 utcnow()(docs.python.org) |
| Java java.time | JDK 内嵌 tzdata | JDK 更新(或 tzupdater) | 极小 | Joda-Time 官方要求迁移(joda.org) |
| C# | Windows 注册表或 ICU(Linux);.NET 6+ 双向接受 Windows/IANA ID | Windows Update / ICU 随 OS | 小 | Noda Time、TimeZoneConverter(ID 映射兜底) |
| Go | 系统 zoneinfo;import _ "time/tzdata" 可内嵌(1.15+,约 +450KB) | 系统 / 重新编译 | 小(pkg.go.dev/time/tzdata) | 无主流第三方替代(官方库够用) |
| Rust chrono | 编译期由 chrono-tz 把 tzdb 烧进二进制(lib.rs/chrono-tz);或 chrono::Local 走系统 | 重新编译(升级 chrono-tz 即可) | 二进制 +数百 KB,换零运行时文件依赖 | time crate(更轻但不内建命名时区,chrono#1423);jiff(0.2.x,1.0 未发) |
| C++20 chrono | 系统 tzdata(Windows 10 1903+ 内置) | 随 OS/发行版 | 零依赖 | Hinnant date 库(旧编译器回退);支持矩阵:MSVC 2019 16.10+、GCC 13(2023-03)补齐(MSVC docs、GCC commit) |
| PHP | bundled timelib + tzdb | 随 PHP 版本 | 极小 | Carbon(生态最广) |
| Ruby tzinfo | 系统 tzdata | 系统 / gem 更新 | 小 | Rails ActiveSupport 包装 |
| Swift | ICU(随 OS) | 随 OS | 小 | SwiftDate 等 |
中间件层(跨语言共享的基础设施):IANA tzdb 是所有语言最终归一的数据源,版本滞后即事故(老机器不更新 = 黎巴嫩式混乱);ICU/CLDR 是 Java、C++、Swift、Node、Windows 部分路径共享的本地化中间层;各运行时自建分发机制(Java tzupdater、Python tzdata pip 包、Go time/tzdata 内嵌、Windows 月度 DST 更新);IANA ↔ Windows 时区 ID 映射(TimeZoneConverter、ICU 映射表)仍是跨平台集成的事故高发区——"同一时区两套名字"。
2.4 同一任务的跨语言写法:五个试金石任务
统一假设:目标时区 America/New_York;试金石时刻 2026-11-05T01:12:27Z——它在纽约是 2026-11-04 20:12:27 EST(夏令时已于 2026-11-01 结束,偏移从 −04:00 变 −05:00)。
抽样说明:五个任务不追求每任务覆盖全部语言——每任务只列该场景下最有代表性或最易踩坑的写法。任务 A 取九门、任务 D 取八门(PHP 未单列,判定以 2.2/2.7 为准);任务 B 聚焦解析行为差异最集中的六门;任务 C/E 补入 C++20 chrono(其转换与歧义行为值得单独对照);Swift 缺席 B/C/E、PHP 缺席 B/C/D,仅表示该任务下未单列其写法(或无独立检测 API 可展示,如 Swift 的歧义检测),相应判定以 2.2 总表为准。试金石时刻贯穿任务 A/B/C:以该时刻为"当前",任务 A 的正确输出即 2026-11-05T01:12:27Z(或等价的 +00:00 形式),任务 B 的输入即该字符串,任务 C 的正确输出为 2026-11-04T20:12:27−05:00。
任务 A:取当前 UTC 瞬时并输出 ISO 8601
Temporal.Now.instant().toString() // JS 新写法;旧写法 new Date().toISOString()
datetime.now(timezone.utc) # 3.12+ 正解(utcnow() 已弃用)
Instant.now().toString()
DateTimeOffset.UtcNow.ToString("o") // "o" round-trip 格式才保证带偏移
time.Now().UTC().Format(time.RFC3339)
Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true)
(new DateTimeImmutable())->format(DATE_ATOM)
Time.now.utc.iso8601
ISO8601DateFormatter().string(from: Date())要点:以试金石时刻为"当前"时,以上写法的正确输出即 2026-11-05T01:12:27Z(或等价的 +00:00 形式);Python 3.11 起 fromisoformat 才接受 Z 后缀;JS 旧 Date 的 toISOString() 恰好是少数永远输出 UTC 的方法;C# 必须用 DateTimeOffset + "o" 组合。
任务 B:解析带偏移的 ISO 8601 / RFC 3339 字符串
Temporal.Instant.from("2026-11-05T01:12:27Z")
datetime.fromisoformat("2026-11-05T01:12:27+00:00") # 3.11+ 也接受 "Z"
OffsetDateTime.parse("2026-11-05T01:12:27Z") // 不建议 Instant.parse
DateTimeOffset.Parse("2026-11-05T01:12:27Z", CultureInfo.InvariantCulture,
DateTimeStyles.RoundtripKind)
time.Parse(time.RFC3339, "2026-11-05T01:12:27Z") // 无偏移输入默认按 UTC
DateTime::parse_from_rfc3339("2026-11-05T01:12:27+00:00")?要点:只有 RFC 3339 是"互联网交换"的严格子集——禁 24:00(ISO 8601 允许)、强制带偏移、-00:00 专表"本地偏移未知"(RFC 3339)。自研解析器最常见的坑是只支持一种子集却声称支持 ISO 8601(周日期 2026-W45-4、无秒、省略 T 等变体全部解析失败)。
任务 C:转换到目标时区显示
Temporal.Now.zonedDateTimeISO("America/New_York")
datetime.now(timezone.utc).astimezone(ZoneInfo("America/New_York"))
Instant.now().atZone(ZoneId.of("America/New_York"))
TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow,
TimeZoneInfo.FindSystemTimeZoneById("America/New_York")) // .NET 6+ Windows 也接受 IANA ID(需 ICU)
now.In(nyLoc) // loc, _ := time.LoadLocation("America/New_York")
Utc::now().with_timezone(&ny) // let ny: chrono_tz::Tz = "America/New_York".parse()?
zoned_time ny{locate_zone("America/New_York"), system_clock::now()};
Time.now.in_time_zone("America/New_York") # Rails;纯 Ruby 用 TZInfo::Timezone.get(...).now要点:以试金石时刻 2026-11-05T01:12:27Z 为输入,以上写法的正确输出为 2026-11-04T20:12:27−05:00(EST——夏令时已于 11-01 结束,偏移从 −04:00 变 −05:00);C# 的 FindSystemTimeZoneById 在 .NET 6+ 的 Windows 上直接接受 IANA ID,但依赖 ICU(MS Learn)。
任务 D:日历日 +1(试金石:2026-03-08,该日只有 23 小时)
ny.add({ days: 1 }) // Temporal:日历日,墙钟保留
new Date(ny.epochMilliseconds + 86400_000) // ❌ 反例:绝对 24h,墙钟漂移 1 小时
aware + timedelta(days=1) # ⚠️ 墙钟相加,落缺口会静默规范化
zdt.plusDays(1) // plus(Duration.ofHours(24)) 才是绝对时长
dt.AddDays(1) // 墙钟;绝对时长用 dt + TimeSpan.FromHours(24)
t.AddDate(0, 0, 1) // 日历;t.Add(24*time.Hour) 绝对
dt.checked_add_days(Days::new(1)) // Some/None:落缺口或重叠返回 None
dt + Duration::days(1) // 绝对时长,作用于瞬时
cal.date(byAdding: .day, value: 1, to: date) // 日历语义,正确跨 DST
1.day.from_now # Rails 日历语义要点:所有成熟设计最终都分化出两套 API——日历日(墙钟不变)与绝对时长(瞬时推进)。混用是"会议整整迟到了/早到了一小时"类 bug 的第一来源。Rust chrono 把分工写进了文档:TimeDelta 加法不做 DST 感知,checked_add_days 才感知并在歧义时返回 None(docs.rs/chrono)。
任务 E:DST 缺口与重叠(同输入、跨语言行为对照)
输入一:2026-03-08 02:30 America/New_York(不存在);输入二:2026-11-01 01:30 America/New_York(出现两次——截面前 4 天刚发生)。
| 方案 | 缺口 02:30(不存在) | 重叠 01:30(出现两次) |
|---|---|---|
| Temporal | 默认 compatible → 03:30 EDT;可 disambiguation:"reject" 抛错 | 默认取较早;可 "later"/"reject" |
| Python zoneinfo | 不报错,fold=0 按过渡前偏移解释 | fold=0→EDT、fold=1→EST,调用方必须自己知道 fold |
| Java | 规则化前移 → 03:30 EDT(javadoc) | 默认取较早偏移 |
| C# TimeZoneInfo | IsInvalidTime == true;ConvertTimeToUtc 抛 ArgumentException | IsAmbiguousTime == true,偏移需手动定 |
| Go | 不报错,官方文档明言"选择不保证" | 同左 |
| Rust chrono | LocalResult::None,调用方显式处理 | LocalResult::Ambiguous(earlier, later) |
| C++20 | 按规范取过渡前偏移解释 | 取较早偏移;需用 get_info 自查 |
| Ruby TZInfo 2.x | 抛 PeriodNotFound | 抛 AmbiguousTime |
| PHP | 宽松处理,不报错 | 宽松 |
2.5 数据与标准层:IANA tzdb / Windows / ICU,ISO 8601 vs RFC 3339
| 维度 | IANA tzdb | Windows 时区库 | ICU/CLDR |
|---|---|---|---|
| ID 风格 | Asia/Shanghai(区域/城市) | China Standard Time(注册表键名) | BCP-47 时区 ID(usnyc 等)+ 别名映射 |
| 更新机制 | 版本化发布,一年数版(截面最新 2026f,2026 年已发 2026a–f) | Windows Update | 随 OS/ICU 版本 |
| 历史/规则深度 | 最深(含 1970 前历史、别名链) | 浅,面向当前业务规则 | 中,含本地化名称 |
| 谁在用 | Linux/macOS、Java、Go、Python、Rust(chrono-tz)、JS 引擎(经 ICU) | 仅 .NET/Windows 生态 | 浏览器、.NET Linux、Temporal |
| 互操作性 | 事实标准 | 需映射表(TimeZoneConverter / .NET 6 内建桥接) | 与 IANA 双向映射 |
关键活案例:tzdb 2026c(2026-07-08)写入 America/Edmonton 自 2026-11-01 起永久 UTC−6(Alberta 新 Time Act)——横截面前 4 天的规则变更,证明"时区 ID 是引用、规则会变、偏移不能存"(IANA releases、tzdb NEWS、Wikipedia tz list)。存量系统已经把未来事件预转换成 UTC 存起来的,迁移路径见 3.3(3)。
ISO 8601 vs RFC 3339:RFC 3339 是 ISO 8601 的互联网严格子集——每个 RFC 3339 时间戳都是合法 ISO 8601,反之不成立。差异要点:RFC 3339 小时仅 00–23(禁 24:00);强制完整日期+时间+偏移;-00:00 专表"偏移未知";空格替代 T 仅是显式扩展选项(RFC 3339)。实践含义:对外 API 声明用 RFC 3339,别声明"ISO 8601 兼容"。
闰秒:Unix 时间与所有主流库都假设"每天 86400 秒"、不处理 23:59:60——tzdb 虽含闰秒表但应用层无人使用。政策面:CGPM 2022 第 4 号决议决定 2035 年前废除闰秒;2026 年第 5 号决议跟进负闰秒风险(约 30%,见 BIPM 2022、BIPM 2026)。对课题的含义:应用层继续忽略闰秒是理性选择,但长周期 NTP/精确计时系统需另配方案。
2.6 架构策略层:把"怎么存时间"当替代方案对比
| 策略 | 存什么 | 正确场景 | 典型坑 |
|---|---|---|---|
| UTC 瞬时 | epoch 毫秒或 ISO Z 字符串 | 日志、审计、事件流、"已经发生的时刻" | 表达不了"未来本地 9:00 开会":时区规则一变(如 Edmonton 案例),按旧偏移预转换的 UTC 就是错的 |
| 墙钟 + IANA ID | 2026-11-06T09:00 + Asia/Shanghai | 日历、会议、班次、cron、任何"未来重复事件" | 必须在使用时才解析为瞬时;解析要处理缺口/重叠 |
| 固定偏移 | 2026-11-06T09:00+08:00 | 一次性快照、报表列 | 偏移≠时区:同偏移可能属多个时区;政策变更后历史失真 |
| Windows ID | China Standard Time | 仅限全栈 .NET/Windows | 跨平台即碎;.NET 6+ 可双向桥接但依赖 ICU(无 ICU 或 invariant 模式会失败,见 dotnet/runtime#62329、Jon Skeet) |
| "本地时间"裸存 | 无时区信息的时间戳 | —— | Python utcnow() 型事故的通用形态:值含义取决于读取方环境 |
结论:瞬时报 UTC、未来事件存墙钟 + IANA ID,两者互补而非互斥——这是 2026 年各生态文档的共识式答案。
2.7 陷阱速查表(两章合并去重)
| 陷阱 | 涉及语言 | 一句话处方 |
|---|---|---|
| naive/无时区时间戳 | 全部 | 新代码一律 aware/带偏移;Python 弃用 utcnow() 即为此(docs.python.org) |
| 绝对时长 ≠ 日历日 | 全部 | 明确区分两套 API(任务 D);测试夹具固定在 DST 边界日(2026-03-08 / 2026-11-01) |
| 缺口/重叠默认值不一致 | 全部 | 强一致场景显式选策略(reject/earlier/later),不依赖默认(场景→策略对照见 3.3(1)) |
== 比较时间 | Go(含单调时钟读数)、JS Date | 用 Equal/Before/After、.getTime() 比较 |
| 解析器子集差异 | 全部(自研解析器重灾区) | 对外声明 RFC 3339;测试加入 24:00、周日期、负偏移等对抗样本 |
| 时区数据缺失 | 容器/最小系统 | Go 内嵌 time/tzdata;Python 补 tzdata pip 包 |
| 旧 API 存量 | JS Date/Moment、pytz、Joda-Time、java.util.Date | 新项目禁止引入;存量按官方迁移表清偿(Moment status) |
localtime 静态缓冲区 | C | 非线程安全,多线程用 localtime_r;mktime 无法表达 fold |
SimpleDateFormat 线程不安全 | Java(旧) | 换 DateTimeFormatter(java.time,不可变) |
解析布局魔数 2006-01-02 | Go | 记忆点:用参考时间做布局;没有占位符体系 |
next month 在 31 号跳月 | PHP | 用 Carbon 的 addMonthNoOverflow 或显式处理月末 |
Time.parse 过于宽松 | Ruby | 输入不可信时禁用;Ruby 官方已建议 Time 优先于 DateTime |
DateFormatter 代价高且 locale 敏感 | Swift | 缓存 formatter 实例;算术一律过 Calendar |
| Excel 序列数/1900 闰年 bug | 导入导出链路 | 换 epoch 交换,别信表格软件的日期类型 |
三、交叉洞察
3.1 两轴结合说明了什么
洞察一:横轴的"共识模型"就是纵轴"错误清单"的反面——这不是品味差异,而是问题结构决定的唯一稳定解。
纵轴列出第一代 API 的四宗罪:可变对象、月份 0 起算、默认本地时区、把"偏移量"当"时区"(外加线程不安全的格式化器)。横轴的 2026 年截面显示,所有"第二代"API——java.time(2014)、Noda Time(2012)、C++20 chrono(Hinnant 设计)、Temporal(2026 Stage 4)、乃至 Rust 社区在酝酿的 jiff——全部收敛于同一组设计:时间点(Instant)/ 公民时间(LocalDateTime / PlainDateTime)/ 时区化时间(ZonedDateTime / DateTime\<Tz\>)三类分离、类型不可变、显式时区 ID、DST 歧义显式策略。四条轴线上独立的演化在同一终点汇合,说明这个模型是被问题结构(而不是任何一家委员会的偏好)逼出来的唯一正解。评价任何新日期 API,先对照第一代错误清单逐条打勾。
洞察二:"时区数据是活的"是纵横两轴共同的元规则,也是横轴选型的第一序参数。
纵轴的主线之一是 tzdb 自 1986 年起作为活的基础设施运营:黎巴嫩 2023 的"一国两时"、哈萨克斯坦 2024 的并区、欧盟至今悬置的废除案,都是"法律一变、全球发版"。横轴把这个观察变成了可操作的数字:截面最新 tzdb 为 2026f(2026 年已发 6 版),且 2026c(2026-07-08)写入 America/Edmonton 自 2026-11-01 起永久 UTC−6——就在报告截面前 4 天,任何写死偏移的系统当场失真。因此选型时,"数据更新机制"(JDK tzupdater、Python tzdata pip 包、Go time/tzdata 内嵌、Windows 月度更新、chrono-tz 编译期烧入)不是能力对比的附注,而是第一序参数;tzdb 版本应该进入发布清单与监控告警。
洞察三:DST 歧义"两派共存"是标准尚未收敛的地方,也是跨语言可移植性事故的固定源头。
2.2 与任务 E 的对照显示行业分裂为显式失败派与静默规范化派——同一个 DST 歧义输入跨语言结果不同(逐项行为只在任务 E 一处给出,此处不再重复)。纵轴解释了成因:这些默认值是各语言在不同年代独立演化的路径依赖(Java 2014 定规则化前移、Python 2016 定 fold、Go 2009 定"不保证"),从来没有一个跨语言标准强制统一。工程结论有二:其一,强一致场景(日程、计费、审计)必须显式选策略并写进团队规约,而不是依赖默认值(场景→策略对照见 3.3(1));其二,跨语言系统对同一条"本地时间"记录可能解析出不同瞬时,集成测试要用 DST 边界日做对抗样本。
洞察四:闰秒的废除是"工程痛感游说出政治决定"的闭环,应用层的简化假设因此在未来会越来越正确。
纵轴记录了完整的因果链:2012 年闰秒之夜 Linux 内核活锁瘫痪 Reddit/Mozilla/LinkedIn/Yelp → Google 首创 smearing 平滑抹秒 → Meta 2022 年发表论文、各大厂游说 → CGPM 2022 决议 2035 年前取消 ±1 秒上限 → CGPM 2026 决议跟进负闰秒风险(2035 年前概率约 30%)。横轴则确认了现状:Unix 时间与所有主流库都假设每天 86400 秒、不处理 23:59:60,tzdb 虽含闰秒表但应用层无人使用。两轴合起来看:应用层"假装闰秒不存在"长期以来是一种将错就错,2022–2035 年它将被制度追认为正确——这是罕见的"工程实践反向重塑制度层"案例;只有长周期 NTP/精确计时基础设施仍需自配 smearing 方案。
3.2 对用户的判断与建议
(1)三条铁律(从 1970 年 Unix 纪元到 2026 年 Temporal 都没有动摇)
- 已发生的时刻:存储一律 UTC(或 epoch 秒/毫秒,测量场景配单调时钟),展示才做本地化。
- 未来事件:存"墙钟时间 + IANA 时区 ID"两个字段,在使用时才解析为瞬时——时区规则会变(Edmonton 案例),提前预转换的 UTC 到时候就是错的。
- 区分三类时间概念:时间点(Instant,绝对瞬时)、公民时间(LocalDateTime,无时区的墙钟读数)、时长(Duration,两时点之间流逝的绝对时间量——它不是"起止区间 interval",区间类需求要用两个时间点表达)——选 API 时第一步看它是否把三者分开。
(2)按语言的选型矩阵(2026-11 截面,两章推荐合并)
| 语言 | 2026 推荐 | 关键配套 |
|---|---|---|
| JavaScript/TS | 新项目直接 Temporal(配官方 polyfill);维护期项目迁 Luxon/date-fns,Day.js 仅当需要 Moment 平替 | Safari 实装前 polyfill 兜底(生产化代价与迁移触发条件见 3.3(4)) |
| Python | zoneinfo + 显式 UTC 存储 + fold;禁 naive | 3.12 已弃 utcnow();dateutil 处理 relativedelta |
| Java/JVM | 全面 java.time;Android API<26 用 ThreeTenABP | 禁 java.util.Date/Calendar/SimpleDateFormat 新增 |
| .NET | 传输/存储用 DateTimeOffset(官方默认);规则引擎/日程系统上 Noda Time;TimeZoneConverter 做 ID 映射 | .NET 6+ Windows 接受 IANA ID 但依赖 ICU |
| Go | time.Time + 显式 time.Location;跨机器部署 import _ "time/tzdata" 内嵌 | 容器 distroless 场景必配 |
| Rust | chrono + chrono-tz 起步(事实标准);jiff(0.2.39,1.0 未发)作为观察项,等 1.0 再评估迁移;只要 offset 且体积敏感才考虑 time crate | —— |
| C++ | 工具链 ≥ MSVC 2019 16.10 / GCC 13 用 C++20 chrono;否则 Hinnant date 库 | 注意 Windows 10 1903+ 才内置 tzdata |
| PHP | DateTimeImmutable + Carbon | 禁可变 DateTime 与 next month 裸语法 |
| Ruby | Rails 项目用 ActiveSupport zone API;纯 Ruby 用 TZInfo 2.x | TZInfo 默认抛错,保守性最好 |
| Swift | Foundation + ISO8601DateFormatter;算术一律过 Calendar | 缓存 formatter |
| C | 64 位 time_t(-D_TIME_BITS=64)+ 系统 tzdb | 多线程用 localtime_r |
(3)发布清单与团队规约(把洞察二、三落成流程)
- tzdb 版本纳入发布清单;OS/JDK/容器的时区数据更新纳入运维日历(各运行时怎么查版本、怎么告警,见 3.3(2))。
- DST 边界日(如 2026-03-08、2026-11-01)固定为测试夹具;跨语言集成测试加入缺口/重叠对抗样本。
- 对外 API 声明 RFC 3339(不是"ISO 8601 兼容");解析测试覆盖
24:00、周日期、-00:00、负偏移。 - IANA ↔ Windows ID 映射(TimeZoneConverter / .NET 6 桥接)写进跨平台集成测试。
(4)未来变量观察清单
- 欧盟废除夏令时案:2019 年议会已通过、理事会搁置至今,2026-10 报道确认拨钟仍在继续——一旦落地将是 2007 年美国案之后最大规模的全球补丁事件(Euronews (2026-10-22))。
- CGPM 闰秒新框架:2035 前落地;负闰秒若提前出现(概率约 30%)会冲击未做 smearing 的系统。
- Temporal 生态迁移:Safari 实装时间点;Moment→四继任者迁移进度。
- jiff 1.0:Rust 生态可能出现的第二次标准迁移(chrono→jiff)。
- Y2038:32 位嵌入式长尾(约 2038-01-19 溢出),工具链已就绪、设备淘汰周期是主要风险面。
3.3 落地行动:把 3.2 的判断变成可执行步骤
(1)DST 歧义:场景 → 策略对照表
"显式选策略"落到场景上是三张牌。以 Temporal 的四选项(reject/earlier/later/compatible)为参照;不支持 reject 的运行时按任务 E 的检测能力自行实现拒绝语义——C# 的 IsInvalidTime/IsAmbiguousTime、Rust chrono 的 LocalResult::None/Ambiguous、Ruby TZInfo 的 PeriodNotFound/AmbiguousTime,或调用前用 get_info(C++)/fold 检测(Python)自查:
| 场景 | 推荐策略 | 依据 |
|---|---|---|
| 日程/会议(用户录入的未来墙钟事件) | 缺口与重叠都 reject | 歧义/缺口即录入错误,此时用户在场可立即纠正;静默改写会把"9:00 例会"挪到没人知道的小时 |
| 计费/账务(从墙钟回推瞬时计价) | 优先 reject;系统约束下必须选边时取 earlier,并把所选偏移落审计字段 | 计费对"歧义那一小时算谁的"敏感,earlier/later 都会多算或漏算一小时;取 earlier 与多数运行时的默认一致,至少不引入第二套意外 |
| 审计/合规(事后重建事件序列) | 不允许"从墙钟猜":源数据必须已带偏移或 UTC;确有墙钟存量时取 earlier 并把偏移写入审计备注 | 审计要求可重现;fold 信息一旦丢失不可恢复,任何选边都必须显式记录,绝不能静默 |
上表是本报告的建议而非标准答案;关键是把"每个场景一行策略"写进团队规约,而不是"遇到歧义再说"(对应洞察三的工程结论一)。
(2)tzdb 版本进发布清单:怎么查、怎么告警
| 运行时 | 版本查询 |
|---|---|
| Java | java.time.zone.ZoneRulesProvider.getVersions(zoneId) 返回按版本串(如 "2009e" 式)升序的规则历史,取最后一键即当前 tzdata 版本(Javadoc);tzdata 更新也可走 tzupdater 工具(见 2.3) |
| ICU(C/C++、Node 绑定、ICU4J) | ICU4C ucal_getTZDataVersion() / ICU4J TimeZone.getTZDataVersion(),返回 "2026f" 式版本串(ICU4C ucal.h、ICU4J TimeZone) |
| Python | pip 回退包用 importlib.metadata.version("tzdata");系统 tzdata 标准库不暴露版本,以发行版包管理器为准(rpm -q tzdata / dpkg -s tzdata) |
| Go | 标准库未提供版本查询:系统 zoneinfo 跟发行版包走;import _ "time/tzdata" 内嵌时数据随 Go 版本走(升 Go 才换数据),升级 Go 需纳入变更评估 |
| .NET / 浏览器 | 无版本 API(Windows 数据随 Windows Update,浏览器随 ICU/OS 更新),只能走规则探针 |
拿不到版本号的运行时用规则探针兜底:用一次最近的真实规则变更构造断言,探针失败即说明运行时 tzdata 落后。可直接复用本文的活案例——America/Edmonton 在 2026-11-01 后应为 UTC−6(2.5,tzdb 2026c)、Asia/Almaty 自 2024-03-01 起应为 UTC+5(1.5,tzdata 2024a)。告警做法三条:① CI 冒烟测试每月跑一次版本查询/探针,低于发布基线或探针失败即阻断发布;② 基础镜像固定 tag,CI 比对镜像重建前后 tzdata 版本差异,变了就要求显式确认;③ 探针与版本基线本身随 tzdb 发布(一年数版)滚动更新,防止基线腐化。
(3)存量系统"未来事件已预转换成 UTC"的迁移路径
2.5/2.6 指出的病灶(Edmonton 类规则变更让按旧偏移预转换的 UTC 失真)分四步给药方:
- 摸底:找出所有"把未来事件的 UTC 瞬时先算好存起来"的表/字段。特征:只有 UTC/epoch 列、无时区列,且业务时间晚于记录创建时间(日历、排班、预约类表是重灾区)。
- 补列:新增
wall_time+tz_id两列;用存入当时的 tzdb 规则把存量 UTC 反推回墙钟——不能用今天的规则反推,否则正是要消除的错误。反推落在 DST 缺口/重叠的记录打标,走 3.3(1) 的策略并人工复核。 - 双读校验:迁移期读取端改为"墙钟 + ID 使用时解析",与旧 UTC 列比对;差异记录为规则变更(Edmonton 类)所致并告警,全量一致后切读新列。
- 收尾:观察一个完整 DST 周期后下线旧 UTC 列(或仅留作审计),此后按 3.2 铁律 2 运行——未来事件在使用时才解析为瞬时。
已无法反推的场景(原始墙钟语义丢失、存入时的规则版本未知):数据损失不可逆,能做的是在事件表补 tz_rule_version(写入时的 tzdb 版本)字段,让下一次规则变更时可以仲裁"哪条记录按哪套规则解释"。
(4)Temporal 生产可用性:把 3.2(4) 的观察项变成触发条件
- polyfill 代价:官方
@js-temporal/polyfill用纯 JS 实现日历与时区算术,体积与运行时开销都不可忽略(具体数值随版本变化,引入前用自家 bundle 与目标机型实测),定位是过渡期方案而非长期依赖。 - Safari 缺口:截面日 Safari/JavaScriptCore 尚未实装(见 1.7),iOS 全家桶的 WebView 因此是最大盲区——polyfill 在苹果系上是必需品而非可选项。
- 迁移触发条件(替代无限期观望):新代码把日期算术收敛到自己的一层薄封装内,底层先走 polyfill 版 Temporal。3.2(4) 的两个观察项由此转成有出口的条件分支:Safari 实装 → 移除 polyfill(封装层不动);jiff 1.0 → 启动 chrono→jiff 迁移评估(Rust 侧同理)。观察项不再是无期限的等待,而是到达即执行的动作。
参考来源(两章实读 URL 合并去重)
制度与标准
- Wikipedia: Coordinated Universal Time | NIST: Leap Seconds | Wikipedia: Railway time | Wikipedia: International Meridian Conference
- Wikipedia: ISO 8601 | NIST FIPS PUB 4-1 (1991) | ISO TC154 草案
- RFC Editor: RFC 3339(正文另引全文版 rfc-editor.org/rfc/rfc3339)| RFC Editor: RFC 9557(IXDTF)
数据与基础设施
- Wikipedia: tz database | ICANN 博客 (2023) | OneZero: Paul Eggert 特稿 | IANA Theory
- IANA tzdb releases | tzdb NEWS | Wikipedia: List of tz database time zones
- LWN: A fork for the time-zone database? (2021) | Colebourne: Big problems at the timezone database (2021) | The Register (2021)
政治事件与闰秒政策
- 欧洲议会新闻稿 (2019) | 欧盟理事会专题页 | Euronews (2026-10-22)
- timeanddate: Kazakhstan 单一时区 | Reuters (2024-02-29) | Microsoft DST 博客
- The Register: 黎巴嫩 DST 混乱 (2023) | Reuters (2023-03-26)
- Wired: 2012 闰秒瘫痪始末 | Wikipedia: Leap second
- BIPM: CGPM 27th Resolution 4 (2022) | BIPM: CGPM 28th Resolution 5 (2026) | NYT (2022-11)
JavaScript 生态
- github.com/tc39/proposal-temporal(2026-11-05 实读,Stage 4 与引擎落地) | Temporal 官方文档 | Socket: TC39 advances Temporal to Stage 4 (2026-03) | Bloomberg: 9-Year Journey | Bryntum (2026-01)
- moment.js 项目状态(2020-09 维护模式) | Moment status 文档 | TuxCare: moment.js 遗留状态 | momentjs.com/docs
各语言运行时与官方文档
- Oracle: java.util.Date javadoc | JCP: JSR-310 | Oracle: Java SE 8 Date and Time | InfoQ (2014) | Colebourne: Joda→java.time (2014) | joda.org(官方劝迁) | Joda FAQ: Illegal instant | java.time ZonedDateTime javadoc | java.time.zone.ZoneRulesProvider javadoc
- Python zoneinfo 文档 | PEP 615 | PEP 495 fold(backports.zoneinfo) | LWN: Python time-zone handling (2020) | datetime.utcnow() 弃用说明(3.12)
- Microsoft Learn: Compare date and time types | TimeZoneInfo.FindSystemTimeZoneById | InfoQ: Noda Time 1.0 (2012) | Noda Time 博客 (2011) | Jon Skeet: TimeZoneInfo on .NET 6 | dotnet/runtime#62329
- Go 1.9 Release Notes | Russ Cox: Monotonic 提案 | pkg.go.dev/time | pkg.go.dev/time/tzdata
- docs.rs/chrono | chrono#1423 | chrono#1768 soft-deprecating | lib.rs/chrono-tz | crates.io API: jiff(实测 max_version=0.2.39)
- cppreference: std::chrono | MSVC: zoned_time 类 | GCC libstdc++ commit(C++20 时区补齐) | cppstories: zones_around_world_chrono
- tzdb 版本查询(3.3(2)):ICU4C: ucal_getTZDataVersion | ICU4J: TimeZone.getTZDataVersion
(报告完 · 横纵综合 · 2026-11-05)