業務應用程式的日本年號・國定假日・結算日處理 ── 抗改元設計與 JapaneseCalendar・營業日計算實務

· · C#, .NET, 日本年號(和曆), 國定假日, 結算日, 日期時間處理, 業務應用程式, Windows 開發, 技術諮詢

「報表的日期要用年號」「付款期限是20日結算、次月底付款,若月底是假日則提前至前一個營業日」「彙總以會計年度為單位」──日本的業務應用程式標準配備了只在這個國家才通用的日期需求。而這個領域埋藏著與一般日期時間處理截然不同的陷阱。年號會隨改元增加,國定假日會隨法規修訂而變動,「月底」則因月份而長度不同。換句話說,這些全都是「日後會變動的規格」。

上一篇「業務應用程式的日期時間與時區」整理了以 UTC 儲存的原則以及時區的處理方式。這一次作為續篇,將處理日本在地的三大需求──年號・國定假日・結算日。從抗改元的設計、國定假日不可寫死在程式碼中的理由與資料化的方式,一直到考量 AddMonths 進位規則的結算日計算,將透過程式範例與判斷表逐一說明。

1. 先講結論

  • 內部處理與儲存一律採用西曆(格里曆),年號只存在於顯示的那一瞬間。 以年號字串儲存會導致三重問題:無法排序、改元時表示法混雜、以及混入不正確日期的風險。
  • 年號轉換交給 JapaneseCalendar,而非自製對照表。年號的定義由作業系統端供給,因此改元時只要套用平台更新即可跟上。1 唯有留在應用程式中「平成」「令和」的寫死字串會被拋下。
  • 國定假日無法用公式判定。 春分與秋分是在前一年才確定,且國定假日本身會因一般法規修訂或特別措施法而變動。2 應以國定假日主檔+更新作業的方式設計,資料來源可使用內閣府公布的國定假日 CSV。
  • 「月底」「營業日」「會計年度」應先在規格中定義,而非先寫進程式碼。尤其 AddMonths 會自動對月底進行進位,因此結算日的計算原則上應每次都從基準日重新計算。3

以下先彙整各類資料的保存方式。

資料 保存方式 轉換・判定的時機
日期本身(受訂日・付款期限) 西曆的 DateOnly / DateTime(資料庫為 date 型別)
年號表示(報表・畫面) 不保存 僅在顯示・列印瞬間以 JapaneseCalendar 轉換
國定假日 國定假日主檔(資料表或設定檔) 於營業日計算時參照主檔
公司休業日(夏季休假・創立紀念日) 與國定假日分開的休業日主檔 同上(不與國定假日混用)
結算日・付款條件 客戶主檔的屬性(「20日結算次月底付款」) 請款處理時從基準日計算
會計年度 不保存(導出值) 顯示・彙總時由日期計算

2. 年號 ── 讓它只存在於顯示層

2.1 為何內部要統一為西曆

年號是給人看的表示法,並不是適合計算的表現形式。「平成9年」與「令和9年」在字串比較下無法決定順序,任何跨越年號的期間計算最終都需要轉換為西曆。而且年號體系會隨每次改元而增加,因此以年號原樣保存的資料,每次改元都會累積新舊表示法混雜的問題。像「昭和64年1月10日」(實際不存在,1月8日起已是平成)這類不正確資料的混入,也是把年號輸入原封不動保存的系統中常見的麻煩。

還有一個容易被忽略的限制:.NET 的 JapaneseCalendar 支援的範圍僅限明治元年(1868年)9月8日以後4 在此之前的日期──例如源自戶籍的舊生日──若仍以年號形式保存將無法正確處理。只要內部統一為西曆格里曆,這些問題就都會被侷限在「顯示層的問題」而已。

2.2 以 JapaneseCalendar 顯示與輸入

.NET 中能處理多個年號(era)的行事曆只有 JapaneseCalendar(與 JapaneseLunisolarCalendar),格式指定字元 g 對應年號名稱。1 以年號顯示的最簡程式碼如下。

