2つの日時の差を、小数有りの月数で算出 (月数の小数部分は、1ヵ月を固定31日として算出)

# はじめに

C#で、DateTime型の差を求めると、TimeSpan型が得られて、
　TimeSpan型 ← DateTime型 - DateTime型
TimeSpan型の**TotalDays**や、**TotalHours**、TotalMinutes、TotalSeconds、TotalMillisecondsで、**小数有りの日数**や**時間数**が得られるのに対して、

月数は、1ヵ月の長さが年月によって変わるため(28～31日)、**小数有りの月数**が得られません。
(同様に、年数は、1年の長さが閏年によって変わるため(365～366日)、小数有りの年数が得られません。)

ここでは、2つの日時の差を、年月に応じた月数を小数有りで算出する関数を自作してみます。

# 処理方法

* 関数名DiffMonthsAsFloatで、2つの日時の差を月数(小数有り)で算出。
* 2つの日時の差を、年月に応じた月数で、かつ、小数有りで算出する関数。月数の小数部分は、1ヵ月を固定31日として算出。
  * 月数を小数有りで算出する為、小数部分は1ヵ月を月日数の最大値である31日として算出。小数部を31で掛けると、1ヵ月以下の端数となる日数が計算できる。
* t1 - t2を考えて、単純な月表示部での月数の差をまず計算、t2から月表示の差だけまず進める。これがt1を過ぎる場合は、1ヵ月繰り下げて、整数部分の月数とする。月数の小数部分は、1ヵ月を固定31日として、単純に日数を31で割って、月数の小数部を決める。

当処理部分のコード抜粋: 
```C#
        // 2つの日時の差を月数(小数有り)で算出 (小数部分は1ヵ月を固定31日として算出)
        public static double DiffMonthsAsFloat(this DateTime t1, DateTime t2, // t1 - t2
            bool keep_last = true) { // T:月末の日であれば次の月末までを1ヵ月として月数を算出
            // assume: 月数を小数有りで算出する為、小数部分は1ヵ月を月日数の最大値である31日として算出
            // note: 小数部を31で掛けると、1ヵ月以下の端数となる日数が計算できる
            // note: 1ヵ月が固定31日(最大値)の為、"1 - 当月数"等の引き算で差を取る事は不適

            var n_M = t1.DiffMonthsAsSeen(t2); // 単純な月表示の差(整数)
            var t2_M = t2.AddMonths(n_M, keep_last); // 月表示の差だけまず進める
            if (t1 < t2_M) { // if: t1を過ぎた
                n_M -= 1; // 1ヵ月繰り下げの分
                t2_M = t2.AddMonths(n_M, keep_last); // 再計算 // NOTE: 月末の日を考慮するので-1の減算は不可
            }
            var t_D = (t1 - t2_M).TotalDays;
            return n_M + t_D / 31; // 1ヵ月を固定31日として月数の小数部を決める
        }

        // 単純な月表示部での月数の差(整数) 見たままの月数の差
        public static int DiffMonthsAsSeen(this DateTime t1, DateTime t2) { // t1 - t2
            return (t1.Year * 12 + t1.Month) - (t2.Year * 12 + t2.Month);
        }
```

