こんにちは。遠藤です。
今回はLaravelのCarbonを使用して指定日時の1ヶ月前の月末日を算出する際の注意点に関して記載させていただきます。実際に私がつまずいた事例をもとに紹介させていただきます。
- 私がつまずいた事例
- Carbonにおける日付あふれ対応について
私がつまずいた事例
私が指定日時の1ヶ月前の月末日を算出する際に使用した最初のコードは以下の通りです。
1 2 3 4 5 |
A $date = Carbon::now(); // 今日の日付が2022-10-10の場合 $previousEndOfMonth = Carbon::parse($date)->endOfMonth()->subMonth()->toDateString(); print($previousEndOfMonth); // 出力:2022-10-01 |
最初のCarbon::parse($date)->endOfMonth()の部分で2022-10-31となり、1ヶ月前を取得する関数「subMonth()」を使用すれば、2022-9-30が取得できると想定しておりました。しかし、結果は2022-10-01と30日しか戻っていないことに気づきました。調査したところ、日付あふれを許さずに1ヶ月前を取得するメソッドがあると判明したため、subMonthNoOverflow()を使用することにしました。
1 2 3 4 |
B $date = Carbon::now(); // 今日の日付が2022-10-10の場合 $previousEndOfMonth = Carbon::parse($date)->endOfMonth()->subMonthNoOverflow()->toDateString(); print($previousEndOfMonth); // 出力:2022-09-30 |
正しく月末を取得できており問題ないと思っていました。しかし、2022-11-10で確認したところ、
1 2 3 4 5 |
C $date = Carbon::now(); // 今日の日付が2022-11-10の場合 $previousEndOfMonth = Carbon::parse($date)->endOfMonth()->subMonthNoOverflow()->toDateString(); print($previousEndOfMonth); // 出力:2022-10-30 |
2022-10-31とならなければいけないのですが、月末より1日前の2022-10-30が取得されてしまいました。
そこで、取得する順番に問題があると考え、以下のコードで実行してみました。
先に1ヶ月前を取得してから、月末日を取得すれば問題ないと考えました。
1 2 3 4 |
D $date = Carbon::now(); // 今日の日付が2022-11-10の場合 $previousEndOfMonth = Carbon::parse($date)->subMonth()->endOfMonth()->toDateString(); print($previousEndOfMonth); // 出力:2022-10-31 |
無事出力されましたが、先ほどsubMonth()が30日分しか前に戻らなかったことを考慮し、10-31でも問題ないか確認したところ、Carbon::parse($date)->subMonth()の部分で2022-10-01となり、もちろんendOfMonth()すると10-31が出力されてしまいました。
1 2 3 4 |
E $date = Carbon::now(); // 今日の日付が2022-10-31の場合 $previousEndOfMonth = Carbon::parse($date)->subMonth()->endOfMonth()->toDateString(); print($previousEndOfMonth); // 出力:2022-10-31 |
そこで、format(‘Y-m’)を使用し、今日の年月のみを取得し、それをparse()した値に対して1ヶ月前に戻すsubMonth()を適用するという方法を試みました。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
F $date = Carbon::now()->format('Y-m'); // 今日の日付が2022-10-31の場合 $previousEndOfMonth = Carbon::parse($date)->subMonth()->endOfMonth()->toDateString(); print($previousEndOfMonth); // 出力:2022-09-31 $date = Carbon::now()->format('Y-m'); // 今日の日付が2022-11-10の場合 $previousEndOfMonth = Carbon::parse($date)->subMonth()->endOfMonth()->toDateString(); print($previousEndOfMonth); // 出力:2022-10-31 $date = Carbon::now()->format('Y-m'); // 今日の日付が2022-12-10の場合 $previousEndOfMonth = Carbon::parse($date)->subMonth()->endOfMonth()->toDateString(); print($previousEndOfMonth); // 出力:2022-11-30 |
上記の方法では全て正しく前月末を取得することができました。今日の年月のみを取得し、parse()することで全ての今日の月の最初の日付、つまり、YYYY-MM-01の値を取得します。その値に対して、subMonth()を実施することで、Eのようになってしまう事象を避けることができました。YYYY-01〜YYYY-12までを入れてテストしてみたところ問題なく動きました。
Carbonにおける日付あふれ対応について
上記つまずきを順に記載させていただきましたが、大元の原因となった日付あふれについてまとめておきます。
Carbonには1ヶ月前を取得するメソッドとして以下の3つがあります。
1 2 3 |
subMonth() // 1月減らす subMonthWithOverflow() // 日付あふれを許可して1月減らす subMonthNoOverflow() // 日付あふれを許可せず1月減らす |
それぞれで2022-10-31から1月減らしてみると以下のようになります。
subMonth()
1 2 3 |
$date = Carbon::parse('2022-10-31')->subMonth(); print($date); // 2022-10-01 |
subMonthWithOverflow()
1 2 3 |
$date = Carbon::parse('2022-10-31')->subMonthWithOverflow(); print($date); // 2022-10-01 |
subMonthNoOverflow()
1 2 |
$date = Carbon::parse('2022-10-31')->subMonthNoOverflow(); print($date); // 2022-9-30 |
ここで、Carbonにおける日付あふれとは、2022-10-31を1月減らし、2022-09-31とするが、9月に31日は存在しないため、あふれた1日を10月に持ち越すことです。そのため、日付あふれを許可しているsubMonthWithOverflow()では、2022-10-01と表示されます。一方、日付あふれを許可しないsubMonthNoOverflow()においては、あふれた分が取り消されて2022-9-30となります。
subMonth()は日付あふれを許可するということで、subMonthWithOverflow()と同じ挙動になるようです。