YourWorldTime

ISO 8601,以及怎样把日期写得毫无歧义

· 约 1 分钟

ISO 8601 之所以存在,是因为 03/04/2026 在美国是三月、在别处是四月。写成 2026-04-03 就消除了歧义,作为文本也能正确排序,而这正是每个接口都该接受的写法。

03/04/2026 在世界大部分地方是 4 月 3 日,在美国是 3 月 4 日。仅凭这串字符没法判断本意是哪一个,而两种读法都完全合情合理。每年,这种歧义都会造出错过的截止期限、重复的预订,以及至少一次让人记得住的故障。

ISO 8601 就是消除它的国际标准。

核心格式

2026-04-03T14:30:00Z

从左往右读,从最大单位到最小单位:

  • 2026-04-03 —— 年、月、日,各自补零,且永远是这个顺序
  • T —— 日期与时间之间的分隔符,严格形式下必需
  • 14:30:00 —— 24 小时制的时、分、秒
  • Z —— 时区标示。Z 恰好表示 UTC

重点就在顺序上。因为各部分从最高位排到最低位,一条 ISO 8601 字符串按纯文本就能正确排序。在简单的字符串比较里,2026-04-03 排在 2026-04-10 之前,这是别的常见格式都做不到的。单凭这一条性质,这个标准就立得住。

时区标示

三种形式都合法,含义各不相同:

形式 含义
2026-04-03T14:30:00Z 14:30 UTC。没有歧义
2026-04-03T14:30:00+05:45 某个比 UTC 快 5 小时 45 分的时区里的当地 14:30。没有歧义
2026-04-03T14:30:00 某个未指明时区里的 14:30。这不是时间戳

惹麻烦的是第三种。它是一个_本地日期时间_——一次没有办法放到时间轴上的钟面读数。把它送过系统边界,指望另一头假定的时区跟你一样,这是一个正等着某次异地部署来引爆的缺陷。

也请注意偏移量不是什么:+05:45 告诉你的是尼泊尔的偏移量,而不是这个时间戳来自尼泊尔。偏移量不是时区——同一个偏移量被好几个时区共用,而一个时区的偏移量还会变。如果你需要知道“在哪里”,就把标识符一并带上。

RFC 3339,你多半真正想要的那个子集

ISO 8601 很大。它允许周日期(2026-W14-5)、序数日期(2026-093)、带小数的单位,还有一种省略世纪的截断形式。这里头大部分都不是你在接口里想要的。

RFC 3339 是为互联网协议收紧过的子集。它要求完整日期,要求时区标示,并允许用 T 或一个空格作分隔符。当有人在谈 API 时说“ISO 格式”,他几乎总是指 RFC 3339。

一条有用的准则: 凡是好的解析器能吃下的都接受;输出时只输出带 Z 的严格 RFC 3339。

时长与区间

用得较少,遇上时值得认得。

时长以表示「period」的 P 开头,T 把日期部分和时间部分分开:

  • P3Y6M4D —— 三年六个月四天
  • PT1H30M —— 一小时三十分钟
  • P1DT12H —— 一天十二小时

T 不是可选的:P1M 是一个月,PT1M 是一分钟。这就是 ISO 8601 时长里的经典缺陷。

区间用斜杠连接两个时刻:2026-04-03T14:00Z/2026-04-03T16:00Z;也可以是一个时刻加一段时长:2026-04-03T14:00Z/PT2H

本站的 HowTo 结构化数据用的正是时长语法——两分钟的任务写作 PT2M——因为 schema.org 规定使用 ISO 8601 时长。

周日期,以及其中的陷阱

2026-W14-5 是 2026 年第 14 个 ISO 周的星期五。ISO 周从星期一开始,第 1 周是包含一月第一个星期四的那一周。

人们栽跟头的后果是:ISO 周年并不总等于日历年。 2027 年 1 月 1 日落在 2026 年的第 53 个 ISO 周里。把 ISO 周号跟日历年配在一起的格式化代码,每年都会有几天给出错误的日期——而在多数语言里,格式说明符几乎长得一模一样(YYYYGGGG,或 %Y%G)。

实用规则

  1. 按 UTC 存,带 Z 输出。 2026-04-03T14:30:00Z 在任何地方都只有一个意思。
  2. 绝不要把裸的本地日期时间送过边界。 如果你没法附上时区,你手里就没有时间戳。
  3. 所有位都补零。 2026-4-3 不是合法的 ISO 8601,尽管大多数解析器会接受。
  4. 使用 -: 分隔符。 紧凑形式(20260403T143000Z)合法但不友好,有些解析器会拒绝。
  5. 把时区标识符单独保存,用于你需要知道_在哪里_、而不只是_偏移多少_的时候。Europe/London 熬得过规则变更,+01:00 熬不过。

解析中的坑

即便有标准,出岔子的地方也在解析。

JavaScript 的 Date 构造函数对非 ISO 输入并不一致。 new Date('2026-04-03') 被当作 UTC 午夜解析。new Date('2026-04-03T00:00:00')——没有时区标示——被当作_本地_午夜解析。两者相差正是你的偏移量,也就是说同一段代码会因运行地点不同而给出不同的日期。永远带上时区标示。

两位数年份有歧义,应当拒绝。 ISO 8601 允许一种截断形式;几乎没有什么东西应该接受它。26-04-03 可能是 1926 也可能是 2026,而决定这件事的滑动窗口,是一条藏在解析器里的策略。

秒的小数位精度不一。 14:30:00.514:30:00.50014:30:00.500000 都合法,而且都是同一个时刻。一个比较字符串而不是比较时刻的解析器可不这么认为。

逗号是合法的。 ISO 8601 允许 14:30:00,5——十进制逗号——而且更推荐它。多数解析器会拒绝。如果你在接收来自欧洲系统的数据,请有心理准备。

午夜有两种写法。 某一天的 24:00:00 和第二天的 00:00:00 是同一个时刻,两者都是合法的 ISO 8601。输出用后一种;接收时两种都收。

关于排序的一点说明

ISO 8601 的可排序性,只在时区表示相同的字符串之间成立。2026-04-03T14:00:00Z2026-04-03T10:00:00-05:00 是同一个时刻,但作为字符串,后者排在前面。

如果你把时间戳当文本排序——在日志文件里、在文件名里、在类型为文本的数据库列里——请先把它们统统归一成带 Z 的 UTC。否则那个顺序反映的是格式的性质,而不是时间的性质。