みなさんこんにちは、ヒロポンです!
C# の日付計算って、地味に事故りませんか??
請求の締め日。営業日ベースの納期。月末の締め処理。
この辺を自前で書き始めた瞬間、「あれ、1ヶ月後っていつだ」「土日ってどう飛ばすんだ」で手が止まる。私も止まりました。
今日は、その日付計算を業務で踏みがちな3パターンに絞ります。全部コピペで動くコードつき。営業日計算まで通してやるので、締め処理を書く前にサッと拾っていってください。
💡 日付の表示・書式(
yyyy/MM/ddやログ用フォーマット)は別記事 C# DateTime のフォーマット完全早見表 にまとめてます。今回は計算のほうに絞った内容です。
結論: C# の日付計算は AddMonths・TimeSpan・営業日ループの3つを型で押さえる
先に答えから書きます。
- Nヶ月後・N日後 →
AddMonths/AddDays。ただし月末またぎで日がずれる - 2つの日付の差 → 引き算で
TimeSpan。ただし出るのは「日」まで。月・年は自前計算 - 営業日(土日祝スキップ) → 1日ずつ進めて土日と祝日を飛ばすループ。祝日はマスタで持つ
この3つを「型」として持っておくと、締め処理のたびに悩まなくて済みます!!
以下、順番に動くコードで見ていきます。
なぜ日付計算は「ちょっとずれる」のか
日付がやっかいなのは、桁の繰り上がりが月ごとにバラバラだから。
秒や分は60、時は24で固定。ところが日は28〜31日でブレるし、うるう年まである。
この「繰り上がりが不定」なところを、DateTime は裏でいい感じに吸収してくれています。
その吸収のしかたが、たまに直感とズレる。これがズレの正体です。
代表が2つ。ひとつは AddMonths の月末補正。もうひとつは TimeSpan に「月」という単位が無いこと。
この2つを知ってるかどうかで、締め処理の安定感がまるで変わります。
パターン1: N日後・Nヶ月後を出す(AddDays / AddMonths)
まずは一番よく使う「◯日後・◯ヶ月後」から。
using System;
class Program
{
static void Main()
{
var today = new DateTime(2026, 7, 11);
// N日後・N日前(マイナスで前に戻る)
Console.WriteLine(today.AddDays(10).ToString("yyyy/MM/dd")); // 2026/07/21
Console.WriteLine(today.AddDays(-3).ToString("yyyy/MM/dd")); // 2026/07/08
// Nヶ月後・N年後
Console.WriteLine(today.AddMonths(2).ToString("yyyy/MM/dd")); // 2026/09/11
Console.WriteLine(today.AddYears(1).ToString("yyyy/MM/dd")); // 2027/07/11
}
}
こんな感じで、AddDays / AddMonths / AddYears は素直に足してくれます。マイナスを渡せばそのまま「◯前」になるので、Subtract を別で覚える必要はないです。
問題はここから。月末を起点にするとズレます。
using System;
class Program
{
static void Main()
{
var end = new DateTime(2026, 1, 31); // 1月末
// ん? 1ヶ月後は 2月31日…? と思いきや
Console.WriteLine(end.AddMonths(1).ToString("yyyy/MM/dd")); // 2026/02/28
// うるう年だと 2月29日 に補正される
var end2024 = new DateTime(2024, 1, 31);
Console.WriteLine(end2024.AddMonths(1).ToString("yyyy/MM/dd")); // 2024/02/29
}
}

存在しない「2月31日」を、その月の最終有効日(28日か29日)に自動で丸めてくれるんですね。
これ自体は親切なんですが、締め日を末日で持ってると、ここでしれっと狂います。
パターン2: 2つの日付の差を出す(TimeSpan と、月数・年齢)
次は「AとBの間、何日空いてる?」。これは引き算一発です。
using System;
class Program
{
static void Main()
{
var start = new DateTime(2026, 7, 1);
var end = new DateTime(2026, 7, 11);
TimeSpan diff = end - start;
Console.WriteLine(diff.Days); // 10 … 丸ごと何日か
Console.WriteLine(diff.TotalDays); // 10 … 端数込みの合計日数(double)
Console.WriteLine(diff.TotalHours); // 240 … 時間換算もいける
}
}
いい感じに差が出ます。Days が「まるっと何日」、TotalDays が「端数込みの合計」。時間や分で欲しければ TotalHours / TotalMinutes もあります。
で、ここで多くの人が詰まる。「じゃあ何ヶ月・何年離れてるの??」ってやつです。
TimeSpan が持ってる一番大きい単位は Days。Months も Years も存在しません。
なので月数や年齢は自分で計算します。
using System;
class Program
{
static void Main()
{
// 満月数(丸々何ヶ月経ったか)
var from = new DateTime(2026, 1, 15);
var to = new DateTime(2026, 7, 11);
int months = (to.Year - from.Year) * 12 + (to.Month - from.Month);
if (to.Day < from.Day) months--; // 日数が届いてなければ1ヶ月引く
Console.WriteLine(months); // 5
// 年齢(誕生日が来てなければ1引く)
var birth = new DateTime(1990, 12, 20);
var basis = new DateTime(2026, 7, 11);
int age = basis.Year - birth.Year;
if (birth.Date > basis.AddYears(-age)) age--;
Console.WriteLine(age); // 35
}
}

