2020/05/05

日付処理 - 月末日を計算する

月末日を求めたいときは、だいたい月で条件分岐しますよね。1, 3, 5, 7, 8, 10, 12月なら31日、4, 6, 9, 11月なら30日、2月なら閏年判定をやって28か29日。

でも今回は、計算式で月末日を算出してみたいと思います。

アルゴリズム

$Y$: 年, $M$: 月に対し、月末日 $E$ は以下のように計算します。

$$
m = \begin{cases}
M & (M \ge 3) \\
M + 12 & (M \le 2)
\end{cases}
$$
$$
E = \lfloor 0.6m+31.2 \rfloor - \lfloor 0.6(m+1) \rfloor - \lfloor (\frac{m}{14})  \rfloor (2+\lfloor \frac{Y \bmod 4 - 4}{4} \rfloor - \lfloor \frac{Y \bmod 100 - 100}{100} \rfloor + \lfloor \frac{Y \bmod 400 - 400}{400} \rfloor)
$$

$E$ の算出式の前半 (最初の2つの項) は、2月以外の月末日の算出です。ツェラーの公式の部分適用で、1, 2月を13, 14月に置き換えることによって簡単に計算できます。
2月についてはやはり、閏年は29日、閏年以外は28日にする補正が必要です。前半部では2月は30と算出されていますので、-1 ないし -2 する必要があります。
後半部の $\lfloor (\frac{m}{14})  \rfloor$ は、2月(14月) のときのみ1で、他は 0 となりますから、あとは閏年に1、閏年以外に2となる式を作るだけです。
$\lfloor \frac{a \bmod b - b}{b} \rfloor$とすると、a が b で割り切れるときは -1、そうでないときは 0 となります。これを使うと括弧の中はこうなります。
  • 4 で割り切れない年 (not 閏年)
    • $2 + 0 - 0 + 0 = 2$
  • 4 で割り切れるが、100 で割り切れない年 (閏年)
    • $2 + (-1) - 0 + 0 = 1$
  • 100 で割り切れるが、400 で割り切れない年 (not 閏年)
    • $2 + (-1) - (-1) + 0 = 2$
  • 400 で割り切れる年 (閏年)
    • $2 + (-1) - (-1) + (-1) = 1$

実装例 (Java)

public static int getEndOfMonth(int y, int m) {
    if (m == 1 || m == 2) {
        m += 12;
    }
    return (int) (m * 0.6 + 31.2) - (int) (m * 0.6 + 0.601) - m / 14 * (2 + (y % 4 - 4) / 4 - (y % 100 - 100) / 100 + (y % 400 - 400) / 400);
}

0.601 となっている箇所は、0.6 だと浮動小数点加算の誤差の影響でキャスト後の結果が想定より1小さくなることがあるため、ゲタをはかせています。
後半部は全て整数演算なので、切り捨てを明示する必要はありません。

最後に

計算式でできるのはかっこいいのですが、業務プログラミングにおける実用性としては、あまりありません。業務プログラミングであれば、処理系で持っているカレンダー機構に頼った方が可読性等から考えると望ましいです。