菜雞抓蟲: DateTime.ToString() 之我們不一樣 & CultureInfo 文化特性小筆記
事發緣由
咱們內部套件中有個方法,會將各個參數組合為 QueryString 去打指定的 Api。就是這麼稀鬆平常的場景,神奇的事情就發生了。
同樣的套件、同樣的語法,在團隊中兩個人的電腦上安裝執行,卻是一個成功一個失敗。
原來該方法的參數中,包含一欄型別為 DateTime 的資料,並且會把該欄位的值拿來 ToString() 再做為參數傳遞給目標 Api。
而呼叫失敗的人就是在這個 DateTime.ToString()
的過程中產生了中文字,使得目標 Api 接到參數後,無法將中文字轉換回 DateTime 而發生了錯誤。
問題就浮現了:同一行 DateTime.ToString() 在不同電腦執行的結果竟然不一樣?!
為了讓我們更快了解狀況,現在就簡單地使用 Linqpad 進行測試:
void Main()
{
DateTime.Now.ToString().Dump();
}
首先,在我的 Windows 時間設定中,完整時間的格式為 09:40:07
,也就是 24 小時制。
現在讓我們先執行上面這段語法看看:
// 2021/10/04 21:00:00
接著讓我們變更時間格式看看。
以我的 Win10 為例,在 Windows 工具列,也就是畫面的右下角右鍵,選擇 調整日期時間 → 日期時間格式設定 → 變更資料格式
,將時間格式變更為 上午 09:40:07
。
重新啟動 Linqpad 再執行如下:
// 2021/10/04 下午 09:00:00
可以看到 下午
兩個字就蹦出來了!
之所以會有這樣的差異,是因為 DateTime.ToString()
預設轉換的目標格式會是抓取目前執行緒的文化特性。
文化特性
在 CSharp 中有個專門負責處理文化特性、地區設定等在地化處理的類別:CultureInfo。舉凡國家地區的資訊、時間日期的格式、字串的排序方式等等,都是它的工作。
要取得當前的文化特性,我們可以使用 CultureInfo.CurrentCulture
:
CultureInfo.CurrentCulture.Dump(); // zh-TW
可以看到我們目前的文化特性是 zh-TW
,這個格式叫做 IETF 語言標籤,其中 zh
是指語言,TW
則是地區。例如說 en-US
就是「英語,美國」。
補充:關於語言標籤,有需要查詢的朋友可以翻看看 i18n 的 RFC 3066 表。
此外,我們平常用的語言標籤還會有一些子標籤之類的,例如zh-Hant-HK
,可以參見 Language tags in HTML and XML (w3.org)
補充:除了
CurrentCulture
以外,CultureInfo 還有另一組文化特性的設定:CurrentUICulture
。從名稱上可以看得出來,前者是用來預設系統的文化設定,例如數值、排序等等;而加上UI
的後者,則是用來預設使用者介面要顯示什麼語言。當然,大多數時候這兩個文化特性會是一樣的,不過如果想要國際化,弄個使用者介面的多國語言版本,就需要特別注意一下囉!
接著讓我們來看看裡面包了哪些重要東西吧:
DateTimeFormat
DateTimeFormat
是用來放跟 DateTime
相關的設定值的,其中包含了該文化的日期時間的顯示格式。
我們同樣用 Linqpad 看一下裡面有些什麼:
CultureInfo.CurrentCulture.DateTimeFormat.Dump();
可以看到裡面包含了一年有哪些月份和名稱、一週有哪些日子和名稱,以及一堆時間格式。
那一堆時間格式主要是對照 標準日期和時間格式字串 的。
例如 ShortDatePattern 就是對應到文件中的簡短日期模式("d"
),也就是說,當我們 ToString
的時候,如果使用 “d” 的話,就會根據文化特性顯示該文化的「簡短日期」以此類推。
關於這部分的欄位內容,可以參照 Vito 大大的 使用文化特性 這篇文章,裡面的整理已經相當詳細,供各位參考。
補充:我們常常會看到
ToString("yyyy-MM-dd HH:mm:ss")
這種用法,但要小心這邊的-
和:
其實是用來標示日期和時間的分隔符號,並不是真正的-
和:
。所以
:
在 format 的時候,會被替換為TimeSeparator
;
而/
同樣也會使用DateSeparator
也就是說,如果你使用的文化特性
TimeSeparator
是#
的話,HH:mm:ss
在21:07:00
出來的結果將會是21#07#00
。如果發現 ToString 出來的結果怪怪的,可以優先檢查文化特性的這兩個分隔符號有沒有問題。相關的說明請參見 Date and time separator specifiers 這一小節。
現在讓我們用 Thread.CurrentThread.CurrentCulture
來更改當前執行緒的時間,並用完整日期時間模式("F"
)來測試看看:
var date = new DateTime(2006, 1, 2, 3, 4, 5);
Thread.CurrentThread.CurrentCulture = new CultureInfo("zh-tw");
date.ToString("F").Dump(); // 2006年1月2日 03:04:05
Thread.CurrentThread.CurrentCulture = new CultureInfo("zh-cn");
date.ToString("F").Dump(); // 2006年1月2日 3:04:05
Thread.CurrentThread.CurrentCulture = new CultureInfo("en-us");
date.ToString("F").Dump(); // Monday, January 2, 2006 3:04:05 AM
當然,在 ToString 的時候直接扔文化特性進去也是可以的:
var date = new DateTime(2006, 1, 2, 3, 4, 5);
date.ToString(new CultureInfo("zh-tw")).Dump(); // 2006/1/2 03:04:05
date.ToString(new CultureInfo("zh-cn")).Dump(); // 2006/1/2 3:04:05
date.ToString(new CultureInfo("en-us")).Dump(); // 1/2/2006 3:04:05 AM
而本篇起因的翻船事件,我們可以試著觀察一下 CultureInfo
的預設曆法,也就是 Calendar
的內容:
void Main()
{
CultureInfo.CurrentCulture.Calendar.Dump();
CultureInfo.CurrentUICulture.Calendar.Dump();
}
// 完整時間格式: 15:04:05
// MinSupportedDateTime 0001/1/1 上午 12:00:00
// MaxSupportedDateTime 9999/12/31 下午 11:59:59
// 完整時間格式: 下午 03:04:05
// MinSupportedDateTime 0001/1/1 00:00:00
// MaxSupportedDateTime 9999/12/31 23:59:59
可以明確看到格式的變更,兇手就在我們之中!
其實文檔已經明確地說了:使用者可以透過主控台的『地區及語言選項』部分,選擇覆寫與 Windows 目前文化特性相關聯的一些值。 (CultureInfo.Calendar 屬性 (System.Globalization) | Microsoft Docs)
當然,我們也可以調整 Calendar 來把曆法換掉。例如說當我們要做西元年轉民國年的時候,就可以把 Calendar 指定為 TaiwanCalendar 來處理。
有了這些日期時間的格式,我們就可以用它們來處理一些日常的時間處理囉~例如:
- 食譜好菜 DateTime 具有文化特性的格式化及時區的轉換 | 軟體主廚的程式料理廚房 - 點部落 (dotblogs.com.tw)
- 【.NET】利用 CultureInfo 取得各語系星期顯示名稱 | 暴走的程式碼… - 點部落 (dotblogs.com.tw)
NumberFormat
除了日期格式以外,數值也是各個文化常常不同的部份。在 CultureInfo
中的 NumberFormat
就是用來處理數值相關的格式。
我們同樣用 Linqpad 看一下裡面有些什麼:
CultureInfo.CurrentCulture.NumberFormat.Dump();
可以看見裡面包含了 CurrencySymbol
貨幣符號、PercentSymbol
百分比符號、CurrencyNegativePattern
貨幣負數格式 等等數值相關的設定。
現在讓我們使用 ToString("C")
來指定轉換為貨幣格式,並且丟不同的文化特性進去看看吧:
var price = -49.99;
price.ToString("C", new CultureInfo("zh-tw")).Dump(); // -NT$49.99
price.ToString("C", new CultureInfo("zh-cn")).Dump(); // ¥-49.99
price.ToString("C", new CultureInfo("en-us")).Dump(); // ($49.99)
可以看見整個格式都不一樣了呢。
備註:
CurrencyNegativePattern
這類的格式只會存數字編號,例如 0 對應到 ($n) 之類的。 各編號對應的格式可以參見 NumberFormatInfo.CurrencyNegativePattern 屬性 (System.Globalization) | Microsoft Docs
InvariantCulture
那麼,當我們想要統一格式的時候呢?例如說我們在世界各地都有服務,這些客戶端的資訊會集中傳回主伺服器。要是直接 ToString
那鐵定是每個地方回來的時間、貨幣和數值格式都不一樣的。該怎麼辦呢?總不能發個字串下去叫大家寫死吧囧?
這時候就可以使用 InvariantCulture
啦!
讓我們看看文檔是怎麼說明的:「取得與文化特性無關的 (不變的) CultureInfo 物件」、「不區分文化特性的文化特性不區分文化特性」(也太饒舌)
也就是說,只要使用約定好一起使用這組不變的文化特性,就可以保證大家的格式都是同一套囉。
使用的時候可以 CultureInfo.InvariantCulture
或是直接 new CultureInfo("")
就可以取得這組不變的文化特性。
var culinfo = CultureInfo.InvariantCulture;
var date = new DateTime(2006, 1, 2, 3, 4, 5);
date.ToString(culinfo).Dump(); // 01/02/2006 03:04:05
var price = -49.99;
price.ToString("C", culinfo).Dump(); // (¤49.99)
不過如果覺得 月/日/年 的格式很鳥的話,其實大家約好挑一組文化特性就好了齁?
後日談
最後交代一下篇頭的問題怎麼解決的:
直接把該套件打開然後把 .ToString()
加上 "yyyy/MM/dd"
打完收工囧。
就是這麼簡單。爽發一篇廢文,筆記筆記。
2022.05.06 補充:
朋友在將 Asp.net Framework 的服務佈到多台 Windows Server 上時,也遇到了時區設定不一致的問題。
經過一番排查,最後決定在 Web.config 中的 System.web 設定 Globalization 指定服務採用的時區來解決這個問題。
這邊也提供相關的資訊給遇到同樣問題的朋友們參考:
- 多國語系 - 了解文化特性 | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)
- 請參閱:「在 Web.config 設定文化特性」一節
- 在 ASP.NET 中以程式設計方式設定目前的文化環境 | Microsoft Docs
參考資料
- VITO の 學習筆記: 使用文化特性 (vito-note.blogspot.com)
- ASP.NET 多國語系 - 了解文化特性 | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)
- 食譜好菜 DateTime 具有文化特性的格式化及時區的轉換 | 軟體主廚的程式料理廚房 - 點部落 (dotblogs.com.tw)
- 【.NET】利用 CultureInfo 取得各語系星期顯示名稱 | 暴走的程式碼… - 點部落 (dotblogs.com.tw)
- ASP.net Core 多國語系 Part 1 初探篇 | 高級打字員的技術雲 - 點部落 (dotblogs.azurewebsites.net)
- C# CultureInfo 中常用的 InvariantCulture - 郑小超 - 博客园 (cnblogs.com)
- CultureInfo 類別 (System.Globalization) | Microsoft Docs
- CultureInfo.CurrentCulture 屬性 (System.Globalization) | Microsoft Docs
- What is the difference between CurrentCulture and CurrentUICulture properties of CultureInfo in .NET? - Stack Overflow
- .net - What does CultureInfo.InvariantCulture mean? - Stack Overflow
其他文章
哈囉,如果你也有 LikeCoin,也覺得我的文章有幫上忙的話,還請不吝給我拍拍手呦,謝謝~ ;)