年齢計算はコピペ用に置いときます。(今年 − 誕生年) を出して、今年の誕生日がまだ来てなければ1引く。これが定石です。
うるう年生まれ(2月29日)も、この書き方なら破綻しません。
パターン3: 土日祝を飛ばす営業日計算
本題の営業日です。「3営業日後が納期」みたいなやつ。
やりがちなのが AddDays(3) で済ませてしまうこと。土日を無視して単純に3日足すので、金曜起点だと月曜じゃなく日曜を返してきます。
正しくは、1日ずつ進めて土日と祝日を飛ばす。
using System;
using System.Collections.Generic;
class Program
{
// 祝日はマスタで持つ(ここではサンプルで直書き)
static readonly HashSet<DateTime> Holidays = new HashSet<DateTime>
{
new DateTime(2026, 7, 20), // 海の日
};
// days 営業日後を返す(土日 + 祝日をスキップ)
static DateTime AddBusinessDays(DateTime start, int days)
{
DateTime d = start;
int added = 0;
while (added < days)
{
d = d.AddDays(1);
if (d.DayOfWeek == DayOfWeek.Saturday) continue;
if (d.DayOfWeek == DayOfWeek.Sunday) continue;
if (Holidays.Contains(d.Date)) continue;
added++;
}
return d;
}
static void Main()
{
var friday = new DateTime(2026, 7, 17); // 金曜
// 翌営業日: 土日(18,19)と海の日(20)を飛ばして火曜へ
Console.WriteLine(AddBusinessDays(friday, 1).ToString("yyyy/MM/dd")); // 2026/07/21 (火)
// 3営業日後: 21(火)→22(水)→23(木)
Console.WriteLine(AddBusinessDays(friday, 3).ToString("yyyy/MM/dd")); // 2026/07/23 (木)
}
}

こんな感じで営業日を数えます。ポイントは DayOfWeek で土日を判定しつつ、祝日は HashSet に問い合わせているところ。
祝日を飛ばしたぶんループが余分に回るので、added は土日祝じゃなかった時だけ増やす。ここが肝です。
土日と祝日をひとつの if にまとめても動きます。ただ、あとで「祝日だけ無効化したい」みたいな要望が来た時に、バラしておくと楽です。
同じ発想を before / after で並べるとこうなります。

3パターンの使い分け
3つを一枚に並べておきます。「何をしたいか」から逆引きで使ってください。

「差」は日までしか直接出ない。「月末」は勝手に補正が入る。
この2つの「—」じゃない部分が、そのままハマりポイントになります。
ハマりポイント: 知らないと締めがずれる3つ
私が実際にやられた or 現場で見たやつを3つ。時間を溶かす順に並べます。
① AddMonths の月末補正は「往復で戻らない」
さっきの 1/31 + 1ヶ月 = 2/28。ここまでは知ってる人も多い。
本当の罠は「往復しても元に戻らない」ところです。
using System;
class Program
{
static void Main()
{
var d = new DateTime(2026, 1, 31);
// +1ヶ月してから -1ヶ月しても、1/31 には戻らない
Console.WriteLine(d.AddMonths(1).AddMonths(-1).ToString("yyyy/MM/dd")); // 2026/01/28
// さらに: 2回足すのと、1回ずつ足すのも結果が違う
Console.WriteLine(d.AddMonths(2).ToString("yyyy/MM/dd")); // 2026/03/31
Console.WriteLine(d.AddMonths(1).AddMonths(1).ToString("yyyy/MM/dd")); // 2026/03/28
}
}

