[C#] 處理農曆及潤月自由輸入生日的 DateTime 解析

  • 172
  • 0

最近遇到一個老專案,他之前設計都讓客戶自由輸入生日,不過幸好 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 去給我測試資料我實際跑出來後去驗證的

# 輸入字串 TryParse DateTime 判定
011年1月1日False合理(規格不支援 1 位數民國)
0210年12月31日True1921-12-31正確
0387年1月1日True1998-01-01正確
0499/12/31True2010-12-31正確
05100年1月1日True2011-01-01正確
06101/01/09True2012-01-09正確
07109年6月30日True2020-06-30正確
08110年02月28日True2021-02-28正確
09111/12/01True2022-12-01正確
10112年7月7日True2023-07-07正確
11113/03/03True2024-03-03正確
12114年11月11日True2025-11-11正確
13120/1/1True2031-01-01正確
14國曆120年12月31日True2031-12-31正確
15國曆95/6/5True2006-06-05正確
16農曆111年1月初一True2022-02-01正確
17農曆111年2月初二True2022-03-04正確
18農曆111年3月初三True2022-04-03正確
19農曆111年4月初四True2022-05-04正確
20農曆111年5月初五True2022-06-03正確
21農曆111年6月初六True2022-07-04正確
22農曆111年7月初七True2022-08-04正確
23農曆111年8月初八True2022-09-03正確
24農曆111年9月初九True2022-10-04正確
25農曆111年10月初十True2022-11-03正確
26農曆111年11月十一True2022-12-04正確
27農曆111年12月十二True2023-01-03正確
28農曆110年8月廿九True2021-10-05正確
29農曆110年12月三十False合理(該年無此日)
30農曆109年1月十五True2020-02-08正確
31農曆108年正月初一True2019-02-05正確
32農曆108年二月初二True2019-03-08正確
33農曆108年三月初三True2019-04-07正確
34農曆108年四月初四True2019-05-08正確
35農曆108年五月初五True2019-06-07正確
36農曆108年六月初六True2019-07-08正確
37農曆108年七月初七True2019-08-07正確
38農曆108年八月初八True2019-09-06正確
39農曆108年九月初九True2019-10-07正確
40農曆108年十月初十True2019-11-06正確
41農曆112年潤2月初一True2023-03-22正確(潤月)
42農曆112年2月初一True2023-02-20正確
43農曆112年3月初一True2023-04-20正確
44農曆112年潤三月初一False合理(潤月錯誤)
45農曆112年0月初一False合理(月非法)
46農曆112年13月初一False合理(月超界)
47農曆112年2月廿False合理(日不完整)
48農曆112年2月三十一False合理(日超界)
49農曆一一二年正月初一False合理(不支援中文年)
50農曆112年正月初零False合理(非法日)

這邊就不過多解釋,畢竟這是方便自己之後使用的,有需要就自取吧

 

--

本文原文首發於個人部落格:[C#] 處理農曆及潤月自由輸入生日的 DateTime 解析

--

---

The bug existed in all possible states.
Until I ran the code.