程式中的時間議題

簡介

如果要做一個國際化的應用程式,就需要對應各個國家/文化的時間格式。

議題

  1. 時間的表示法
    1. 格式、排序
    2. 年份
      1. 紀元
      2. 年號
    3. 月份
    4. 星期
      1. 每個星期的開始日
    5. 小時
    6. 時區
    7. 曆法
    8. 一段時間
  2. 時區
    1. 夏令時間
    2. 閏年、閏月、閏秒
  3. 實作
    1. 資料儲存
    2. 精度
    3. 時間同步
    4. 生日
    5. 時間段和時間點

時間表示法

參照ISO8601標準,下列以⭐代表標準表示法。

格式

年由4位數字組成YYYY,月、日用兩位數字表示:MM、DD。
只使用數字為基本格式。使用短橫線"-"間隔開年、月、日為擴展格式。

  1. 日曆日期表示法
  • 2022-05-21 (⭐)
  • 20220521 (⭐)
  • 2022/05/21
  • 2022,05,21
  • 2022.05.21
  1. 順序日期表示法:將一年內的天的序數用3位數字表示
  • 2022-141 (⭐)
  • 2022141 (⭐)
  1. 星期日曆表示法:用2位數表示年內第幾個日曆星期,再加上一位數表示日曆星期內第幾天,但日曆星期前要加上一個大寫字母W。每個日曆星期從星期一開始,星期日為第7天
    • 2022-W20-6 (⭐)
    • 2022W206 (⭐)
    • 2022-W01-1是從2022年1月3日開始的,前幾天屬於上年的第52個日曆星期

年月日排序

  • 年 月 日 (⭐)
  • 月 日 年
  • 日 月 年

年份

紀元

  1. 西元/公元
    • BC(Before Christ)/AD(Anno Domini)
    • BCE(Before Common Era)/CE(Common Era)
    • 正負號表示 (⭐)
      • 公元1年為0001年,公元前1年為0000年,公元前2年為-0001年
  2. 年號
    • 日本、(中華)民國
    • 年號會增加大量轉換成本

❓某民國什麼時候要廢年號

月份

  • 01, 02, 03, ... (⭐)
  • January, February, March...
  • Jan, Feb, Mar, ...
  • 1, 2, 3, ...

星期

通常會用當地語言表示

  • 1, 2, 3, 4, 5, 6, 7 (⭐)
  • Sun, Mon, Tue, ...
  • 日、月、火、水、木、金、土 (日本)
  • 一、二、三、四、五、六、日

星期的開始日:會影響日曆的顯示

  1. 星期日(美國、加拿大、日本、以色列、港澳、多數拉丁美洲國家)
  2. 星期一(歐洲絕大多數國家、中國,⭐)
  3. 星期六(埃及等部分中東、北非國家)

小時

  • 24小時制 (⭐)
  • 12小時制
  • 12:00是pm還是am
    • 我很久以前就有的疑惑
    • 一般來說,12:00pm是中午,12:00am是午夜

時區

  • 如果時間在零時區,那麼(不加空格地)在時間最後加一個大寫字母Z
  • 其他時區用實際時間加時差表示
    • 22:30:05+08:00 (⭐)
    • 223005+0800 (⭐)
    • 223005+08

日期+時間

在時間前面加一大寫字母T

  1. 2004-05-03T17:30:08+08:00 (⭐)
  2. 20040503T173008+08 (⭐)

曆法

公曆(格勒哥里曆)之外,目前廣泛使用的曆法:

  • 希伯來曆(猶太曆)
  • 伊斯蘭曆(回回曆)
  • 農曆
    • 陰陽合曆:其日期採朔望月以指示月球的相位,年則與太陽相關

一段時間

時間段:前面加一大寫字母P

  • P1Y3M5DT6H7M30S(一年三個月五天六小時七分三十秒內) (⭐)
  • 19850412/19860101(起始/結束) (⭐)
  • 19850412/P6M(起始/一段時間) (⭐)
  • P6M/19860101(一段時間/結束) (⭐)

循環時間:前面加上一大寫字母R

  • R{循環次數}/{開始時間}/{時間間隔}/{結束時間}
    • R3/20040506T130000+08/P0Y6M5DT3H0M0S (⭐)
    • R/P1Y2M/20250101(無限次循環) (⭐)

