週日正午那封跨洲會議邀請
2026-03-08T12:00:00.000Z,UTC 的週日中午。人在臺北的我,給雪梨、紐約、倫敦各發一條「下午三點上線」,核對完還是把邀請撤了:同一頁上擺著兩個數,一個來自牆上的時鐘,一個來自表裡寫死的小時數。
一、13 座城是寫死在常數裡的
城市清單是模板第 32-46 行的常數,13 條,用得到的只有名稱、一個偏移小時數、一個 tz,英文名稱不渲染。列表頁文案寫「等 20+ 主要城市」,實測陣列長度 13;本機 Intl 列舉出 418 個時區,表裡只佔 13 個,臺北、首爾、多倫多、奧克蘭都不在。
| 城市 | 寫死偏移 | 2026 全年不符 |
|---|---|---|
| 北京 / 東京 / 杜拜 / 莫斯科 / 夏威夷 | +8 / +9 / +4 / +3 / -10 | 0 個樣點 |
| 雪梨 | +10 | 8737 個樣點 = 182.02 天 |
| 倫敦 / 巴黎 / 柏林 | 0 / +1 / +1 | 各 10080 個 = 210 天 |
| 紐約 / 芝加哥 / 丹佛 / 洛杉磯 | -5 / -6 / -7 / -8 | 各 11422 個 = 237.958 天 |
每城按 30 分鐘一步取 17473 個樣點,13 城 227149 個,84665 個不符,佔 37.3%。
二、偏移是誰給的:一條正則,和一個寫死的小時數
標籤有兩條來源。主路徑在第 92-112 行:向 Intl 要 shortOffset 那串文字,餵給第 101 行的正則 /GMT([+-])(\d{1,2})(?::(\d{2}))?/,命中才折成分;命中不了就落到第 111 行 return fallbackOffset * 60,也就是表裡寫死的小時數乘以 60。
parse 那條路對非整點偏移算得對:實測 "GMT+5:30" → 330 分、"GMT+8:45" → 525、"GMT-3:30" → -210,標籤打成 UTC+5:30;回退那條的粒度是 60 分鐘,只出整小時,裸串 "GMT" 與縮寫一律給 null。
同一瞬時把 Intl 的選項換成 short,紐約給 "EDT"、洛杉磯給 "PDT",4 座美國城市在第 101 行一個都不命中,標籤退回寫死值。夏令時間被丟掉只有這一個入口——它不在時鐘上,在標籤上。
三、八座城的全年不符天數
圖 1:寫死 offset 與真實偏移全年不符的天數
三檔:雪梨 182.02 天,倫敦、巴黎、柏林各 210 天,4 座美國城市各 237.958 天,也就是 4368.5、5040、5711 小時,剩下 5 座全年 0 個不符。基準 1772971200000 上牆鐘沒問題,這堆天數量的則是標籤會退回哪個數。
四、降級路徑連牆鐘一起錯
第 115-129 行的兜底不問 tzdb:第 116 行 getTimezoneOffset()*60000 先按宿主偏移把瞬時平移,第 121-125 行再從平移後的瞬時取值。基準 1772971200000 實測 5 座城慢 1 小時——雪梨 22:00:00(真值 23:00:00)、紐約 07:00:00、芝加哥、丹佛、洛杉磯同樣,其餘 8 城 ±0h;換成 1784116800000(同年 7 月)變成 7 座,1768478400000(1 月)只剩 1 座。
第二處誤差更隱蔽。模擬一台洛杉磯宿主(T=1772962200000)後,13 城分成兩組:7 座被拖快 1 小時、6 座顯示 0h,雪梨恰好在這 0h 裡——「宿主 +1h」與「寫死 -1h」互相抵銷,看著嚴絲合縫,其實兩處各錯 1 小時。發邀請時,這種「剛好對上」最貴。
五、沒有「相差 N 小時」,差值是你自己減的
兩城時差在頁面上讀不到:<input、<select、<button 命中 0 處,也沒有「相差」這個詞。能相減的只有兩個標籤,而標籤來自第 111 行、牆鐘來自第 61-66 行,切換日兩個口徑各給一個數:北京—紐約 1772971200000 差 13h、30 分鐘後 1772955000000 變 12h;北京—倫敦 1792888200000 差 7h、1792891800000 變 8h,回撥日差值反而變大;北京—雪梨 1775316600000 是 -3h、1775320200000 變 -2h。
星期也不總是同一個:1772947800000 那刻,北京已是週日 13:30,芝加哥、丹佛、洛杉磯、夏威夷還停在週六。我那句「週日加班」,對半螢幕的收件人其實是週六。
六、徽章只認小時
第 152 行 t.hour >= 6 && t.hour < 18 決定卡上是太陽圖標還是月亮圖標,第 153 行那對閾值是 9 與 18,決定綠框與「工作時間」徽章;第 174 行把星期打了出來,兩處判斷都不看它。基準那天恰是週日,實測仍有 5 張卡掛著徽章,同一時刻太陽圖標 8 張、月亮圖標 5 張。
圖 2:兩套小時閾值畫在同一條 24 小時軸上
邊界實測:1776257999000 false、1776258000000 true、1776290399000 true、1776290400000 false。
七、一秒重繪一次,而且什麼都不記得
重新整理只有第 204 行那一個 setInterval(render, 1000),第 185 行整塊 innerHTML 重建 13 張卡、每次 8067 位元組,程式碼口徑 86400 次/天 ≈ 664.7 MB/天;visibilitychange、clearInterval 命中 0 處,切到背景仍照寫。訪客時區靠字串完全相同才算命中,418 個時區只有 13 個在表內,Asia/Taipei、Europe/Zurich 的高亮卡數都是 0——對臺北或蘇黎世的訪客來說,一張卡也不會被標成「您的時區」。
現在我把它當成對照表來讀:只信牆鐘那一格,標籤要和第 111 行的寫死值對上號,才敢抄進郵件。表裡沒有的城市,得先在世界時間之外另找一份 tzdb 口徑;毫秒級的核對看時間戳換算那篇;「同一件事兩種數法」這類坑,每 28 天與每月同日那篇裡踩過一模一樣的。
常被問反的三件事
Q:那這頁還能不能用?
能,只讀卡片時刻那一格:牆鐘走第 61-66 行,標籤走第 111 行,兩者在切換日會分成兩個數。
Q:換個瀏覽器就繞得過去?
不能。Node 與 headless Chrome 在同一基準下逐位元組一致,innerHTML 同為 8067 位元組。
Q:哪幾座城一年到頭不會錯?
北京、東京、杜拜、莫斯科、夏威夷 5 座,不符樣點 0 個;其餘 8 座最少 182.02 天、最多 237.958 天。