ToUniversalTime() 之時區陷阱
4 | 2,479 |
Side Project 有段程式從外部 API 取得一個 DateTimeKind.Unspecified 的時間值,彙整時用 ToUniversalTime() 轉換為 UTC 時間再轉 JSON 儲存,依據規格該日期時間以台北時間為準,開發及測試階段結果如預期,不覺得這寫法有什麼不對。
不過,當程式部署到雲端主機,儲存的時間值出現時差。這才想起,踩到 Unspecified DateTimeKind 的轉換陷阱了:
Unspecified很有趣(但也可能變成陷阱),ToUniversalTime()時被視為本地時間(台北時區)減去8小時,ToLocalTime()時被視為UTC時間加上8小時。
開發跟測試機跟雲端主機的時區不同,本地時間基準不同,會得到不同的結果,用以下範例重現問題。
DateTimeKind.Unspecified 的 2022-8-14 08:00 跑 ToUniversalTime(),在台北時區得到 8-14 00:00,若主機設成東京時間則為 8-13 23:00 (比台北早一小時):
既然規格上該 DateTimeKind.Unspecified 時間應視為台北時間處理,正確做法應用 TimeZoneInfo.FindSystemTimeZoneById() 取得台北時區 TimeZoneInfo 再用 TimeZoneInfo.ConvertTimeToUtc 以指定時區為基準轉換成 UTC 時間。程式範例如下:
var dateFmt = "yyyy-MM-dd HH:mm";
Action<DateTime> dispTime = (dt) => {
Console.WriteLine(dt.ToString(dateFmt) + "/" + dt.Kind);
};
var d = new DateTime(2022, 8, 14, 08, 00, 00);
var tpeTime = TimeZoneInfo.FindSystemTimeZoneById("Taipei Standard Time");
var tpeTimeToUtc = TimeZoneInfo.ConvertTimeToUtc(d, tpeTime);
dispTime(tpeTimeToUtc);
Console.WriteLine(TimeZone.CurrentTimeZone);
這樣子,即使程式在夏威夷跑,也能得到正確結果:
FindSystemTimeZoneById 的 "Taipei Standard Time" 要怎麼查?在微軟文件 Default Time Zones 可找到完整清單。
【補充】
- MVP demo 分享:用自帶時區資訊的 DateTimeOffset 取代 DateTime 克服問題 :還在用 DateTime 嗎?試試 DateTimeOffset 吧
- MVP 軟體主廚分享:用 TimeZoneInfo.GetSystemTimeZones() 可查詢時區清單。
When unspecified kind DateTime.ToUniversalTime(), it's based on local time and affected by timezone setting. So we need to use TimeZoneInfo to get identical results.
Comments
# by demo
上雲以後總是會踩一下時區問題,我之前也踩了😅 https://demo.tc/post/%E9%82%84%E5%9C%A8%E7%94%A8%20DateTime%20%E5%97%8E%EF%BC%9F%E8%A9%A6%E8%A9%A6%20DateTimeOffset%20%E5%90%A7
# by Jeffrey
to demo, 感謝分享,已補充於本文。
# by Chris Torng
可參考 Noda Time https://nodatime.org/ https://blog.nodatime.org/2011/08/what-wrong-with-datetime-anyway.html
# by Jeffrey
to Chris Torng,Noda Time 好眼熟,原來是 Jon Skeet 的 Side Project。謝謝分享