前言
我一直对程序中日期存在个模糊不可靠的概念,通常依靠方便的库像是:dayjs 去处理日期转换,但底层的原理至今仍不是很清楚,只知道时间是相对的,且存储方式可能是某种标准格式,这篇文章将尽可能补全程序处理时间所需要的知识。
时间有很多陷阱
仔细想想时间是一个看起来简单实际复杂的东西:
- 时间是有上下文为前提的(时区、各种地区的奇怪时间规则)
- 存储时间有不同的格式(各种时间戳记)
- 显示时间有不同的格式习惯
时区的概念
地球自转一圈约 24 小时,但不同经度「正中午」的时间不同,为了避免时出现「在半夜 3 点吃午餐」的奇怪描述,时区的目的是让「太阳在天空最高点大约等于中午 12 点」这件事在每个地区都大致成立。
不同地区会为了方便而统一时区,像是「把整个国家都划分在同个时区」、「对齐某个政治或经济圈」即便可能相差数个小时。表示上会依照某个标准加减小时来代表时区:UTC+8、GMT-2。
夏令时间(Daylight Saving Time, DST)
暂时性的时区改变,夏天把时间拨快 1 小时。夏季昼长夜短,借由调快时间显示多利用自然的光照资源早睡早起。
电脑怎么运算时间?
大多系统使用 Unix Timestamp 沿用至今:
- UTC Coordinated Universal Time 世界协调时间:以原子钟计时,无关时区的时间标准
- Unix Timestamp:自 1970-01-01 00:00:00 UTC 起经过的秒数。毫秒、微秒与纳秒都是各语言为了实务需求所做的延伸。
Date.now() // 1765893674273time.Now().Unix() // 秒time.Now().UnixMilli() // 毫秒time.Now().UnixMicro() // 微秒time.Now().UnixNano() // 纳秒如何存储时间?
总结以上时间应被分为「时刻」和「时区」两种概念,存储时间依照使用性质选择不同的方式:「不该随时区改变意义的时间」和「会随时区改变意义的时间」。
- 不随时区改变意义:发文时间
- 想像如果 DB Timezone 设置、时区规则改动会连带改变发文时间是很奇怪的
- 随时区改变意义:会议时间
- 想像公司约定“每周一早上 9 点开会”,如果我们一开始就把它存成某个 UTC 时刻,当夏令时间切换时,会议就会自动变成 10 点或 8 点,但约定从来没有改变。
对已经发生的事件 UTC 是事实,对尚未发生的排程,Local Time 才是事实。以下是数据库常见的日期存储对照:
| DB | 类型 | 是否存 UTC | 是否自动转时区 |
|---|---|---|---|
| MySQL | TIMESTAMP | ✅ | ✅ |
| MySQL | DATETIME | ❌ | ❌ |
| PostgreSQL | TIMESTAMPTZ | ✅ | ✅ |
| PostgreSQL | TIMESTAMP | ❌ | ❌ |
| MongoDB | Date | ✅ | ❌ |
ISO 8601 时间格式
YYYY-MM-DDTHH:mm:ssZ: UTC- +08:00:时区偏移
为什么 UTC + Offset 还不够?什么是 IANA?
ISO 8601 解决“怎么写时间”, IANA (Internet Assigned Numbers Authority) 解决“这个时间在那个地方到底几点”。
因为 ISO 8601 只能表示“当下与 UTC 的差距”,却无法描述:
- +08:00 这个地方是不是有夏令时间?
- 2026 年会不会改规则?
- 历史上某一年是不是曾经用过别的 offset?
因此在排程或未来事件中,应该加上 IANA 标示:
2025-12-17T09:23:45+08:00[Asia/Taipei]延伸阅读
- The Problem with Time & Timezones - Computerphile
- Time… a programmer’s worst enemy // The Code Report - Fireship