時區

  • 時區和經度沒有絕對關係
    • 夏令時間
    • 換日線不是直線
    • UTC+8的地區有台北、北京、新加坡、印尼等,但新加坡實際的地理位置在UTC+7
    • 中國全境使用相同時區(UTC+8)

所以在選擇時區時,實際上是在選擇國家或地區

夏令時間(Daylight Saving Time)

以實作上來看,這是最噁心的規則

  1. 有些地區在接近春季開始的時候,會將時間調快一小時,並在秋季調回來
  2. 這代表春季的某一天會少一個小時,秋季的某一天會多一個小時
    1. 如2018年的夏令時間就直接跳過了2018年3月25日3:00:00到3:59:59

以上述夏令時間為例,假設用戶設定3:30要執行備份任務,有以下實作方法

  1. 檢查經過某個時間點
    • if (nowTime == 3:30) { runTask(); }
    • 任務不會執行
    • 如果這是一年執行一次的任務,問題就大了
  2. 檢查超過某個時間點
    • if (nowTime > 3:30 and !isRunTask) { isRunTask=true; runTask(); }
    • 這種情況,任務會在3:00過後馬上執行(也就是顯示為4:00的時候)
    • 和用戶想像中不同,並不是3:00再經過30分鐘的時候執行
      • 如果這個任務有前提任務,而且在3:10分執行,此時會同時觸發執行
        • 可能會因為執行順序不同而出錯
  • 同理,離開夏令時間時,同一個任務可能會被執行兩次
  • 用戶如果設定「12小時後」執行任務,也會發生類似的問題
    • 用倒數的方法:實際的12小時後
    • 用時鐘時間:時鐘上的12小時後

注意這裡出問題的不是TimeStamp本身,而是把一個不存在的當地時刻轉成TimeStamp:
3/25 03:00~03:59 在 Athens 被跳過,套件只能自行挑一個解釋,於是 03:00 被當成 +03:00(等於 00:00 UTC),
反而比前一筆 02:30+02:00(00:30 UTC) 還早,時間軸就倒退了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import arrow
from datetime import datetime

timezone = "Europe/Athens" # 有夏令時間的時區

now = arrow.get(datetime(2018, 3, 25, 2, 30, 0), timezone) # 夏令時間前
print(now)
print(now.int_timestamp)
now = arrow.get(datetime(2018, 3, 25, 3, 0, 0), timezone) # 進入夏令時間(實質UTC改變)
print(now)
print(now.int_timestamp)
now = arrow.get(datetime(2018, 3, 25, 3, 30, 0), timezone) # 夏令時間後
print(now)
print(now.int_timestamp)
1
2
3
4
5
6
2018-03-25T02:30:00+02:00
1521937800
2018-03-25T03:00:00+03:00
1521936000
2018-03-25T03:30:00+03:00
1521937800

所以如果要處理上述問題,我目前想到的方法只有在每一次檢查時間時,額外檢查是否進入/離開夏令時間

閏年、閏月、閏秒

會造成和夏令時間類似的問題

  • 閏秒
    • 消彌精確的時間(使用原子鐘測量)和不精確的觀測太陽時(UT1)之間的差異
    • 因為地球的旋轉速度會隨著氣候和地質事件變化,因此UTC的閏秒間隔不規則且不可預知
    • Unix實作:重複23:59:59一次,或新增23:59:60
    • 🆕 2022年11月,在第27屆國際計量大會決定於2035年取消閏秒
  • 閏日
    • 彌補因人為曆法規定的年度天數365日和平均回歸年的大約365.24219日的差距
    • 雖然只多一天,不過普遍被叫做閏年,即閏日出現的年份
    • 公曆(典型程式題)
      • 公元年分非4的倍數,為平年
      • 公元年分為4的倍數但非100的倍數,為閏年
      • 公元年分為100的倍數但非400的倍數,為平年
      • 公元年分為400的倍數為閏年
  • 閏月
    • 農曆
      • 平年比一回歸年少約11天
      • 3年一閏,5年二閏,19年七閏
      • 閏月加到哪個月,以農曆曆法規則推斷,主要依照與農曆的二十四節氣相符合來確定
      • 基本上19年為一周期對應於公曆同一時間,但亦有部分例外
      • 每年天數差距過大,不利以年計算的會計 (梁啟超《改用太陽曆法議》)
  • 閏年
    • 真正的閏年,即於原本的曆法中插入一年
    • 古埃及民用曆固定365日、不置閏,每1460個回歸年會比實際多算出一整年(Sothic 週期)

