C#で年月(yyyyMM)を扱う|月初・月末・○か月後を簡単に求める

C#・.NET

「対象年月」をどう持つか、意外と迷う

経費精算や売上の集計のような業務システムでは、「2026年10月分」のように年月の単位でデータを扱うことがよくあります。

年月だけを持ちたいのに、DateTimeだと日や時刻まで付いてきて扱いにくい

C#には、年と月だけを表す型がありません。そこで立替精算システムでは、年月を「202610」のような6桁の文字列(yyyyMM)で持ち、計算するときだけ日付に直す小さな部品を作っています。

本記事では、その部品を使って、月初・月末・先月・○か月後・日付が年月に入っているかを簡単に求める方法を紹介します。

立替精算システムで使ったところ

私が自作した立替精算システムでは、申請を「対象年月」ごとにまとめています。締め処理・仕訳データの出力・集計も、対象年月の単位で行います。

立替精算システムの締め処理の画面。対象年月ごとに申請の件数と合計金額、締めの状態が並んでいる

締め処理の画面です。対象年月ごとに申請の件数と合計金額を出し、月ごとに締めます。上に「今月は2026年10月、入力できる月は2026年9月〜2026年10月」と出しているのも、この部品で求めています。

立替精算システムの申請入力の画面。入力できる月と、申請目的・明細の入力欄がある

申請の入力画面です。明細の日付が申請の年月に入っていない場合は、印を付けて上長や経理に分かるようにしています。

年月を6桁の文字列で持つ理由

持ち方よいところ困るところ
“202610”(6桁の文字列)並べ替えると年月の順になる。データベースの種類に関係なく同じ形で保存できる。見て分かる計算するときは日付に直す必要がある
202610(数値)並べ替えられるそのまま足し算すると 202612+1=202613 になる
DateTime・DateOnly(その月の1日)日付の計算がそのままできる「年月」なのか「その日」なのかが分かりにくい

立替精算システムはSQLite・PostgreSQL・SQL Serverのどれでも動くように設計している為、データベースごとの日付の型の違いに左右されない、6桁の文字列にしています。

C#で年月(yyyyMM)を扱うサンプルコード

動作確認は Windows 11・.NET 10 です(.NET 8 以降で動きます)。Visual Studio でコンソールアプリを作り、Program.cs に貼り付けて実行してください。.NET 10 なら、ファイルに保存して dotnet run ファイル名.cs でも実行できます。

using System.Globalization;

// 年月(yyyyMM)を扱う
var kyou = new DateOnly(2026, 10, 25);
var tougetsu = Nengetsu.From(kyou);
Console.WriteLine($"今日            :{kyou:yyyy/MM/dd}");
Console.WriteLine($"今月            :{tougetsu}({Nengetsu.ToHyouji(tougetsu)})");
Console.WriteLine($"先月            :{Nengetsu.AddMonths(tougetsu, -1)}");
Console.WriteLine($"3 か月後        :{Nengetsu.AddMonths(tougetsu, 3)}(年をまたぐ)");
Console.WriteLine($"今月の月初      :{Nengetsu.Shonichi(tougetsu):yyyy/MM/dd}");
Console.WriteLine($"今月の月末      :{Nengetsu.Matsujitsu(tougetsu):yyyy/MM/dd}");
Console.WriteLine($"2028年2月の月末 :{Nengetsu.Matsujitsu("202802"):yyyy/MM/dd}(うるう年)");
Console.WriteLine();

// 明細の日付が、申請の年月に入っているか
foreach (var hiduke in new[] { new DateOnly(2026, 10, 1), new DateOnly(2026, 10, 31), new DateOnly(2026, 9, 30) })
{
    Console.WriteLine($"{hiduke:yyyy/MM/dd} は {Nengetsu.ToHyouji(tougetsu)} に入る? {Nengetsu.Contains(tougetsu, hiduke)}");
}
Console.WriteLine();

// 入力できる月の範囲(今月から 1 か月さかのぼれる場合)
var kaishi = Nengetsu.AddMonths(tougetsu, -1);
Console.WriteLine($"入力できる月    :{Nengetsu.ToHyouji(kaishi)} 〜 {Nengetsu.ToHyouji(tougetsu)}");
Console.WriteLine();

// 正しい年月か
foreach (var s in new[] { "202610", "202613", "2026/10", "20261" })
{
    Console.WriteLine($"\"{s}\" は正しい年月? {Nengetsu.IsValid(s)}");
}


/// <summary>年月(yyyyMM の 6 桁の文字列)を扱う。</summary>
public static class Nengetsu
{
    public const string Format = "yyyyMM";

    /// <summary>日付から年月を作る。</summary>
    public static string From(DateOnly hiduke) =>
        hiduke.ToString(Format, CultureInfo.InvariantCulture);

