C#のDateTime型のAddMonthsで、月末の日から月を加算した時に、加算後も月末となるように月を加算する

# はじめに

DateTimeのAddMonthsで月末の日に月を加算すると、
* 加算後の日付が月内を超える場合は月末の日に丸められ、(1/31(月末)→2/28(丸め)等)
* 月内を超えない場合は加算前と同日を維持するように、(2/28(月末)→3/28(同日、非月末)等)

月が加算されます。

C#のDateTime型のAddMonthsの動作を実測した結果：

https://qiita.com/dl_from_scratch/items/8687f71c94e904f60735

ここでは、C#のDateTime型のAddMonthsに代えて、**月末の日から月を加算した時に、加算後も月末となる**ように月を加算する関数を自作してみます。

# 処理方法
* ExtensionMethodsとして、`AddMonths`関数を自作。
  * `public static DateTime AddMonths(this DateTime t1, int n_M, bool keep_last = false)`
* 引数`keep_last`に`true`を指定すると、月末の日から月を加算した時に、加算後も月末となる。
* 1日進めることで月末かどうかを判定して、(`t1.AddDays(1).Day == 1`)、月末の時は、1日進めた状態で月を加算し、最後に1日戻す。(`t1.AddDays(1).AddMonths(n_M).AddDays(-1)`)

コード: 
```C#
        // 月を加算(月末の日であれば加算後も月末となるような加算)
        public static DateTime AddMonths(this DateTime t1, int n_M,
            bool keep_last = true) { // T:月末の日であれば次の月末までを1ヵ月として月数を算出

            if (!keep_last)
                return t1.AddMonths(n_M); // 標準の月加算

            var t_1d = t1.AddDays(1);
            var is_last = t_1d.Day == 1; // 月末か?
            if (!is_last)
                return t1.AddMonths(n_M); // 標準の月加算

            // if: 月末の時

            return t_1d.AddMonths(n_M).AddDays(-1); // 月末を維持して月加算
        }
```

# テストコード
```C#:Program 2301-2 AddMonthsの月末丸め動作テスト.cs
using System;

namespace ConsoleApp1 {
    class Program {
        static void Main(string[] args) {

            // sec: DateTimeのAddMonths

            /* 結果: 
DateTimeのAddMonthsで月末の日にNヵ月を加算すると、
加算後の日付が月内を超える場合は月末の日に丸められ(1/31(月末)→2/28(丸め)等)、
月内を超えない場合は加算前と同日を維持する(2/28(月末)→3/28(同日、非月末)等)

2023/02/28 0:00:00
2023/03/31 0:00:00
2023/03/28 0:00:00
2023/05/30 0:00:00
2023/05/31 0:00:00
            */

            Func<string, DateTime> DT = DateTime.Parse;
            Console.WriteLine($"{DT("2023/01/31").AddMonths(1)}");
            Console.WriteLine($"{DT("2023/01/31").AddMonths(2)}");
            Console.WriteLine($"{DT("2023/02/28").AddMonths(1)}");
            Console.WriteLine($"{DT("2023/04/30").AddMonths(1)}");
            Console.WriteLine($"{DT("2023/01/31").AddMonths(4)}");

            // sec: AddMonthsを拡張

            /* 結果: 
月末の日であれば加算後も月末となるように月を加算(1/31(月末)→2/28(丸め)、2/28(月末)→3/31(月末)等)

2023/02/28 0:00:00
2023/03/31 0:00:00
2023/03/31 0:00:00
2023/05/31 0:00:00
2023/05/31 0:00:00
            */

            Console.WriteLine($"{DT("2023/01/31").AddMonths(1, keep_last: true)}");
            Console.WriteLine($"{DT("2023/01/31").AddMonths(2, keep_last: true)}");
            Console.WriteLine($"{DT("2023/02/28").AddMonths(1, keep_last: true)}");
            Console.WriteLine($"{DT("2023/04/30").AddMonths(1, keep_last: true)}");
            Console.WriteLine($"{DT("2023/01/31").AddMonths(4, keep_last: true)}");
        }
    }

    public static class ExtensionMethods {

        // 月を加算(月末の日であれば加算後も月末となるような加算)
        public static DateTime AddMonths(this DateTime t1, int n_M, 
            bool keep_last = false) { // T:月末の日であれば月加算後も月末となる

            if (!keep_last)
                return t1.AddMonths(n_M); // 標準の月加算

            var t_1d = t1.AddDays(1);
            var is_last = t_1d.Day == 1; // 月末か?
            if (!is_last)
                return t1.AddMonths(n_M); // 標準の月加算

            // if: 月末の時

            return t_1d.AddMonths(n_M).AddDays(-1); // 月末を維持して月加算
        }
    }
}
```

# 環境
Microsoft Visual Studio Community 2019 Version 16.11.22
VisualStudio.16.Release
Microsoft .NET Framework
Version 4.8.04084
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/606924/a80b2b96-ebec-1c6b-a5a2-700f34441206.png)