一度2月28日に丸められると、戻る時は「28日」が起点になる。だから +1 → -1 で1/28に着地します。
締め日を「先月末+1ヶ月」でループ生成してると、月を追うごとに日がジワジワ手前にズレていく。地味にタチが悪いやつです。
- 症状: 月次の締め日が月を追うごとに前倒しになる
- 原因:
AddMonthsの月末補正が非可逆(一度丸めると戻せない) - 対処: 末日が欲しいなら足し算せず、
new DateTime(y, m, DateTime.DaysInMonth(y, m))で毎回作り直す - ⏱ 目安: 気づくまで半日、直すのは10分
② TimeSpan に .Months .Years は無い
これはコンパイルエラーにならないぶん、たちが悪いです。
(end - start).Days は書けるので、つい .Months もあると思って IntelliSense を探す。でも、無い。
TimeSpan の最大単位は日。そう割り切って、月・年は自前計算に切り替えるのが早いです(パターン2のコード参照)。
- 症状: 月数・年数を出そうとして
.Monthsを探し続ける - 原因:
TimeSpanはそもそも「暦」を知らない(ただの時間量) - 対処: 月数は
Year * 12 + Monthの差、年齢は誕生日到達チェックで自前算出 - ⏱ 目安: 探し回って15〜30分
③ 営業日を AddDays で済ませる
「3営業日後」を AddDays(3) で出してしまうやつ。金曜起点だと日曜を返してきて、納期表示がそのまま休日になります。
- 症状: 納期・締め日が土日祝に着地する
- 原因:
AddDaysは曜日を区別しない - 対処: 1日ずつ進めて土日祝をスキップするループにする(パターン3)
- ⏱ 目安: テスト工程まで気づかないと、手戻り数時間
現場メモ: 締め処理で日付を溶かさないために
正直に書くと、①の月末補正で青ざめたのは私です。
数年前、流通系のSIerで月末の請求締めを回してた時期がありました。締め日を「基準月の末日」で持って、翌月分を AddMonths(1) で生成してたんですよ。
テスト環境では月初起点のデータしか流してなくて、素通り。「よし、締め処理いける、完了!」ってなってました。
で、本番で1月末起点のデータが来た瞬間、締め日が2月28日にズレて、月をまたぐたびに手前へ手前へ動いていった。
原因に気づくまで、ログとにらめっこで半日。背中に冷や汗かきながら業務側に頭を下げに行って、そこからの信頼回復にもう半日。技術的な復旧より、そっちがしんどかったです。
今だったら「日付は足し算じゃなく末日を作り直せ」で一瞬やん?って思いますよ?
でも当時は、AddMonths に末日を渡せばそのまま末日が返ってくると信じ切ってたんですよね。これを覚えてからは、締め処理が一気に安定しました。
祝日についてもひとつだけ。日本の祝日は式で出そうとしないほうがいいです。
ハッピーマンデーで第2月曜がどうとか、振替休日、春分・秋分にいたっては天文計算。ここを頑張るくらいなら、HashSet か DB の祝日マスタで持って、年1回メンテするほうが圧倒的に楽で正確です。
余談ですが、DateOnly という日付専用の型が .NET 6 以降にはあって、時刻を持たないぶん締め日計算はスッキリします。ただ現場が .NET Framework 4.7.2 だと使えない。その場合は今回みたいに DateTime の .Date(時刻を切り落とした部分)で日付だけ扱えばOKです。
まとめ
C# の日付計算は、この3つを型で持っておけば締め処理で迷いません。
- Nヶ月後・N日後は
AddMonths/AddDays。ただし月末起点は補正で日がズレる(末日はDaysInMonthで作り直す) - 差は引き算で
TimeSpan。出るのは日まで、月・年は自前計算 - 営業日は1日ずつ進めて土日祝をスキップ。祝日はマスタ管理が現実的
足し算で押し切ろうとすると、月末とうるう年と土日が全部まとめて牙をむいてきます。
ここだけコピペで型にしておくと、締めのたびに悩まなくて済むのでラクですよ。
よくある質問
AddMonths で月末が翌月にずれるのはバグですか?
バグではなく仕様です。1月31日に1ヶ月足すと存在しない「2月31日」になるため、その月の最終有効日(2月28日か29日)に自動補正されます。末日を厳密に扱いたい場合は、足し算ではなく new DateTime(y, m, DateTime.DaysInMonth(y, m)) で末日を作り直してください。
2つの日付が何ヶ月・何年離れているか、TimeSpan で一発で出せますか?
出せません。TimeSpan が持つ最大の単位は Days(日)で、Months や Years はありません。月数は (Year差 × 12 + Month差)、年齢は誕生日が到達したかのチェックで、それぞれ自分で計算します。
5営業日後を AddDays(5) で出してはいけないのはなぜですか?
AddDays は土日祝を区別せず単純に5日足すためです。金曜起点だと日曜を返してしまい、納期が休日に着地します。営業日を数えるには、1日ずつ進めて土日と祝日をスキップするループが必要です。
日本の祝日は計算式で求められますか?
ハッピーマンデー・振替休日・春分秋分(天文計算)が絡むため、単純な式では正確に出せません。実務では祝日マスタ(HashSet や DB テーブル)で管理し、年1回更新するのが現実的です。
DateTime と DateOnly、どちらを使えばいい?
時刻が不要な締め日・営業日の計算なら DateOnly(.NET 6 以降)のほうがスッキリします。ただし .NET Framework 4.7.2 など旧環境では使えないので、その場合は DateTime を .Date で日付だけに揃えて扱えば同じことができます。
次に読むべき記事




以上!
同じ月末の罠でハマってる人いたら、この記事シェアしてくれると嬉しいです!!
執筆者
バイブス父さん — 業務 SE 7 年(SIer 正社員 2 / フリーランス 5)。現職は SEO 直轄部の AI アドバイザー兼 PL、副業で中小 SIer の CTO。SIer の正社員からフリーランスに転じ、複数のエージェント経由で案件を回してきた経験ベースで「業務 SE 視点」の技術 + キャリア記事を書いています。
🐦 X: @hiro_progra0524(日々の現場メモ更新中)
📝 About Me で経歴詳細を見る