    /// <summary>正しい年月か。</summary>
    public static bool IsValid(string? nengetsu) =>
        nengetsu is { Length: 6 }
        && DateOnly.TryParseExact(
            nengetsu + "01", "yyyyMMdd", CultureInfo.InvariantCulture,
            DateTimeStyles.None, out _);

    /// <summary>その年月の初日。</summary>
    public static DateOnly Shonichi(string nengetsu) =>
        DateOnly.ParseExact(nengetsu + "01", "yyyyMMdd", CultureInfo.InvariantCulture);

    /// <summary>その年月の末日(次の月の初日の前の日)。</summary>
    public static DateOnly Matsujitsu(string nengetsu) =>
        Shonichi(nengetsu).AddMonths(1).AddDays(-1);

    /// <summary>日付がその年月に入っているか。</summary>
    public static bool Contains(string nengetsu, DateOnly hiduke) =>
        hiduke >= Shonichi(nengetsu) && hiduke <= Matsujitsu(nengetsu);

    /// <summary>○か月後(マイナスなら前)の年月。</summary>
    public static string AddMonths(string nengetsu, int months) =>
        From(Shonichi(nengetsu).AddMonths(months));

    /// <summary>画面に出す形(2026年10月)。</summary>
    public static string ToHyouji(string nengetsu)
    {
        if (!IsValid(nengetsu))
        {
            return nengetsu ?? string.Empty;
        }

        var shonichi = Shonichi(nengetsu);
        return $"{shonichi.Year}年{shonichi.Month}月";
    }
}

実行結果

今日            :2026/10/25
今月            :202610(2026年10月)
先月            :202609
3 か月後        :202701(年をまたぐ)
今月の月初      :2026/10/01
今月の月末      :2026/10/31
2028年2月の月末 :2028/02/29(うるう年)

2026/10/01 は 2026年10月 に入る? True
2026/10/31 は 2026年10月 に入る? True
2026/09/30 は 2026年10月 に入る? False

入力できる月    :2026年9月 〜 2026年10月

"202610" は正しい年月? True
"202613" は正しい年月? False
"2026/10" は正しい年月? False
"20261" は正しい年月? False

12月をまたぐ「3か月後」や、うるう年の2月の月末も正しく求められています。

コードの説明

  • Shonichi は、年月の後ろに “01” を付けて、その月の1日の DateOnly に直します。ParseExact で形を決めて読むので、”2026/10″ のような違う形は受け付けません。
  • Matsujitsu は、月末を「次の月の1日の前の日」で求めます。月によって28日・29日・30日・31日と変わる日数を自分で数えずに済み、うるう年も正しく出ます。
  • AddMonths は、一度日付に直してから AddMonths で月を進め、また6桁に戻します。12月の次は翌年の1月になります。
  • CultureInfo.InvariantCulture は、PCの地域の設定(カレンダーを和暦にしている場合など)に左右されずに、いつも西暦の “yyyyMM” で読み書きするために指定しています。
  • IsValid は、TryParseExact で読めるかどうかで、”202613″(13月)のような正しくない年月をはじきます。

入力できる月の範囲を求める

立替精算システムでは、「今月から何か月さかのぼって入力できるか」をシステム設定で決められます。範囲は AddMonths を使うだけで求められます。

var tougetsu = Nengetsu.From(clock.Today);            // 今月(202610)
var kaishi = Nengetsu.AddMonths(tougetsu, -sakanoboru); // さかのぼれる一番古い月(202609)

// 6 桁の文字列は、そのまま比べても年月の順になる
var hairu = string.CompareOrdinal(nengetsu, kaishi) >= 0
         && string.CompareOrdinal(nengetsu, tougetsu) <= 0;

6桁の文字列は、文字として比べても年月の順になります。「申請の年月が範囲に入っているか」は、string.CompareOrdinal で比べるだけで判定できます。今日の日付(clock.Today)の取り方は、日本時間(JST)を取得する記事で紹介しています。

注意点

  • 6桁の文字列で持つときは、保存する前に必ず IsValid で確かめてください。”2026/10″ や “20261” が混ざると、並べ替えや比較が正しくなくなります。
  • 文字列の大小を比べるときは string.CompareOrdinal を使ってください。CompareTo はPCの地域の設定によって比べ方が変わることがあります。
  • 4月始まりの会計年度などで集計するときは、年月から年度を求める処理を別に用意してください。

最後に

以上が、C#で年月(yyyyMM)を扱う方法です。

月初・月末・○か月後の計算は、DateOnly に直して AddMonths・AddDays を使えば、うるう年や年またぎを気にせずに済みます。小さな部品にまとめておくと、締め処理・集計・入力チェックのどこでも同じ書き方で使えます。

この部品を使っている立替精算システムは、こちらで紹介しています。

Pythonで日付や時刻を指定した形で取得する方法は、こちらで紹介しています。

コメント

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