はじめに 「月一覧生成」は“月次処理の背骨”になる
「指定期間の月ごとの集計」「月次レポート」「月別売上グラフ」
こういう“月単位”の処理をするときに土台になるのが「月一覧生成」です。
C# では、DateTime(または DateOnly)に AddMonths(1) を繰り返すことで、
シンプルに「年月の一覧」を作れます。
大事なのは、「どこからどこまで」「開始・終了を含むか」「月の代表日をどう持つか」を
きちんと決めておくことです。
ここでは、
基本の「月を1つずつ進める」パターンから、
期間指定・年跨ぎ・DateOnly 版・実務でのユーティリティ化まで、
初心者向けにかみ砕いて説明していきます。
基本:開始年月から終了年月までを1ヶ月ずつ列挙する
一番シンプルな「年月一覧」の作り方
まずは、「2024/01〜2024/12 のような月一覧が欲しい」という一番基本の形です。
using System;
using System.Collections.Generic;
public static class MonthListUtil
{
public static IEnumerable<DateTime> GetMonthList(DateTime from, DateTime to)
{
// 日付は1日にそろえる
DateTime current = new DateTime(from.Year, from.Month, 1);
DateTime end = new DateTime(to.Year, to.Month, 1);
if (end < current)
{
throw new ArgumentException("終了月は開始月以上である必要があります。");
}
while (current <= end)
{
yield return current;
current = current.AddMonths(1);
}
}
}
C#使い方の例です。
DateTime from = new DateTime(2024, 1, 15); // 日は15日でもOK
DateTime to = new DateTime(2024, 4, 3); // 日は3日でもOK
foreach (var m in MonthListUtil.GetMonthList(from, to))
{
Console.WriteLine(m.ToString("yyyy-MM"));
}
// 出力:
// 2024-01
// 2024-02
// 2024-03
// 2024-04
C#ここでの重要ポイントは次の2つです。
new DateTime(year, month, 1)で「その月の1日」にそろえていることcurrent <= endとして「開始月・終了月を含む」閉区間にしていること
「月一覧」は“年月の列”が欲しいだけなので、
その月の代表として「1日」を持つのが一番シンプルです。
年をまたぐ月一覧も同じロジックでOK
2023/11〜2024/03 のようなケース
上のユーティリティは、年をまたいでもそのまま使えます。
DateTime from = new DateTime(2023, 11, 10);
DateTime to = new DateTime(2024, 3, 5);
foreach (var m in MonthListUtil.GetMonthList(from, to))
{
Console.WriteLine(m.ToString("yyyy-MM"));
}
// 出力:
// 2023-11
// 2023-12
// 2024-01
// 2024-02
// 2024-03
C#AddMonths(1) は、年をまたぐ場合も自動で年を繰り上げてくれます。
「年が変わるときどうしよう」と悩む必要はありません。
.NET 6以降なら DateOnly 版も素直に書ける
「年月だけ扱いたい」なら DateOnly が相性抜群
日付だけを扱う DateOnly を使うと、
「その月の1日」を DateOnly で持つことができます。
using System;
using System.Collections.Generic;
public static class MonthListDateOnlyUtil
{
public static IEnumerable<DateOnly> GetMonthList(DateOnly from, DateOnly to)
{
DateOnly current = new DateOnly(from.Year, from.Month, 1);
DateOnly end = new DateOnly(to.Year, to.Month, 1);
if (end < current)
{
throw new ArgumentException("終了月は開始月以上である必要があります。");
}
while (current <= end)
{
yield return current;
current = current.AddMonths(1);
}
}
}
C#使い方の例です。
DateOnly from = new DateOnly(2024, 1, 15);
DateOnly to = new DateOnly(2024, 4, 3);
foreach (var m in MonthListDateOnlyUtil.GetMonthList(from, to))
{
Console.WriteLine(m); // 2024/01/01 など
}
C#DateOnly を使うと、「時刻をどうするか」を考えなくてよくなるので、
月次処理やカレンダー系のロジックではかなり扱いやすくなります。
応用1:月の開始日・終了日も一緒に持たせる
「その月の範囲」をすぐに使える形にする
実務では、「月一覧」だけでなく、
「その月の開始日と終了日」も一緒に欲しいことが多いです。
例えば、月次集計で「その月のデータだけ抽出したい」ときなどです。
小さな struct を作って、「月+開始日+終了日」をまとめて持たせると便利です。
public readonly struct MonthRange
{
public int Year { get; }
public int Month { get; }
public DateTime StartDate { get; }
public DateTime EndDate { get; }
public MonthRange(int year, int month)
{
Year = year;
Month = month;
StartDate = new DateTime(year, month, 1);
int daysInMonth = DateTime.DaysInMonth(year, month);
EndDate = new DateTime(year, month, daysInMonth);
}
public override string ToString() => $"{Year:D4}-{Month:D2}";
}
C#この MonthRange を一覧で返すユーティリティです。
public static IEnumerable<MonthRange> GetMonthRanges(DateTime from, DateTime to)
{
DateTime current = new DateTime(from.Year, from.Month, 1);
DateTime end = new DateTime(to.Year, to.Month, 1);
if (end < current)
{
throw new ArgumentException("終了月は開始月以上である必要があります。");
}
while (current <= end)
{
yield return new MonthRange(current.Year, current.Month);
current = current.AddMonths(1);
}
}
C#使い方の例です。
DateTime from = new DateTime(2024, 1, 10);
DateTime to = new DateTime(2024, 3, 5);
foreach (var mr in GetMonthRanges(from, to))
{
Console.WriteLine($"{mr}: {mr.StartDate:yyyy-MM-dd} ~ {mr.EndDate:yyyy-MM-dd}");
}
// 出力イメージ:
// 2024-01: 2024-01-01 ~ 2024-01-31
// 2024-02: 2024-02-01 ~ 2024-02-29
// 2024-03: 2024-03-01 ~ 2024-03-31
C#ここでの重要ポイントは、
「月の開始日・終了日を MonthRange の中に閉じ込めている」ことです。
うるう年の2月なども DaysInMonth が正しく計算してくれるので、
呼び出し側は「月を回す」ことだけ考えればよくなります。
応用2:件数指定で「直近Nヶ月」を生成する
「直近12ヶ月分のグラフ」などのよくある要件
「今日を基準に直近6ヶ月」「今月を含めた直近12ヶ月」
といった要件もよく出てきます。
public static IEnumerable<DateTime> GetRecentMonths(int count, DateTime? baseDate = null)
{
if (count <= 0)
{
throw new ArgumentOutOfRangeException(nameof(count));
}
DateTime baseMonth = (baseDate ?? DateTime.Today);
baseMonth = new DateTime(baseMonth.Year, baseMonth.Month, 1);
// 過去にさかのぼる
for (int i = count - 1; i >= 0; i--)
{
yield return baseMonth.AddMonths(-i);
}
}
C#使い方の例です。
foreach (var m in GetRecentMonths(6))
{
Console.WriteLine(m.ToString("yyyy-MM"));
}
C#このようにしておくと、
「直近Nヶ月」のロジックを一箇所にまとめられます。
実務での注意点1:開始・終了の順序チェック
「終了月が開始月より前」の不正な入力
ユーザー入力や外部データから期間を受け取る場合、
「終了月が開始月より前」という不正な範囲が紛れ込むことがあります。
先ほどの GetMonthList では、end < current のときに ArgumentException を投げるようにしました。
if (end < current)
{
throw new ArgumentException("終了月は開始月以上である必要があります。");
}
C#ここをどう扱うかは、システムの方針次第です。
- 例外にして「入力がおかしい」と早めに気づく
- 自動的に入れ替えてしまう(from/to を swap)
個人的には、「入力ミスを早く見つけたい」場面では例外にするほうが安全です。
実務での注意点2:日付をそのまま渡して月を比較してしまう
「2024/01/31 から 2024/02/01 まで」のような境界
月一覧生成では、「日付」ではなく「年月」を比較したいのに、
そのまま DateTime を比較してしまうと、意図しない挙動になることがあります。
例えば、こういうコードは危険です。
public static IEnumerable<DateTime> BadGetMonthList(DateTime from, DateTime to)
{
DateTime current = from;
while (current <= to)
{
yield return current;
current = current.AddMonths(1);
}
}
C#from が 2024/01/31 の場合、AddMonths(1) は 2024/02/29(うるう年)になり、
さらに AddMonths(1) すると 2024/03/29… と、
「月末基準でズレた日付」が続いてしまいます。
月一覧を作るときは、
必ず「その月の1日」にそろえてから AddMonths(1) する、
というルールを徹底しましょう。
実務での注意点3:タイムゾーンは基本的に関係ないが「日付の解釈」は意識する
月一覧は「カレンダーの世界」の話
月一覧生成は、「カレンダー上の年月」の話なので、
通常はタイムゾーンを意識する必要はあまりありません。
ただし、「どの国のカレンダーか」は意識しておくべきです。
サーバー内部で UTC を使っていても、
「ユーザーにとっての“2024年2月”」はユーザーのタイムゾーンでのカレンダーです。
実務的には、
- ユーザーのタイムゾーンで「今日」を決める
- その「今日」から年月を取り出して月一覧を作る
という流れにしておくと、
「日付が1日ズレる」といった事故を防ぎやすくなります。
まとめ 「月一覧生成ユーティリティ」は“月次処理の共通エンジン”
月一覧生成は、一見ただの AddMonths(1) の繰り返しですが、
開始・終了の扱い、年跨ぎ、月の代表日、月の範囲、直近Nヶ月など、
月次処理のエッセンスが詰まっています。
押さえておきたいポイントを整理すると、こうなります。
- 基本形は「開始月・終了月の1日を基準にして、
AddMonths(1)で回す」 - 年をまたぐ場合も
AddMonths(1)が自動で処理してくれるので、そのまま使える - 月の開始日・終了日を一緒に持つ
MonthRangeのような型を用意すると、月次集計ロジックが書きやすくなる - 「直近Nヶ月」などのよくある要件は、専用ユーティリティにしておくと再利用性が高い
- 開始・終了の順序チェックと、「その月の1日にそろえる」ルールを徹底することで、境界のバグを防げる
ここを押さえておけば、
「その場しのぎで AddMonths を書いている」状態から一歩進んで、
“月次処理やレポートの土台として安心して使える、実務で使える月一覧生成ユーティリティ”を
自分の C# コードの中に気持ちよく組み込めるようになります。
