📚

Carbonの subMonth と subMonthsWithOverflow とか

2021/03/29に公開

テストを回している時に、テストデータとして先月の情報を入れるはずが今月の情報を入れてしまいテストがコケるという事が起きたのでメモ。

使用している Carbon は ^2.0 です。

SubMonths メソッドという物

先月や数ヶ月前の日付を取り出すのに使用しています。ただ、Carbonには subMonths メソッド以外にも subMonthsWithOverflow, subMonthsWithoutOverflow, subMonthsWithNoOverflow, subMonthsNoOverflow とあります。

説明文には「overflow explicitly forbidden」や「overflow explicitly allowed」と言った区別がされています。

とりあえず 2021/03/29 の一ヶ月前を見てみます。

>>> \Carbon\Carbon::create( 2021, 3, 29, 1 )->subMonths(1)
=> Carbon\Carbon @1614528000 {#3827
     date: 2021-03-01 01:00:00.0 Asia/Tokyo (+09:00),
   }
>>> \Carbon\Carbon::create( 2021, 3, 29, 1 )->subMonthsWithOverflow(1)
=> Carbon\Carbon @1614528000 {#3856
     date: 2021-03-01 01:00:00.0 Asia/Tokyo (+09:00),
   }
>>> \Carbon\Carbon::create( 2021, 3, 29, 1 )->subMonthsWithoutOverflow(1)
=> Carbon\Carbon @1614441600 {#3860
     date: 2021-02-28 01:00:00.0 Asia/Tokyo (+09:00),
   }
>>> \Carbon\Carbon::create( 2021, 3, 29, 1 )->subMonthsWithNoOverflow(1)
=> Carbon\Carbon @1614441600 {#3858
     date: 2021-02-28 01:00:00.0 Asia/Tokyo (+09:00),
   }
>>> \Carbon\Carbon::create( 2021, 3, 29, 1 )->subMonthsNoOverflow(1)
=> Carbon\Carbon @1614441600 {#3861
     date: 2021-02-28 01:00:00.0 Asia/Tokyo (+09:00),
   }

一ヶ月前を取るなら subMonthsWithoutOverflow でいいじゃんって言う感じですが、いくつか疑問が出ます。

  1. 内部(PHP)では -30 日してるみたいだけど何で 3/1 や 2/28 とブレが出る?
  2. subMonthsWithOverflow, subMonthsWithoutOverflow,subMonthsWithNoOverflow,subMonthsNoOverflow のそれぞれの違いは何?

内部(PHP)では -30 日してるみたいだけど何で 3/1 や 2/28 とブレが出る?

Carbon は基本的には PHP の \DateTime を継承している物です。そして独自メソッドの subMonths 等は Carbon\Traits\Date とかでゴリゴリ実装している感じ。

で一ヶ月前の解釈違いで、例えば 3/10 の一ヶ月前は 2/10 としたら、 3/30 の一ヶ月前は 2/30 にはならないので翌月の 3/2 にする、という解釈になる。この翌月に持ち越すのを overflow と言っているみたいです。これは年度にも言えて、うるう年などに関係してきます。

subMonthsWithOverflow,subMonthsWithoutOverflow,subMonthsWithNoOverflow,subMonthsNoOverflow のそれぞれの違いは何?

overflow をするしないかの違いなのですが、なぜに WithoutWithNoNo が増えたというアレ。あと久々に見たらえらい見にくくなってたのでソースコード追っかけメモ書き。

ここらへん -- github をみれば大体わかります。

見た限り Month|Quarter|Year|Decade|Century|Centurie|Millennium|MillenniaNo|With|Without|WithNo の2種類に分けて、後半の Overflow に関しては With を付けるか付けないかのみの違いみたいです。

そして実際の処理は $this->{"${action}Unit"}($unit, $parameters[0] ?? 1, $overflow); といった感じですごい投げ方してます。 subMonthsWithOverflow で展開すると $action = 'sub'; $unit = 'Month'; $parameters = []; $overflow = true; になって subUnit('Month', 1, true); といった感じになるはず。
そんなわけで Units::subUnit に飛ぶのです が単純に数値を負にするだけのようで、結局は Units::addUnit が処理をします。

すべての add/sub 処理は Units::addUnit で Datetime::modify で実行しています。しかし Units::addUnit の末尾の方で overflow 設定をしている場合は先月末に戻す 処理を入れています。

したがって、 subMonths()subMonthsWithOverflow() は処理内容は同じです。そして subMonthsWithoutOverflow,subMonthsWithNoOverflow,subMonthsNoOverflow ここらは否定の仕方が違うだけで中身は同じです。

参考

同じハマり方 : PHP Carbonで月の足し引きオーバーフローにはまった

Discussion