C# Tips | 日付・時間処理:月一覧生成

C# C#
スポンサーリンク

はじめに 「月一覧生成」は“月次処理の背骨”になる

「指定期間の月ごとの集計」「月次レポート」「月別売上グラフ」
こういう“月単位”の処理をするときに土台になるのが「月一覧生成」です。

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# コードの中に気持ちよく組み込めるようになります。

タイトルとURLをコピーしました