4

.NET 彈性日期時間字串解析及小地雷

 3 years ago
source link: https://blog.darkthread.net/blog/flex-datetime-string-parsing/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

.NET 彈性日期時間字串解析及小地雷

2021-01-30 08:06 AM 0 655

專案裡有個需求,希望當使用者輸入字串為日期或日期時間時自動轉成 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");	

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK