PHPで月数を相対指定すると平年3月31日の1か月前は3月3日になる

3月1日の1か月前は2月1日で間違いない。では3月31日の1か月前はいつだろう? 2月31日は存在しない……。

以下は2019年3月31日の1か月前を出力するPHPコード。

$date = date_create('-1 month 2019/03/31');
echo date_format($date, 'Y/m/d');

実行すると、

2019/03/03

日常的な感覚とは随分掛け離れた結果になる。

ちなみに4月3日の1か月前も同日だ。1日と2日に至っては3月31日の1か月前を追い越して過去になる。なんとも奇妙な時空が現れたものだが、しかしこれはバグではない。

月数を相対指定すると、その途中に経過する月の日数を使って結果を算出します。たとえば "+2 month 2011-11-30" の結果は "2012-01-30" となります。11 月の日数は 30 日、12 月の日数は 31 日なので、その合計である 61 日後となるわけです。

「その途中に経過する月の日数」というのは、月を跨ぐ際の月末側の月の日数を指す(相対指定がマイナスであっても「経過」という時間の向きは変わらない)。これを数え上げることで月数を日数に換算している。

日数換算することにより、2月31日のような存在しない日付を避けられる。言い換えると、存在しない日付の部分さえ調整すればいいということだ。したがって、以下のプロセスでも(処理はともかくとして)同じ結果が得られる。

  1. 平年3月31日の1か月前 → 2月31日
  2. 2月29,30,31日の計3日分が超過
  3. 超過3日分を3月1,2,3日にスライド → 3月3日