專案裡有個需求,希望當使用者輸入字串為日期或日期時間時自動轉成 DateTime 型別,我想到用 DateTime.TryParse 來做(註:更嚴謹的做法是改用 DateTime.TryParseExact 正向表列所有支援格式,但我選擇借用 TryParse 內建的彈性較省事),目標是要能支援多種格式,例如:

  • 2021/01/01 00:00:00
  • 2021-01-01 08:00
  • 2021/01/01 08:00
  • 2021/1/1 8:00
  • 2021-01-01T00:00:00+0800
  • 2021-12-31T16:00:00Z

我選擇的解析參數是 DateTime.TryParse(dateString, culture, styles, out dateResult),culture 傳 null,styles 為 DateTimeStyles 列舉,實測 AssumeLocal、AssumeUniversal、AdjustToUniversal、RoundtripKind 幾種選項的結果:

void Main()
{
	string v = null;
    DateTime d = DateTime.Now;
    Action<System.Globalization.DateTimeStyles> testStyle = (style) => {
        Console.Write($"{style,-18} => ");
        if (DateTime.TryParse(v, null, style, out d)) 
            Console.WriteLine($"{d:yyyy-MM-dd HH:mm:ss} ({d.Kind})");
        else 
            Console.WriteLine("invalid");
    };
    
    Action<string> test = (dateStr) => {
        v = dateStr;
        Console.WriteLine($"**** {v} ****");
        testStyle(System.Globalization.DateTimeStyles.AssumeLocal);
        testStyle(System.Globalization.DateTimeStyles.AssumeUniversal);
		testStyle(System.Globalization.DateTimeStyles.AdjustToUniversal);
        testStyle(System.Globalization.DateTimeStyles.RoundtripKind);
        Console.WriteLine();
    };
	
    test("2021/01/01 00:00:00");    
	test("2021-01-01 08:00");
	test("2021/01/01 08:00");
	test("2021/1/1 8:00");
    test("2021-01-01T00:00:00+0800");
    test("2021-12-31T16:00:00Z");
}

評估 AssumeLocal 最接近我要的結果。RoundtripKind 的優點是遇到 2021-01-01T00:00:00+0800 或 2021-12-31T16:00:00Z 可保留 DateTimeKind 資訊,但字串未標明時區時會傳回 DateKind.Unspecified;AssumeLocal 除了會將 2021-12-31T16:00:00Z 硬轉成本地時間(系統以用本地時間為準,例也無所謂),其他輸入字串的結果都符合預期。

不過,後來發現我踩到小地雷 - 有一些小數,例如:3.1416、12.345、12.3456 會被當成日期。

在微軟文件 Standard date and time format strings 只有年月日用 "-" 或 "/" 分隔的範例,沒提到可以用 dot "."。但我查到這篇 - How to write dates - English Language Help Desk

When writing the date by numbers only, one can separate them by using a hyphen (-), a slash (/), or a dot (.): 05-07-2013, or 05/07/2013, or 05.07.2013.
Omitting the initial zero in the numbers smaller than 10 is also accepted: 5-7-2013, 5/7/2013, or 5.7.2013.

所以的確有人會用 . 分隔日月年,但倒也沒提到「月.年」這種格式。依實測,DateTime.Parse 會將整數介於 1 - 12、小數三位或四位的數字解讀為 M.yyyy。

最後我決定修改程式,加上 Regex.IsMatch(dateStr, @"^\d{4}[/-]") 限定以年份起始並以 / 或 - 分隔年月的字串才當成日期時間,收工。

Action<string> parse = (dateStr) => {
	DateTime dateVal;
	Console.Write($"{dateStr, -20} => ");
    if (System.Text.RegularExpressions.Regex.IsMatch(dateStr, @"^\d{4}[/-]") &&
		DateTime.TryParse(dateStr, null, 
		System.Globalization.DateTimeStyles.AssumeLocal, out dateVal)) 
        Console.WriteLine($"{dateVal:yyyy-MM-dd HH:mm:ss} ({dateVal.Kind})");
    else 
        Console.WriteLine("invalid");
};

parse("2021.1.1 00:00:00");
parse("3.1416");
parse("12.34");
parse("12.345");
parse("13.3456");
parse("12.3456");		
parse("2021/01/01 00:00:00");    
parse("2021-01-01 08:00");
parse("2021/01/01 08:00");
parse("2021/1/1 8:00");
parse("2021-01-01T00:00:00+0800");
parse("2021-12-31T16:00:00Z");	

Example of parsing different formats of date time string with DateTime.TryParse, and a pitfall for numbers with decimal point.


Comments

Be the first to post a comment

Post a comment