# テストコード
```C#:Program 2301-3 月数(小数有り)を算出.cs
using System;

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

            // sec: 月数(小数有り)を算出

            /* 結果: 
// 同日となる1ヵ月は、ぴったり1ヵ月となる
1[month]
2[month]

// 月末から月末は、ぴったり1ヵ月単位となる
1[month]
3[month]

// 月末から始まる場合は、次の月末でぴったり1ヵ月となる
1.870967741935484[month]
1.903225806451613[month]
1.935483870967742[month]
2[month]

// 月末でない日から始まる場合は、次の同日でぴったり1ヵ月となる
1.967741935483871[month]
2[month]
2.032258064516129[month]
2.096774193548387[month]

// 算出した月数の小数部(31を掛けて日数に変換)
0.03225806451612903[month]
1[days](月数の小数部x31)
1.870967741935484[month]
27[days](月数の小数部x31)
1.935483870967742[month]
29[days](月数の小数部x31)

// 秒単位の変化でも月数を算出可
11.935483870594386[month]
12.000000000373356[month]
            */

            Action<string> WL = Console.WriteLine; // 短縮
            Func<string, DateTime> DT = DateTime.Parse;

            // 同日となる1ヵ月は、ぴったり1ヵ月となる
            WL($"{DT("2023/02/01").DiffMonthsAsFloat(DT("2023/01/01"))}[month]");
            WL($"{DT("2023/03/02").DiffMonthsAsFloat(DT("2023/01/02"))}[month]");
            WL("");

            // 月末から月末は、ぴったり1ヵ月単位となる
            WL($"{DT("2023/02/28").DiffMonthsAsFloat(DT("2023/01/31"))}[month]");
            WL($"{DT("2023/04/30").DiffMonthsAsFloat(DT("2023/01/31"))}[month]");
            WL("");

            // 月末から始まる場合は、次の月末でぴったり1ヵ月となる
            WL($"{DT("2023/04/27").DiffMonthsAsFloat(DT("2023/02/28"))}[month]");
            WL($"{DT("2023/04/28").DiffMonthsAsFloat(DT("2023/02/28"))}[month]");
            WL($"{DT("2023/04/29").DiffMonthsAsFloat(DT("2023/02/28"))}[month]");
            WL($"{DT("2023/04/30").DiffMonthsAsFloat(DT("2023/02/28"))}[month]");
            WL("");

            // 月末でない日から始まる場合は、次の同日でぴったり1ヵ月となる
            WL($"{DT("2023/04/26").DiffMonthsAsFloat(DT("2023/02/27"))}[month]");
            WL($"{DT("2023/04/27").DiffMonthsAsFloat(DT("2023/02/27"))}[month]");
            WL($"{DT("2023/04/28").DiffMonthsAsFloat(DT("2023/02/27"))}[month]");
            WL($"{DT("2023/04/30").DiffMonthsAsFloat(DT("2023/02/27"))}[month]");
            WL("");

            // 算出した月数の小数部(31を掛けて日数に変換)
            WL($"{DT("2023/01/03").DiffMonthsAsFloat(DT("2023/01/02"))}[month]");
            WL($"{DT("2023/01/03").DiffMonthsAsFloat(DT("2023/01/02")) % 1 * 31}[days](月数の小数部x31)");
            WL($"{DT("2023/03/01").DiffMonthsAsFloat(DT("2023/01/02"))}[month]");
            WL($"{DT("2023/03/01").DiffMonthsAsFloat(DT("2023/01/02")) % 1 * 31}[days](月数の小数部x31)");
            WL($"{DT("2023/04/29").DiffMonthsAsFloat(DT("2023/02/28"))}[month]");
            WL($"{DT("2023/04/29").DiffMonthsAsFloat(DT("2023/02/28")) % 1 * 31}[days](月数の小数部x31)");
            WL("");

            // 秒単位の変化でも月数を算出可
            WL($"{DT("2024/02/29").DiffMonthsAsFloat(DT("2023/02/28 0:00:00.001"))}[month]");
            WL($"{DT("2024/02/29 0:00:00.001").DiffMonthsAsFloat(DT("2023/02/28"))}[month]");
            WL("");

            // sec: 月数(小数有り)を算出 マイナスの期間となる場合

            /* 結果: 
// 同日となる1ヵ月は、ぴったり1ヵ月となる
-1[month]
-2[month]

// 月末から月末は、ぴったり1ヵ月単位となる
-1[month]
-3[month]

// 月末から始まる場合は、次の月末でぴったり1ヵ月となる
-1.967741935483871[month]
-2[month]
-2[month]
-2[month]

// 月末でない日から始まる場合は、次の同日でぴったり1ヵ月となる
-1.967741935483871[month]
-2[month]
-2.032258064516129[month]
-2.129032258064516[month]

// 算出した月数の小数部(31を掛けて日数に変換)
-0.032258064516129004[month]
-1[days](月数の小数部x31)
-1.967741935483871[month]
-30[days](月数の小数部x31)
-2[month]
-0[days](月数の小数部x31)

// 秒単位の変化でも月数を算出可
-11.999999999626644[month]
-12.096774193921744[month]
            */

            // 同日となる1ヵ月は、ぴったり1ヵ月となる
            WL($"{DT("2023/01/01").DiffMonthsAsFloat(DT("2023/02/01"))}[month]");
            WL($"{DT("2023/01/02").DiffMonthsAsFloat(DT("2023/03/02"))}[month]");
            WL("");

            // 月末から月末は、ぴったり1ヵ月単位となる
            WL($"{DT("2023/01/31").DiffMonthsAsFloat(DT("2023/02/28"))}[month]");
            WL($"{DT("2023/01/31").DiffMonthsAsFloat(DT("2023/04/30"))}[month]");
            WL("");

            // 月末から始まる場合は、次の月末でぴったり1ヵ月となる
            WL($"{DT("2023/02/28").DiffMonthsAsFloat(DT("2023/04/27"))}[month]");
            WL($"{DT("2023/02/28").DiffMonthsAsFloat(DT("2023/04/28"))}[month]");
            WL($"{DT("2023/02/28").DiffMonthsAsFloat(DT("2023/04/29"))}[month]");
            WL($"{DT("2023/02/28").DiffMonthsAsFloat(DT("2023/04/30"))}[month]");
            WL("");

            // 月末でない日から始まる場合は、次の同日でぴったり1ヵ月となる
            WL($"{DT("2023/02/27").DiffMonthsAsFloat(DT("2023/04/26"))}[month]");
            WL($"{DT("2023/02/27").DiffMonthsAsFloat(DT("2023/04/27"))}[month]");
            WL($"{DT("2023/02/27").DiffMonthsAsFloat(DT("2023/04/28"))}[month]");
            WL($"{DT("2023/02/27").DiffMonthsAsFloat(DT("2023/04/30"))}[month]");
            WL("");

            // 算出した月数の小数部(31を掛けて日数に変換)
            WL($"{DT("2023/01/02").DiffMonthsAsFloat(DT("2023/01/03"))}[month]");
            WL($"{DT("2023/01/02").DiffMonthsAsFloat(DT("2023/01/03")) % 1 * 31}[days](月数の小数部x31)");
            WL($"{DT("2023/01/02").DiffMonthsAsFloat(DT("2023/03/01"))}[month]");
            WL($"{DT("2023/01/02").DiffMonthsAsFloat(DT("2023/03/01")) % 1 * 31}[days](月数の小数部x31)");
            WL($"{DT("2023/02/28").DiffMonthsAsFloat(DT("2023/04/29"))}[month]");
            WL($"{DT("2023/02/28").DiffMonthsAsFloat(DT("2023/04/29")) % 1 * 31}[days](月数の小数部x31)");
            WL("");

            // 秒単位の変化でも月数を算出可
            WL($"{DT("2023/02/28 0:00:00.001").DiffMonthsAsFloat(DT("2024/02/29"))}[month]");
            WL($"{DT("2023/02/28").DiffMonthsAsFloat(DT("2024/02/29 0:00:00.001"))}[month]");
        }
    }

    public static class DateTime_ExtensionMethods {

        // 2つの日時の差を月数(小数有り)で算出 (小数部分は1ヵ月を固定31日として算出)
        public static double DiffMonthsAsFloat(this DateTime t1, DateTime t2, // t1 - t2
            bool keep_last = true) { // T:月末の日であれば次の月末までを1ヵ月として月数を算出
            // assume: 月数を小数有りで算出する為、小数部分は1ヵ月を月日数の最大値である31日として算出
            // note: 小数部を31で掛けると、1ヵ月以下の端数となる日数が計算できる
            // note: 1ヵ月が固定31日(最大値)の為、"1 - 当月数"等の引き算で差を取る事は不適

            var n_M = t1.DiffMonthsAsSeen(t2); // 単純な月表示の差(整数)
            var t2_M = t2.AddMonths(n_M, keep_last); // 月表示の差だけまず進める
            if (t1 < t2_M) { // if: t1を過ぎた
                n_M -= 1; // 1ヵ月繰り下げの分
                t2_M = t2.AddMonths(n_M, keep_last); // 再計算 // NOTE: 月末の日を考慮するので-1の減算は不可
            }
            var t_D = (t1 - t2_M).TotalDays;
            return n_M + t_D / 31; // 1ヵ月を固定31日として月数の小数部を決める
        }

        // 単純な月表示部での月数の差(整数) 見たままの月数の差
        public static int DiffMonthsAsSeen(this DateTime t1, DateTime t2) { // t1 - t2
            return (t1.Year * 12 + t1.Month) - (t2.Year * 12 + t2.Month);
        }

        // 月を加算(月末の日であれば加算後も月末となるような加算)
        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); // 月末を維持して月加算
        }
    }
}
```

# 環境
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)