using System.Globalization;

var jaJpWareki = new CultureInfo("ja-JP");
jaJpWareki.DateTimeFormat.Calendar = new JapaneseCalendar();

var date = new DateOnly(2026, 7, 11);
Console.WriteLine(date.ToString("ggy年M月d日", jaJpWareki));  // 令和8年7月11日
Console.WriteLine(date.ToString("ggyy/MM/dd", jaJpWareki));   // 令和08/07/11

若要接受年號輸入,比起解析自由輸入的文字字串,設計成「以下拉選單選擇年號、年月日以數字輸入」的介面更為安全。若無論如何都必須接受文字輸入,請以 ParseExact 固定格式。

var input = "令和8年7月11日";
var parsed = DateTime.ParseExact(input, "ggy年M月d日", jaJpWareki);
Console.WriteLine(parsed.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));  // 2026-07-11

元年的表示法需要留意。在已套用改元支援更新的環境中,各年號的第一年預設會格式化為「令和元年」這類元年表示,並提供了可還原為數字「令和1年」的相容性開關(Switch.System.Globalization.FormatJapaneseFirstYearAsANumber)。5 報表格式要求的究竟是「令和1年」還是「令和元年」,應在實作前作為規格予以確認。另外,預設情況下年號的年份範圍檢查是寬鬆的,因此像「平成33年」這種實際不存在的年份指定也會被接受並換算為實際日期(令和3年)。若要作為輸入驗證嚴格擋下,可透過 Switch.System.Globalization.EnforceJapaneseEraYearRanges 強制執行。5 究竟要維持寬鬆還是改為嚴格,取決於既有資料的髒污程度(該如何處理改元前列印・輸入的「平成32年」)。

2.3 抗改元的程式碼,以及被拋下的部分

從令和改元(2019年)的經驗可以歸納出:平台會跟上,但應用程式自製的實作不會跟上。 年號的定義並未寫死在 .NET 中,而是參照作業系統端所持有的年號資訊,因此只要持續套用作業系統與 .NET 的更新,JapaneseCalendar 的轉換・格式化就會跟上新年號。1 會被拋下的是以下這類應用程式端的資產:

  • if (year >= 2019) era = "令和"; 這類自製的年號轉換表・分支
  • 直接寫在報表範本、Excel 儲存格格式、印刷表單上的年號
  • 以自訂規則處理「R」「H」等單一字母年號縮寫的程式碼(在與核心系統串接的格式中常見)
  • 仍以年號原樣保存的資料本身

在估算改元因應改修時,應先對整個原始碼與範本以「昭和・平成・令和・元年」進行機械式 grep 盤點。從 VB6 或 Access 時代就在運作的業務應用程式,幾乎必定埋有自製的轉換表,因此這個議題常與「VB6 / Access 業務應用程式的延壽與遷移」中提到的存續判斷一併處理。

2.4 潛藏的伏兵:Windows 地區設定

還有一個容易被忽略的陷阱。在 Windows 的地區設定中,使用者可以將作業系統的行事曆類型變更為「年號」。由於 CultureInfo.CurrentCulture 預設會反映使用者的設定變更(使用者覆寫),6 在此設定下的裝置,文化特性預設的 DateTime.ToString() 會突然開始回傳如「令和08/07/11」這類年號字串。若程式碼是以文化特性預設的 ToString() 將日期寫入記錄檔、CSV 或資料庫,就會導致僅在該裝置上,儲存資料混入年號,破壞後續的剖析處理。

對策與前一篇文章的結論相同:任何跨越邊界(儲存・通訊・記錄)的日期字串,都必須以 CultureInfo.InvariantCulture 與明確格式輸出。 文化特性預設格式僅供畫面顯示使用。文化特性對顯示層整體的影響,也可參考「WinForms/WPF 應用程式的多語系化」。

3. 國定假日 ── 不可寫死在程式碼中的理由與主檔運作

3.1 國定假日「會變動」