實作

資料儲存

上下限

  • 2038年問題
    • 因為儲存時間的格式為int32,只能儲存從1970-01-01到2038-01-19T03:14:07Z的時間
    • 受影響的資料
      • 使用POSIX時間表示時間的程式
      • 使用int儲存時間的程式語言,如C語言
      • 使用int儲存時間的檔案格式
        • 註:.zip 原生用的是 MS-DOS 日期格式(有效範圍 1980~2107),不算此類;只有其 Unix extended timestamp 欄位才有 2038 問題

精度

  • 一個system tick會更新一次時間
    • 10MHz的情況下,可得100ns的精度
    • 預設的tick rate沒有這麼高
  • Linux: tickless kernel
    • 沒有詳細研究,看起來是自訂tick rate的工具

時間同步

NTP(Network Time Protocol)

  • 校正本地的UTC時間
    • 定期輪詢不同網路上的三個或更多伺服器,計算其時間偏移量和來回通訊延遲

Bug

  1. 千禧蟲問題

    • 工程師的技術債
    • 自訂年號的國家會更容易遇到,如民國百年蟲問題、日本更換年號問題
  2. 用戶手動(或惡意)調整時間/時區

    • 舊資料的修改時間可能會大於新資料的修改時間
    • 解法
      • 不要相信任何本機檔案的時間
        • 在資料庫中記錄時間
  3. 跨機器比較時間:在機器A連線機器B(如ftp),並顯示機器B的檔案,要顯示誰的時間?🤔

    • 如果顯示機器B的時間,難以判斷何者較新
    • FTP 的 LIST 回的是 server 的本地時間,而且格式裡不帶時區
      • 半年內的檔案顯示 MMM DD HH:MM,超過半年只顯示 MMM DD YYYY(連時分都沒了)
      • 也就是說 client 只能猜,猜錯就差好幾小時,甚至差一整年
      • MLSD (RFC 3659) 才提供 modify=YYYYMMDDHHMMSS 並明定為 UTC,這才是正解
    • rsync 用 mtime + size 判斷要不要傳
      • --modify-window=N 這個選項會存在,就是因為現實中兩端的 mtime 一定會差一點
        • FAT 的 mtime 精度只有 2 秒
        • 掛 SMB/NFS 時可能有時區偏移
      • 差一秒就重傳整個檔案的話成本太高,所以這個容忍窗是必要的
    • 結論:要比新舊就一律轉成 epoch/UTC 再比,顯示時才轉回觀看者的本地時間
      • 不要拿 A 的本地時間直接和 B 的本地時間比大小

生日

雖然上述的時間大多需要考慮時區,但是生日似乎是個例外

不管人在何處,都是在當地的日期過生日

時間段和時間點

通常用Instant代表時間點,Duration代表時間段

套件

  • Python
    • 標準Library(datetime, time, zoneinfo, calendar, dateutil)
    • arrow
      • 光陰似箭?
  • Javascript
    • Moment.js
    • Day.js
  • Java
    • java.time
  • C#
    • Noda Time
  • C++
    • <chrono>

總結

時間相關的問題能夠有效的產生一堆bug。

建議在儲存資料時使用UTC+0時間或TimeStamp,顯示時才切換,以避免許多可能的bug,但是會付出額外的轉換成本。

實際上,使用者也沒在管這些標準;預設格式如果不符合使用者的習慣格式就會被嫌棄。

當然,貨幣、度量衡、標點符號等習慣用法也有和時間一樣的問題,這些被統稱為本地化(Localization)問題。

參考資料

  • Wiki:各國家時間表示法
  • Wiki:農曆
  • Wiki:置閏
  • Wiki:夏令時間
  • 三分鐘科普夏令時,並用Javascript判斷當前時間是否屬於夏令時
  • Wiki:2038年問題
  • How precise is the internal clock of a modern PC?
  • Precision and accuracy of DateTime
  • Wiki: 網路時間協定