务实的答案:输出一个 RFC 3339 形状的字符串
当某个接口说它要一个“ISO 8601 时间戳”时,最稳妥的输出通常长这样:
2024-03-15T14:30:00.123Z
它按从大到小的顺序排列各字段,数字定宽,带秒,也写明了与 UTC 的关系。这个组合人能读懂,在恰当条件下可以直接排序,而且开发者最可能碰到的那些日期时间解析器都认它。
ISO 8601 本身比这一种写法大得多。它支持日历日期、时间、周日期、序数日期、时长、区间和重复区间。RFC 3339 则把这些选择收窄成一种可预期的互联网时间戳。当“合法的 ISO”和“这个 API 认的”不是同一个集合时,这个区别就要命了。
从左往右读懂这个常见时间戳
把 2024-03-15T14:30:00.123Z 拆成这几段:
| 组成部分 | 值 | 含义 |
|---|---|---|
| 年 | 2024 |
四位日历年 |
| 月 | 03 |
三月,补零 |
| 日 | 15 |
当月第几天,补零 |
| 分隔符 | T |
分隔日期和时间 |
| 时间 | 14:30:00 |
24 小时制的本地钟面读数 |
| 小数 | .123 |
可选的小数秒 |
| 偏移 | Z |
UTC,等价于 +00:00 |
那些扩展格式里的标点不是装饰。连字符、冒号、T 和偏移量让约定一目了然,也防止各地的书写习惯改变解释方式。
下面这几个字符串表示的是同一个瞬间:
2024-03-15T14:30:00Z
2024-03-15T09:30:00-05:00
2024-03-15T15:30:00+01:00
如果要把时间戳当纯文本来比较,请先把它们归一到相同的偏移和相同的小数精度。一堆混着 Z、正偏移和不同小数位数的值,不会仅仅因为每一个都长得像 ISO 8601,就自动按时间顺序排好。
偏移量确定的是瞬间,不是时区
-05:00 说的是:此刻显示的钟面比 UTC 慢五小时。它不说明为什么、这块钟在哪儿,也不说下个月这个偏移会不会变。
而 America/New_York 这样的 IANA 时区是一套规则集,里面装着历史上和已排定的偏移切换,包括夏令时变更。这个区别决定了你到底该存什么:
| 需求 | 该存什么 |
|---|---|
| 记录事件发生的时刻 | 瞬间,通常是 UTC 或带偏移的时间戳 |
| 在用户所在时区展示事件 | 瞬间 + 用户的 IANA 时区 |
| 夏令时变了之后,会议仍在当地上午 9 点 | 本地日期时间 + IANA 时区 + 一条消歧策略 |
| 保留最初收到的那个偏移 | 原始字符串,或一个专门的偏移字段 |
EST、CST、IST 这类时区缩写不适合做交换值:它们有歧义,也不携带切换历史。
ISO 8601 和 RFC 3339 解决的问题不是一个量级
RFC 3339 刻意砍掉了 ISO 8601 的许多可选形式,好让互联网解析器少写点分支。
| 特性 | ISO 8601 家族 | RFC 3339 子集 |
|---|---|---|
| 扩展格式日历日期 | 2024-03-15 |
必须用这种 |
| 基本格式 | 20240315T143000Z |
不允许 |
| 与 UTC 的关系 | 某些表示法里可以省略 | 必须有偏移或 Z |
| 周日期与序数日期 | 支持 | 不支持 |
| 小数秒 | 支持 | 秒之后可跟 |
| 日期时间分隔符 | ISO 里有多种语境 | 文法用 T |
| 主要用途 | 广义的信息交换 | 互联网协议时间戳 |
该 RFC 的基础文法允许小写的 t 和 z,但建议输出大写。它也注明其他规范可以用空格代替 T——可这条注记并不等于承诺任意一个 RFC 3339 解析器都会接受空格。老老实实生成保守的那种形式。
RFC 9557 是在偏移量之外追加时区信息,而不是取代它
RFC 9557 定义了互联网扩展日期时间格式(IXDTF)。它最显眼的特性是方括号时区后缀:
2024-03-15T14:30:00+01:00[Europe/Paris]
这个时间戳依然是靠那个数值偏移锚定到某个瞬间的;方括号里的名字提供的是“巴黎的一天之后”这类运算所需要的规则。当时区规则变更,或者生产方用了过期数据时,这两者是可能互相矛盾的。所以解析器需要一条处理不一致的策略,而不是默默相信自己先读到的那个字段。
RFC 9557 还支持一个关键性标记:
2024-03-15T14:30:00+01:00[!Europe/Paris]
! 告诉接收方:必须理解这条标注。如果标注被标记为关键,就不要随手把方括号里的数据剥掉。
ISO 8601 还能表示日期、时长和区间
不是每个 ISO 8601 值都表示一个瞬间:
| 类型 | 示例 | 含义 |
|---|---|---|
| 日历日期 | 2024-03-15 |
只有日期,不含时间和时区 |
| 序数日期 | 2024-075 |
2024 年的第 75 天 |
| 周日期 | 2024-W11-5 |
ISO 第 11 周的星期五 |
| 时长 | P3DT4H30M |
三天四小时三十分 |
| 区间 | 2024-03-01/2024-04-01 |
起点和终点 |
| 起点加时长 | 2024-03-01/P1M |
从起点算一个日历月 |
| 重复区间 | R3/2024-03-01/P1D |
三个每日区间 |
日历意义上的时长,并不总能换算成固定的秒数。P1M 取决于起始日期;而某个时区里的 P1D 在跨越夏令时切换时,实际可能是 23 小时或 25 小时。所以“按瞬间算的时长”和“按日历算的时长”,你得有意识地选一个。
在代码里输出和解析这个窄格式
JavaScript 输出的是毫秒精度的 UTC 字符串:
const nowIso = new Date().toISOString();
const input = "2024-03-15T14:30:00Z";
const parsed = new Date(input);
if (Number.isNaN(parsed.getTime())) {
throw new RangeError("Invalid timestamp");
}
console.log(parsed.toISOString());
// "2024-03-15T14:30:00.000Z"
Python 可以保留明确的偏移,再归一到 UTC:
from datetime import datetime, timezone
value = datetime.fromisoformat("2024-03-15T09:30:00-05:00")
utc_value = value.astimezone(timezone.utc)
print(utc_value.isoformat().replace("+00:00", "Z"))
# 2024-03-15T14:30:00Z
库的名字里带着“ISO”,不代表它覆盖了同样的文法范围。请拿你实际要对接的那个边界,去逐一试这些格式:
- 扩展年份
- 闰秒写法
- 降低精度的形式
- 方括号标注
- 超过毫秒的小数精度
常见的互操作翻车现场
- 值本该标识一个瞬间,却漏掉了
Z或偏移量 - 接受了一个本地时间,然后悄悄按服务器时区去解释它
- 想当然地认为
+00:00、Z和 RFC 9557 更新后的-00:00语义在任何约定下都可以互换 - 把一个偏移量当成了永久的 IANA 时区
- 输出
+0000,而接收方要求的是 RFC 3339 那种带冒号的+00:00 - 拿偏移不同、小数位数也不同的字符串做比较,好像字典序就等于时间序
- 截断小数秒,却没写明到底是四舍五入还是直接截断
- 明明只接受 RFC 3339 时间戳,却对外宣称“全面支持 ISO 8601”
真正有用的 API 约定不是一句“发 ISO 过来”,而更接近于:“请发 RFC 3339 时间戳,大写的 T 和 Z,只用 UTC,小数最多三位。”越具体的约定越好测,也越不容易被误解。
官方参考与延伸阅读
Frequent questions:
- Q: 「2024-03-15 14:30:00Z」是合法的 RFC 3339 吗?
- A: RFC 3339 的文法在日期和时间之间用的是 T。规范里有一条注记允许下游规范放行空格之类的其他分隔符,但这种支持并不可移植。除非接收方的约定明确说了可以,否则请输出 2024-03-15T14:30:00Z。
- Q: 结尾的 Z 是什么意思?
- A: Z 表示 UTC 偏移为 +00:00。因为通话字母表里字母 Z 读作 Zulu,所以它也常被念成「Zulu」。
- Q: JavaScript 里怎么解析 ISO 风格的时间戳?
- A: new Date("2024-03-15T14:30:00Z") 能解析常见的 RFC 3339 风格写法。记得检查是否得到了 Invalid Date,也别想当然地以为 Date.parse() 能吃下 ISO 8601 的每一种变体。
- Q: ISO 8601 和 RFC 3339 是一回事吗?
- A: 不是。RFC 3339 定义的是一个刻意做小的互联网子集。ISO 8601 还涵盖基本格式日期、周日期、序数日期、时长、区间等等,这些 RFC 3339 都不接受。
- Q: RFC 9557 是什么?
- A: RFC 9557 定义了「互联网扩展日期时间格式」,它可以在方括号里追加信息,比如 IANA 时区:2024-03-15T14:30:00+01:00[Europe/Paris]。偏移量负责确定瞬间,方括号里的时区名则提供本地时间运算所需的规则。
- Q: T 这个分隔符是必须的吗?
- A: 要输出可互操作的机器格式,就用 T。RFC 3339 的文法要求它,尽管该 RFC 也注明其他规范可以为了可读性允许空格。ISO 8601 另有基本格式和约定格式,但 API 很少全都支持。
- Q: ISO 8601-1 和 ISO 8601-2 有什么区别?
- A: ISO 8601-1 定义日历日期和 24 小时制时间的基本规则。ISO 8601-2 追加了扩展能力,比如不确定或近似日期、扩展区间、日期集合、重复规则和日期时间算术。