國定假日的判定絕不能寫成 switch (月, 日)。國定假日是依據國民的節日相關法律所訂定,因法規修訂與實務運作而多次變動過2 光是近年的實際案例就有:

年份 變動內容
2016 新設山之日(8月11日)
2019 因應改元,天皇誕生日從12月23日移到2月23日。2019年當年並不存在天皇誕生日
2019 因皇位繼承的特例,5月1日(即位之日)等成為臨時國定假日・休日
2020 體育之日改名為運動之日
2020・2021 依奧運特別措施法,海之日・運動之日・山之日僅在當年移動

此外,春分之日與秋分之日原本就沒有寫進法律的固定日期。 這是根據天文計算,於前一年才確定並公布的機制,因此確實掌握未來春分・秋分日期的唯一方法就是等待官方公布。2 「國定假日每年都相同」這種前提的計算公式,原理上根本不成立。

3.2 資料來源與保存方式判斷表

國定假日的判定應以「資料+更新作業」來設計。以下比較主要的選項。

方式 內容 適合情境 注意事項
國定假日主檔(自行管理) 資料表中保存年月日與名稱,透過管理畫面更新 想與公司休業日一併運作的一般業務系統 將更新納入業務運作(把每年一次的確認排入行事曆)
匯入內閣府 CSV 匯入內閣府公布的國定假日 CSV 以更新主檔 想要主檔更新的第一手資料來源時(建議做法) 只涵蓋已公布的期間,須留意文字編碼
國定假日程式庫(NuGet 等) 以計算邏輯+內建資料進行判定 內部工具等無法維持更新作業的小規模用途 未來的法規修訂需仰賴程式庫更新,須驗證內建規則
寫死程式碼 直接把日期寫在原始碼中 每次法規修訂都需要對所有用戶端重新發布

作為第一手資料來源,可使用內閣府在「國民的節日」頁面公布的自昭和30年(1955年)至隔年為止的國定假日月日・名稱 CSV2 匯入時實務上須注意兩點:(1) 公布的僅為已確定的部分,因此應在規格中決定被查詢後年以後的國定假日時的行為(回傳錯誤,或以法律為基礎填入暫定值並附上「暫定」旗標);(2) 此 CSV 的文字編碼屬於 Shift_JIS 系列,讀取時須如「CSV並非單純的「文字檔」」中所述明確指定編碼。

3.3 補假・國民休日與營業日計算

除了日期清單之外,國定假日相關法律還定義了兩條衍生規則:補假(國定假日若恰逢星期日,則其後最接近的「非國定假日之日」訂為休日)與國民休日(前後兩日皆為國定假日所夾的平日訂為休日)。2 內閣府 CSV 中也將這些以資料列形式包含在內,因此若採用 CSV 匯入方式,只需參照主檔即可。即使自行實作補完這項邏輯,由於其源自法律,相對穩定。

營業日計算應以「非週末・不在國定假日主檔中・不在公司休業日主檔中」三項條件撰寫。

public sealed class BusinessDayCalendar
{
    private readonly HashSet<DateOnly> _publicHolidays;   // 國定假日主檔(含補假)
    private readonly HashSet<DateOnly> _companyHolidays;  // 公司休假日主檔

    public BusinessDayCalendar(IEnumerable<DateOnly> publicHolidays,
                               IEnumerable<DateOnly> companyHolidays)
    {
        _publicHolidays = [.. publicHolidays];
        _companyHolidays = [.. companyHolidays];
    }

    public bool IsBusinessDay(DateOnly d) =>
        d.DayOfWeek is not (DayOfWeek.Saturday or DayOfWeek.Sunday)
        && !_publicHolidays.Contains(d)
        && !_companyHolidays.Contains(d);

    // 若到期日為假日,則「提前至前一個營業日」。是否延後由合約規格決定
    public DateOnly ToPreviousBusinessDay(DateOnly d)
    {
        while (!IsBusinessDay(d)) d = d.AddDays(-1);
        return d;
    }
}

