最近遇到一個老專案,他之前設計都讓客戶自由輸入生日,不過幸好 placeholder 提示
至少他的客戶是乖乖輸入 國曆xx年xx月xx日,或是農曆xx年xx月xx日,但是他現在跟我說要算
天干地支跟五行...WT...
基本上這問題,給 AI 處理就是最適合的,但是我也是跟 AI 周旋了一下,畢竟因為中間有遇到 潤的問題
因為他讓客戶自由輸入字串,其中包含 "農曆108年七月初七" 或是 "農曆112年潤2月初一" 重點是有閏月
這邊我就直接提供程式碼,中間其實就是無聊的 Regex 跟推算這邊有興趣,就自己再把程式碼貼給 AI 去解釋
程式碼:
public static class DateParseUtil
{
private static readonly Regex RocDateRegex =
new Regex(@"(?:國曆\s*)?(\d{2,3})[年/](\d{1,2})[月/](\d{1,2})[日號]?",
RegexOptions.Compiled);
private static readonly Regex LunarDateRegex =
// new Regex(@"農曆(\d{2,3})年(潤)?(\d{1,2})月(.+)",
new Regex(
@"農曆(\d{2,3})年(潤)?(" +
@"\d{1,2}|正|一|二|三|四|五|六|七|八|九|十|十一|十二" +
@")月(.+)",
RegexOptions.Compiled);
private static readonly TaiwanLunisolarCalendar LunarCalendar =
new TaiwanLunisolarCalendar();
// ===== Public function =====
public static bool TryParseAnyToDateTime(string input, out DateTime date)
{
date = default;
if (string.IsNullOrWhiteSpace(input))
return false;
if (input.Contains("農曆"))
return TryParseLunar(input, out date);
return TryParseRoc(input, out date);
}
// ===== 國曆 / 民國 =====
private static bool TryParseRoc(string input, out DateTime date)
{
date = default;
var match = RocDateRegex.Match(input);
if (!match.Success)
return false;
int rocYear = int.Parse(match.Groups[1].Value);
int month = int.Parse(match.Groups[2].Value);
int day = int.Parse(match.Groups[3].Value);
int year = rocYear + 1911;
try
{
date = new DateTime(year, month, day);
return true;
}
catch
{
return false;
}
}
// ===== 農曆 =====
private static bool TryParseLunar(string input, out DateTime date)
{
date = default;
var match = LunarDateRegex.Match(input);
if (!match.Success)
return false;
int lunarYear = int.Parse(match.Groups[1].Value); // 不要 +1911
bool isLeapMonth = match.Groups[2].Success;
int month = ParseChineseMonth(match.Groups[3].Value);
if (month <= 0 || month > 12)
return false;
int day = ParseChineseDay(match.Groups[4].Value);
if (day <= 0)
return false;
int leapMonth = LunarCalendar.GetLeapMonth(lunarYear);
//驗證潤的是不是這個月
// 驗證潤月合法性
if (isLeapMonth)
{
if (leapMonth == 0 || leapMonth != month + 1)
return false;
}
if (leapMonth > 0)
{
if (isLeapMonth)
{
// 潤月一定要往後推一格
month++;
}
else if (month >= leapMonth)
{
// 非潤月,但在潤月之後
month++;
}
}
try
{
date = LunarCalendar.ToDateTime(
lunarYear,
month,
day,
0, 0, 0, 0);
return true;
}
catch
{
return false;
}
}
private static readonly Dictionary ChineseDayMap =
new Dictionary
{
{ "初一", 1 }, { "初二", 2 }, { "初三", 3 }, { "初四", 4 }, { "初五", 5 },
{ "初六", 6 }, { "初七", 7 }, { "初八", 8 }, { "初九", 9 }, { "初十", 10 },
{ "十一", 11 }, { "十二", 12 }, { "十三", 13 }, { "十四", 14 }, { "十五", 15 },
{ "十六", 16 }, { "十七", 17 }, { "十八", 18 }, { "十九", 19 }, { "二十", 20 },
{ "廿一", 21 }, { "廿二", 22 }, { "廿三", 23 }, { "廿四", 24 }, { "廿五", 25 },
{ "廿六", 26 }, { "廿七", 27 }, { "廿八", 28 }, { "廿九", 29 },
{ "三十", 30 }
};
private static readonly Dictionary ChineseMonthMap =
new Dictionary
{
{ "正", 1 },
{ "一", 1 }, { "二", 2 }, { "三", 3 }, { "四", 4 },
{ "五", 5 }, { "六", 6 }, { "七", 7 }, { "八", 8 }, { "九", 9 },
{ "十", 10 }, { "十一", 11 }, { "十二", 12 }
};
private static int ParseChineseMonth(string text)
{
text = text.Trim();
if (int.TryParse(text, out var m))
return m;
if (ChineseMonthMap.TryGetValue(text, out var cm))
return cm;
return -1;
}
private static int ParseChineseDay(string text)
{
if (string.IsNullOrWhiteSpace(text))
return -1;
text = text.Replace("日", "").Trim();
// 先試中文
if (ChineseDayMap.TryGetValue(text, out var d))
return d;
// 再試數字
if (int.TryParse(text, out var n))
return n;
return -1;
}
}
呼叫方式
bool success = DateParseUtil.TryParseAnyToDateTime(input, out var dt);
測試結果透過 AI 去給我測試資料我實際跑出來後去驗證的
這邊就不過多解釋,畢竟這是方便自己之後使用的,有需要就自取吧
--
本文原文首發於個人部落格:[C#] 處理農曆及潤月自由輸入生日的 DateTime 解析
--
---
The bug existed in all possible states.
Until I ran the code.