前言
我一直對程式中日期存在個模糊不可靠的概念,通常依靠方便的庫像是: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