將國定假日與公司休業日分成不同的主檔是刻意的設計。「銀行匯款期限的判定只看國定假日(自家公司的夏季休假無關)」「出貨日的判定則兩者都看」──依用途不同,需要參照的休日集合也不同。若混在同一個主檔中,日後要分離的改修工程就會變成對既有資料的考古挖掘。

4. 結算日・付款條件・會計年度 ── 以計算正確處理「月底」

4.1 AddMonths 的進位規則

結算日計算的第一個陷阱是 AddMonthsDateTime.AddMonths / DateOnly.AddMonths 在計算結果落在該月不存在的日期時,會進位到該月的最後一天3

var jan31 = new DateOnly(2026, 1, 31);
var feb = jan31.AddMonths(1);   // 2026-02-28 ── 進位到月底(正確)
var mar = feb.AddMonths(1);     // 2026-03-28 ── 原本想要「月底」卻變成了 28 日

進位本身是正確的規格,但若對已經進位過的結果再次套用 AddMonths,「月底」這個意圖就會消失。 若每月請款排程以「上次執行日+1個月」建構,就會在通過2月的瞬間,讓執行日永久變成28日,此問題便會浮現。對策有兩個:

  • 重複的日期不要以前一次的結果為基準,而是每次都從基準的定義(「每月底」「每月20日」)重新計算
  • 「月底」不要使用日期字面值,改由 DateTime.DaysInMonth(y, m) 推導

4.2 「20日結算次月底付款」的實作

結算日與付款條件是每個往來對象的屬性。將「20日結算次月底付款」寫成程式碼如下。

public static class PaymentTerms
{
    // 交易日 → 結算日(每月 20 日結算範例)
    public static DateOnly GetClosingDate(DateOnly tradeDate, int closingDay)
    {
        var dayInMonth = Math.Min(closingDay, DateTime.DaysInMonth(tradeDate.Year, tradeDate.Month));
        var closing = new DateOnly(tradeDate.Year, tradeDate.Month, dayInMonth);
        return tradeDate <= closing ? closing : closing.AddMonths(1);
        // 註:closingDay=31(月底結算)會因 AddMonths 的進位而正確變成下個月底
    }

    // 結算日 → 付款期限(次月月底付款範例)。假日調整請先確認規格(提前/延後)再套用
    public static DateOnly GetPaymentDueDate(DateOnly closingDate, BusinessDayCalendar calendar)
    {
        var nextMonth = closingDate.AddMonths(1);
        var endOfMonth = new DateOnly(nextMonth.Year, nextMonth.Month,
                                      DateTime.DaysInMonth(nextMonth.Year, nextMonth.Month));
        return calendar.ToPreviousBusinessDay(endOfMonth);  // 適用於「若為假日則提前至前一個營業日」的合約條件
    }
}

比程式碼更重要的,是在此之前規格的確定。「月底結算」的月底,究竟是曆日還是營業日?期限若落在假日,該提前還是延後(付款方傾向提前,收款方預計入帳日則傾向延後──立場不同慣例也不同)?29~31日結算的合約在2月該如何解讀?──若這些沒有文件化,實作者的程式碼就會原封不動地變成「沒有任何人同意過的規格」。結算日相關的錯誤調查,多數最後追查到的不是程式碼的錯誤,而是規格的缺席

4.3 會計年度與季度

日本業務應用程式的「會計年度」幾乎都是從4月開始。會計年度不應保存,而應作為從日期導出的值來處理。

public static int GetFiscalYear(DateOnly d) => d.Month >= 4 ? d.Year : d.Year - 1;
// 2026-03-31 → 2025 財年,2026-04-01 → 2026 財年

public static int GetFiscalQuarter(DateOnly d) => ((d.Month + 8) % 12) / 3 + 1;
// 4-6 月 → Q1,7-9 月 → Q2,10-12 月 → Q3,1-3 月 → Q4

須注意不要輕率地將「會計年度」冗餘保存到資料庫中。若把只要有日期就能隨時導出的值另外保存在一個欄位,一旦修正日期時忘記同步更新,就會產生日期與會計年度不一致的資料。若因彙總效能考量無論如何都必須保存,請將其做成計算欄(產生型欄位),並禁止應用程式端直接寫入。

