エキサイトホールディングス Advent Calendar 2021の23日目は、 エキサイト株式会社 エンジニアのあはれん がお送りします。
Oracle データベースから日付データ(例:2022/02/28)を取得する際に、ADD_MOTHS関数を利用して1ヶ月先の日付に変換して取得する処理がありました。
この取得する処理のテストを書く際に、 処理の期待値としてPHPのDateTimeImmutableクラスのmodify関数を利用してある日付のデータの1ヶ月後を生成し、 実際に取得したデータと比較を行いました。
比較した結果、
なんと......テストが落ちたのです!!!
(ADD_MOTHSを利用して算出した1ヶ月後) ≠ (PHPのDateTimeImmutableクラスのmodify関数を利用して算出した1ヶ月後)
そうです。 Oracle の ADD_MOTHS関数とPHPのDateTimeImmutableクラスのmodify関数は仕様が違うのです。 今回は、それぞれの仕様を確認しながら違いを確認したいと思います。
Oracle の ADD_MOTHS関数について
ADD_MONTHSは、日付dateに月数integerを加えて戻します。
引用元:ADD_MONTHS
ですので、例えば、2022/01/28
を引数としたときは、2022/02/28
になります。
ADD_MONTHS('2022/01/28, 1) => 2022/02/28
ADD_MOTHS関数で気をつけておきたい点は、以下の仕様です。
dateが月の最終日の場合、または結果の月の日数がdateの日付コンポーネントよりも少ない場合、戻される値は結果の月の最終日となります。
引用元:ADD_MONTHS
文章を読み解いていきたいと思います。
「dateが月の最終日の場合、戻される値は結果の月の最終日となります。」の意味
dateが月の最終日(2021/12/31
)の場合、結果の月の最終日(2021/01/31
)になるのです。
ADD_MONTHS('2021/12/31', 1) => 2021/01/31
2021/12/31
から2021/01/31
の算出には違和感はありませんが、月の最終日を2022/02/28
とした時はどうでしょうか?
dateが月の最終日(2022/02/28
)の場合、結果の月の最終日(2021/03/31
)になるのです。
ADD_MONTHS('2022/02/28', 1) => 2021/03/31
2022/01/28
の場合は2022/02/28
になるのに、
2022/02/28
の場合は2022/03/28
にならないのは、
実装者が仕様を理解していないと驚いてしまうかと思います。
「結果の月の日数がdateの日付コンポーネントよりも少ない場合、戻される値は結果の月の最終日となります。」の意味
例えば、2022/1/29
の1ヶ月後を単純に考えると2022/2/29
だと思い付くかもしれませんが、
2022年はうるう年ではないので2022/2/29
が存在せず、2022/03/01
が1ヶ月後の日付になります。
このとき、結果(2022/03/01
)の月の日数(01
)が、date(2022/1/29
)の日付コンポーネント(29
)より小さいので、
2022/2/28
になります。
このことより、2022/1/29
から 2022/1/31
の日付の場合は、すべて2022/2/28
になります。
ADD_MONTHS('2022/1/29, 1) => 2022/2/28 ADD_MONTHS('2022/1/30, 1) => 2022/2/28 ADD_MONTHS('2022/1/31, 1) => 2022/2/28
PHP DateTimeImmutableクラスのmodify関数について
modify関数の場合は、文字通り1ヶ月後の日付になっています。
ですので、2022/02/28
の場合は2022/03/28
になります。
(new DateTimeImmutable('2022-02-28'))->modify('next month'); → 2022/03/28
また、2022/01/29
の場合は2022/03/01
になります。2022/01/30
以降も同様な計算になります。
(new DateTimeImmutable('2022/01/29'))->modify('next month'); → 2022/03/01 (new DateTimeImmutable('2022/01/30'))->modify('next month'); → 2022/03/02 (new DateTimeImmutable('2022/01/31'))->modify('next month'); → 2022/03/03
2022/01/31
の次の日である2022/02/01
の場合は2022/03/01
になるので、頭が混乱してしまいますね..。
(new DateTimeImmutable('2022/02/01'))->modify('next month'); → 2022/03/01
最後に
Oracle ADD_MOTHS関数とPHP DateTimeImmutableクラスのmodify関数で1ヶ月後の日付を算出した場合の比較表を用意してみました。
こうして見ると結構違う結果になることがわかります。
人間でも「2022/02/28の1ヶ月後の日付は?」と聞かれると、「2022/03/28」か「2022/03/31」、人それぞれな回答をするかと思います。 関数もそれぞれに回答を決めて実装されています。 日付を操作する関数を利用する際は関数のドキュメントを読んで挙動を理解し、自身の実装目的に合っているものをお使いください。
エキサイトホールディングスのアドベントカレンダーはまだまだ続きます。
明日の執筆担当者は、@ixit_horiさんです。引き続きお楽しみください。
採用情報はこちら↓