業務アプリは日付ルールだらけです。「月末締め翌月末払い」「5 営業日以内に発送」「期日の 3 営業日前にリマインドを送る」——どれも仕様書では 1 行ですが、DateTime で自前実装すると意外に面倒です。月の長さはバラバラ、土日が割り込み、祝日が去年書いたループを壊しに来ます。
表計算の世界では、この問題は何十年も前に専用関数で解決済みです。EDATE・EOMONTH・WORKDAY・NETWORKDAYS の 4 つ。この記事でそれぞれの役割と落とし穴を整理し、ReoGrid(V4.5 で対応)を使って C# アプリの中で同じ日付計算をそのまま動かす方法まで説明します。ユーザーが Excel で使い慣れた日付ルールが、WinForms / WPF の画面でもそのまま通用するようになります。
この記事は Excel 定番関数シリーズの続編です。「値を引く」 VLOOKUP / HLOOKUP / XLOOKUP、「位置を返す」 MATCH / XMATCH、「条件で集計する」 SUMIF / COUNTIF と合わせてどうぞ。
まず結論 — 4 つの関数の役割
| 何に答える関数か | 書式 | |
|---|---|---|
EDATE | 「N か月後の同じ日」 | EDATE(起点日, 月数) |
EOMONTH | 「N か月後の月末日」 | EOMONTH(起点日, 月数) |
WORKDAY | 「N 営業日後の日付」 | WORKDAY(起点日, 日数, [祝日]) |
NETWORKDAYS | 「2 つの日付の間の営業日数」 | NETWORKDAYS(開始日, 終了日, [祝日]) |
前の 2 つは月単位、後ろの 2 つは営業日単位(土日と、指定すれば祝日を飛ばす)で考える関数です。この 4 つで、業務アプリに出てくる日付ルールの大半をカバーできます。
EDATE — N か月後の同じ日
EDATE(起点日, 月数) は、日付を月単位でずらします。日にちはそのまま。契約更新、サブスクの請求日、分割払いのスケジュールなどに使います。
=EDATE("2026/07/07", 1) → 2026/08/07 (翌月の請求日)
=EDATE("2026/07/07", 12) → 2027/07/07 (1 年後の更新日)
=EDATE("2026/07/07", -6) → 2026/01/07 (負の値で過去方向)
知っておくべき挙動が 1 つ。移動先の月が短い場合、日にちはその月の末日に丸められます。Excel と同じ動きです。
=EDATE("2026/01/31", 1) → 2026/02/28 (2 月 31 日は存在しない)
「31 日締め、短い月は末日」という運用ではこの丸めがそのまま欲しい挙動ですが、可逆ではなくなる点に注意——1 月 31 日から +1 か月 → −1 か月と往復すると 1 月 28 日に着地します。
EOMONTH — 「月末締め翌月末払い」の関数
EOMONTH(起点日, 月数) は N か月後の月末日を返します。日本の商習慣でいちばん多い支払条件「月末締め翌月末払い」は、この関数 1 つで書けます。
=EOMONTH(A2, 0) → 請求日の月の末日(締め日)
=EOMONTH(A2, 1) → 翌月末 ← 「月末締め翌月末払い」
=EOMONTH(A2, 2) → 翌々月末(「月末締め翌々月末払い」もこれだけ)
覚えておくと効くイディオムを 2 つ。
=EOMONTH(A2, -1) + 1 → A2 の月の「1 日」(月初)
=DAY(EOMONTH(A2, 0)) → A2 の月の日数
C# で new DateTime(y, m, DateTime.DaysInMonth(y, m)) という定型文を書いたことがあるなら、EOMONTH はあれを 1 語にしたものです。しかもユーザーが読めます。
WORKDAY — N 営業日後の締切
WORKDAY(起点日, 日数, [祝日]) は、土曜・日曜と祝日リストの日付を飛ばしながら 日数 営業日ぶん進んだ日付を返します。
=WORKDAY(A2, 5) → A2 の 5 営業日後(発送期限)
=WORKDAY(A2, 5, H2:H20) → 同上。H2:H20 の祝日も飛ばす
=WORKDAY(D2, -3, H2:H20) → D2 の 3 営業日「前」(リマインド日)
全員が一度は引っかかるルールが 2 つあります。
- 起点日そのものは数えません。「月曜の 1 営業日後」は火曜です。「今日を含めて 5 営業日以内」なら
WORKDAY(A2, 4)。 - 起点日が土日でもエラーにはならず、そこから数え始めて最初の 1 歩が翌営業日に落ちます。
負の日数は地味に業務システム最強の数式です。「期日の 3 営業日前にリマインドを送る」は WORKDAY(期日, -3, 祝日) と書くだけです。
NETWORKDAYS — 間の営業日数を数える
NETWORKDAYS(開始日, 終了日, [祝日]) は 2 つの日付の間の営業日数を数えます。両端の日付を含むのがポイントです。
=NETWORKDAYS("2026/07/06", "2026/07/10") → 5 (月〜金、全部営業日)
=NETWORKDAYS(A2, TODAY(), H2:H20) → A2 から今日までの経過営業日数
=NETWORKDAYS(EOMONTH(A2,-1)+1, EOMONTH(A2,0)) → A2 の月の稼働日数
典型的な用途は、SLA の経過日数(「この問い合わせはオープンから 4 営業日」)、給与計算(「今月の稼働日は 21 日」)、進捗ダッシュボードなど。開始日のほうが終了日より後ろの場合は負の値になります(Excel と同じ)。
WORKDAY との非対称に注意してください。NETWORKDAYS は起点日を含み、WORKDAY は含みません。つまり NETWORKDAYS(A, WORKDAY(A, n)) は n ではなく n + 1——自前実装のテストで定番のオフバイワンです。
祝日リスト — 1 つの範囲を全部の数式で使い回す
営業日系の 2 関数は、第 3 引数に「休みとして扱う日付の範囲」を渡せます。祝日と会社の休業日を 1 列に並べておき(非表示シートでも OK)、全部の数式からそこを参照します。
H2: 2026/01/01 元日
H3: 2026/07/20 海の日
H4: 2026/09/21 敬老の日
...
=WORKDAY(A2, 5, Holidays!H2:H30)
=NETWORKDAYS(A2, B2, Holidays!H2:H30)
この 1 列を保守することが、そのまま自社の営業日カレンダーになります。来年の祝日が発表されてもデータを足すだけ。再コンパイル不要——日付ルールはコードではなくデータとして持つ、の典型例です。
現時点の制限も 1 つ正直に書いておきます。週末は土曜・日曜に固定です。週末パターンを変えられる WORKDAY.INTL / NETWORKDAYS.INTL は V4.5 時点では未対応なので、変則的な定休日(例: 日曜+水曜休み)は祝日リスト側で表現してください。
この数式を C# アプリの中で動かす
ReoGrid は V4.5 でこの 4 関数を、DATE・WEEKDAY・WEEKNUM などの日付関数群と一緒に追加しました。ユーザーが Excel で書くのと同じ数式が、WinForms / WPF のスプレッドシートコントロール内でそのまま計算されます。Office も Interop も不要です。
請求書の支払期日シートをコンパクトに組むとこうなります。
using unvell.ReoGrid;
using unvell.ReoGrid.DataFormat;
var sheet = reoGridControl.CurrentWorksheet;
// ヘッダー
sheet["A1"] = new object[,] {
{ "請求日", "金額", "支払期日(翌月末)", "リマインド日", "残営業日" }
};
// 祝日リストは専用の列(または非表示シート)に置く
sheet["H1"] = "祝日";
sheet["H2"] = new DateTime(2026, 7, 20); // 海の日
sheet["H3"] = new DateTime(2026, 9, 21); // 敬老の日
// 請求 1 行分
sheet["A2"] = new DateTime(2026, 7, 7);
sheet["B2"] = 128000;
// 月末締め翌月末払い
sheet["C2"] = "=EOMONTH(A2, 1)";
// リマインド: 期日の 3 営業日前(祝日も飛ばす)
sheet["D2"] = "=WORKDAY(C2, -3, H2:H10)";
// 期日までの残営業日数
sheet["E2"] = "=NETWORKDAYS(TODAY(), C2, H2:H10)";
必ずやること: 結果セルに日付フォーマットを設定する
Excel と同じく、日付関数の戻り値はシリアル値(OLE オートメーション日付)です。セルを未フォーマットのままにすると、EOMONTH の結果は日付ではなく 46265 と表示されます。結果列に日付フォーマットを一度設定しておきます。
sheet.SetRangeDataFormat("A2:A100", CellDataFormatFlag.DateTime,
new DateTimeDataFormatter.DateTimeFormatArgs { Format = "yyyy/MM/dd" });
sheet.SetRangeDataFormat("C2:D100", CellDataFormatFlag.DateTime,
new DateTimeDataFormatter.DateTimeFormatArgs { Format = "yyyy/MM/dd" });
計算結果の日付を C# 側に取り出すときは、シリアル値を DateTime.FromOADate で変換します。
double serial = sheet.GetCellData<double>("C2");
DateTime dueDate = DateTime.FromOADate(serial); // 2026/08/31
請求書の日付を和暦(令和8年…)で見せたい場合も、データは DateTime のまま表示だけ切り替えられます。やり方は 和暦セルの記事 にまとめてあります。
ユーザーが C2 をクリックして数式を編集し、「この取引先は翌々月末払いだから」と 1 を 2 に変えれば、下流のリマインド日も残営業日もすべて再計算されます。日付ルールを C# の奥に埋めず、画面の数式として持つ意味はここにあります——支払条件が、見えて、編集できるデータになるのです。
まとめ — 落とし穴を 1 枚で
EDATE/EOMONTHは月単位、WORKDAY/NETWORKDAYSは営業日単位EDATEは移動先の月が短いと月末に丸める(1/31 + 1 か月 = 2/28)WORKDAYは起点日を含まない、NETWORKDAYSは両端を含む——テストで混同しない- 営業日系 2 関数は第 3 引数に祝日範囲を渡せる。祝日はコードでなくデータで持つ
- 週末は土日固定(
.INTL系は V4.5 時点で未対応) - 戻り値は日付のシリアル値——結果セルに
DateTimeフォーマットを設定し、C# で読むときはDateTime.FromOADateで変換 - ReoGrid V4.5 はこれらに対応。Office 不要で、WinForms / WPF アプリ内で同じ日付計算がそのまま動く
「探す」「集計する」に続いて「日付を計算する」を押さえれば、見積・請求・勤怠あたりの業務シートはほぼ数式だけで組めます。
ReoGrid 4.5 の新機能を見る / 30 日トライアルを試す