4.4 測試 ── 先踩過「只在那一天」才會發作的錯誤

年號・國定假日・結算日的錯誤,只會在特定日期發作。以前一篇文章介紹的透過 TimeProvider 注入目前時間(「業務應用程式的日期時間與時區」第7章)為前提,這個領域至少應測試以下五個日期:

  • 改元邊界:1989-01-07/08(昭和→平成)、2019-04-30/05-01(平成→令和)的顯示,以及年號輸入的反向轉換
  • 元年:「令和元年」表示法的報表確認(2.2節)
  • 閏年:包含2月29日的結算・付款條件計算(2024年、2028年)
  • 月底的連鎖:以1月31日為起點產生12個月份的結算日,確認月底是否維持(4.1節)
  • 會計年度邊界:3月31日與4月1日的會計年度・季度判定

此外,在程式碼審查時,以下模式也能透過 grep 機械式地找出。

發現時應懷疑的程式碼 可能發生什麼問題 修正方式
"平成" "令和" 等年號字面值 下次改元時被拋下 改用 JapaneseCalendarg 格式(2.3節)
國定假日的日期字面值(如 5, 3 因法規修訂・特別措施法而誤判 改為參照國定假日主檔(3.2節)
無參數 ToString() 的儲存・輸出路徑 在年號設定裝置上,儲存資料混入年號 InvariantCulture +明確格式(2.4節)
重複使用 前一日期.AddMonths(1) 通過2月後月底持續偏離 每次都從基準日重新計算(4.1節)
new DateTime(y, m, 31) 在30日以下的月份會拋出例外 DateTime.DaysInMonth 推導

5. 總結

年號・國定假日・結算日,處理的全都是「日後會變動的規格」。因此設計原則也是共通的:會變動的東西不要寫進程式碼,而是隔離到資料與轉換層中──內部與儲存統一為西曆,年號僅作為顯示用轉換;國定假日以主檔+更新作業的方式管理;「月底」「營業日」「期限的假日調整」則先以合約規格文件化,再以基準日為準進行計算。做到這個地步,下一次改元、下一次國定假日法規修訂,對應用程式而言就只會是「更新主檔」與「套用作業系統更新」即可解決的事件。

本公司承接報表・請款相關日期處理的設計與實作、既有應用程式中埋藏的年號・國定假日寫死問題的盤點與改修,以及包含結算日規格整理在內的設計審查。即使還處於「不知道改元因應散落在哪裡、有多少項」的階段,我們也能提供協助。

相關文章

相關諮詢領域

合同會社小村軟體處理符合日本商業慣例的業務應用程式日期處理(年號報表・營業日計算・結算日處理)的設計與實作,以及既有應用程式的改元因應・國定假日因應的盤點與改修。

參考連結

  1. Microsoft Learn, Work with eras - .NET. 關於 JapaneseCalendar 與 JapaneseLunisolarCalendar 是唯二能識別多個年號(era)的行事曆類別、透過格式指定字元「g」進行年號格式化,以及年號資訊由平台端供給、不依賴應用程式寫死的說明。  2 3

  2. 日本內閣府, 關於「國民的節日」. 關於依國民的節日相關法律訂定的國定假日清單、春分之日・秋分之日於前一年確定並公布、補假・國民休日的規定,以及提供自昭和30年至隔年為止的國定假日月日・名稱 CSV 的說明。  2 3 4 5

  3. Microsoft Learn, DateTime.AddMonths(Int32) Method. 關於相加結果的日期超過結果月份的天數時,會調整為結果月份最後一天(例如1月31日+1個月會變成2月28日/29日)的說明。  2

  4. Microsoft Learn, JapaneseCalendar Class. 關於 JapaneseCalendar 支援的日期範圍為明治元年(西元1868年)9月8日以後,以及年號(era)處理方式的說明。 

  5. Microsoft Learn, Work with calendars - .NET. 關於將年號第一年格式化為「元年」的預設行為與還原為數字表示的 Switch.System.Globalization.FormatJapaneseFirstYearAsANumber 開關,以及年號的年份範圍檢查預設為寬鬆、可透過 Switch.System.Globalization.EnforceJapaneseEraYearRanges 強制嚴格化的說明。  2

  6. Microsoft Learn, CultureInfo.CurrentCulture Property. 關於 CurrentCulture 預設會反映 Windows 使用者的地區設定變更(使用者覆寫),以及格式化預設值依賴此文化特性的說明。 

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

常見問題

整理諮詢這個主題時常見的問題。

資料庫不可以直接以年號原樣儲存日期嗎?
不建議這麼做,理由有四個。第一,「平成9年」與「令和9年」這類年號字串,一旦跨越改元的分界點,用字串比較就會失去大小順序,無法排序也無法做範圍查詢。第二,下一次改元發生的瞬間,既有資料與新資料的表示法就會混雜在一起。第三,容易混入「昭和64年1月10日」(實際不存在,1月8日起已是平成元年)這類不存在日期的髒資料,成為日後剖析失敗的未爆彈。第四,.NET 的 JapaneseCalendar 只支援明治元年(1868年)9月8日之後的日期,更早的生日等資料若仍以年號形式保存,將無法正確處理。原則是內部處理與儲存一律統一為西曆(格里曆),年號只在畫面與報表輸出的瞬間才轉換。
下一次改元來臨時,應用程式該如何因應?
年號的定義本身並未寫死在 .NET 中,而是參照作業系統端提供的年號資訊,因此只要持續套用作業系統與 .NET 的更新,「以新年號顯示」這件事本身會自動跟上。令和改元當時,因應的重點其實就是套用平台更新。真正的問題出在應用程式端的程式碼與資料:以字串寫死「平成」「令和」的分支、自行維護年號與西曆對照表的轉換函式、直接寫在報表範本裡的年號、印刷表單上的「令和 __年」欄位,這些都不會自動跟進。務實的因應方式有四點:(1) 內部與儲存一律統一為西曆,讓年號只用於顯示;(2) 放棄自製年號轉換邏輯,改用 JapaneseCalendar;(3) 對整個原始碼以「昭和・平成・令和」進行機械式 grep 盤點;(4) 準備假設未來改元情境的測試。
國定假日不能用公式計算嗎?為什麼一定要用資料的方式保存?
完整的計算公式原理上做不出來,理由有三個。第一,春分之日與秋分之日是根據天文計算,經內閣會議在前一年才確定,並未以固定日期寫入法律。第二,國定假日會因法規修訂而新增、移動或改名:實際上就有2016年新設山之日、2019年因改元使天皇誕生日從12月23日移到2月23日(**2019年當年並不存在天皇誕生日**)、體育之日改名為運動之日等案例。第三,2020年與2021年由於特別措施法,海之日・運動之日・山之日配合奧運僅在當年移動,顯示出「每年固定第三個星期一」這類規則本身也可能被臨時覆寫。因此國定假日的判定應設計成「國定假日主檔(資料)+更新作業」,計算邏輯只用於補充替代假日等輔助規則。
用了 AddMonths(1) 之後,付款期限偏離了月底。為什麼?
因為 DateTime.AddMonths 的規格是:若計算結果落在該月不存在的日期,就會進位到該月的最後一天。對1月31日套用 AddMonths(1) 會得到2月28日(閏年則是29日),這個行為本身是正確的。但如果把這個經過進位的結果再套用一次 AddMonths,對2月28日套用 AddMonths(1) 會得到3月28日,之後就會一直偏離下去——「原本想要月底」卻在不知不覺間變成了「28日」,並且持續錯下去。對策有兩個:重複計算時不要在前一次的結果上累加,而是每次都從基準日(合約日或「每月底」的定義)重新計算;「月底」不要用日期字面值,而是透過 DateTime.DaysInMonth 推導。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