PHPで月末から1ヶ月後「+1 month」を算出すると想定する日付にならない場合がある
PHPで月末から1ヶ月後「+1 month」を算出すると想定する日付にならない場合がある
「CakePHP4のFrozenDateで1ヵ月前、先月、今月1日、来月末の日付などを算出する方法」
「PHPで1ヵ月前、先月、今月1日、来月末の日付などの算出はDateTimeImmutableを使う」
上記の記事では PHPで 1ヶ月後の日付、今月月初の日付、来月末の日付、5か月後の日付などを算出する際は、CakePHPでは「FrozenDate」を使い、PHPでは「DateTime」「DateTimeImmutable」を使うと便利ですよ、という記事を書きました。
※この記事では、日付を生成する関数に「DateTimeImmutable」を使用しています。
「DateTimeImmutable」を「DateTime」に置き換えてもらっても問題ありません。「DateTime」と「DateTimeImmutable」の違いについても下記に書いていますので併せて参考にしてください。
「PHPで1ヵ月前、先月、今月1日、来月末の日付などの算出はDateTimeImmutableを使う」
PHPの 1ヶ月後「+1 month」が 1ヶ月後にならない事例
この際、PHPでは 1ヶ月後(「+1 month」「next month」)を算出する際に注意が必要な点があります。
下記のように、月末に関連しない日の「1ヶ月後」は全く問題ありません。
1 2 3 4 5 6 |
// 1月15日の 1ヶ月後は? $dateImmutable = new DateTimeImmutable("2024-01-15"); var_export($dateImmutable->modify('+1 month')); // 結果の日付は下記 // 'date' => '2024-02-15 00:00:00.000000', |
ですが、下記のように「1月31日」などの月末に関連する日の「1ヶ月後」を算出する際は注意が必要です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// 1月31日の 1ヶ月後は? $dateImmutable = new DateTimeImmutable("2024-01-31"); var_export($dateImmutable->modify('+1 month')); // 結果の日付は下記 // 'date' => '2024-03-02 00:00:00.000000', // 1月30日の 1ヶ月後は? $dateImmutable = new DateTimeImmutable("2024-01-30"); var_export($dateImmutable->modify('+1 month')); // 結果の日付は下記 // 'date' => '2024-03-01 00:00:00.000000', // 4月30日の 1ヶ月後は? $dateImmutable = new DateTimeImmutable("2024-04-30"); var_export($dateImmutable->modify('+1 month')); // 結果の日付は下記 // 'date' => '2024-05-30 00:00:00.000000', |
「1月31日」の「+1 month(1ヶ月後)」は、1月は 1ヵ月 31日なので 31日後、
「4月30日」の「+1 month(1ヶ月後)」は、4月は 1ヵ月 30日なので 30日後、
という算出がなされます。
1ヶ月後「+1 month」が 1ヶ月後にならないのはバグではなく PHPの仕様
これは、バグではなく、PHPでは「1ヶ月後」を「その月の日数分の日付を進めた日」と定義しているということです。
つまり、
1月、3月のように 31日まである月の 1ヵ月とは 31日後で、
4月、6月のように 30日までの月の 1ヵ月とは 30日後で、
2月のように 28日(閏年は除く)までの月の 1ヵ月とは 28日後です
ということです。
これはバグではなく、PHPの「1ヶ月後」がそう定義されているので仕方がないですよね。
「1ヶ月後」だけではなく「2ヶ月後」「3か月後」を算出するとより分かりやすいかと思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
$dateImmutable = new DateTimeImmutable("2024-01-31"); var_export($dateImmutable->modify("+1 month")); var_export($dateImmutable->modify("+2 month")); var_export($dateImmutable->modify("+3 month")); var_export($dateImmutable->modify("+4 month")); var_export($dateImmutable->modify("+5 month")); // 出力結果は以下のようになります。 // 'date' => '2024-01-31 00:00:00.000000' // 'date' => '2024-03-02 00:00:00.000000' // 31日後(1月分) // 'date' => '2024-03-31 00:00:00.000000' // 29日後(2月分) // 'date' => '2024-05-01 00:00:00.000000' // 31日後(3月分) // 'date' => '2024-05-31 00:00:00.000000' // 30日後(4月分) // 'date' => '2024-07-01 00:00:00.000000' // 31日後(5月分) |
また、上記から分かるように、「1月31日」の「2ヶ月後」「3ヶ月後」...は「31日×2ヶ月分後」「31日×3ヶ月分後」ではない点にも注意が必要ですね。
ちなみに、(「+2 month」) と (「+1 month」に「+1 month」) とでは異なる結果になります。
1 2 3 4 5 6 7 8 9 10 |
$dateImmutable = new DateTimeImmutable("2024-01-31"); var_export($dateImmutable); var_export($dateImmutable2 = $dateImmutable->modify("+1 month")); var_export($dateImmutable3 = $dateImmutable2->modify("+1 month")); // 出力結果は以下のようになります。 // 'date' => '2024-01-31 00:00:00.000000' // 'date' => '2024-03-02 00:00:00.000000' // 31日後(1月分) // 'date' => '2024-04-02 00:00:00.000000' // 31日後(3月分) |
2回目の「$dateImmutable2
」に対して「+1 month」する処理では、「3月2日」に対して実行する処理のためですね。
この点も一つ認識をしておく必要があるかと思います。
PHPで「翌月末」や「1ヶ月後」を算出するサンプルプログラム
さてさて。
ここまで説明した内容は、PHPの「1ヶ月後」の定義ですので変えようがない部分です。
では、「1ヶ月後」をどのように算出するか、を考えていきましょう。
しかし、算出するにあたっては、まず最初に「1ヶ月後」を定義する必要があります。
この「1ヶ月後」がどういう定義なのか、によって対応方法が変わってきます。
取得パターン:「翌月末」を算出する方法
経理処理などをする際は「『申込日』を基準としてその『翌月末』を支払い日とする」というような定義で日付の算出が必要な場合があるかと思います。
そのような場合は、下記のように「+1 month」の前に「last day of」を追加することで翌月末を取得することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
$dateImmutable = new DateTimeImmutable("2024-01-31"); var_export($dateImmutable); var_export($dateImmutable->modify("last day of +1 month")); var_export($dateImmutable->modify("last day of +2 month")); var_export($dateImmutable->modify("last day of +3 month")); var_export($dateImmutable->modify("last day of +4 month")); var_export($dateImmutable->modify("last day of +5 month")); // 出力結果は以下のようになります。 // 'date' => '2024-01-31 00:00:00.000000' // 'date' => '2024-02-29 00:00:00.000000' // last day of +1 month // 'date' => '2024-03-31 00:00:00.000000' // last day of +2 month // 'date' => '2024-04-30 00:00:00.000000' // last day of +3 month // 'date' => '2024-05-31 00:00:00.000000' // last day of +4 month // 'date' => '2024-06-30 00:00:00.000000' // last day of +5 month |
この処理では「月末」を算出する処理ですので、「1月15日」を基準日に処理を実行しても「2月29日」が算出されます。
ただ、「月末」を算出する処理としてはシンプルに記述することができます。
取得パターン:1ヶ月後を取得するけど、月末の場合は翌月末日を取得する
『基本的な処理は「1ヶ月後」を取得するけど、月末の場合は翌月末を取得する』という場合は、「月末」は単純に「+1 month」では対応できませんので、「月末」の場合だけ処理を分岐する下記のような処理をするといいのではないか、と思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
$dateImmutable = new DateTimeImmutable("2024-01-31"); if (in_array($dateImmutable->format("m"),[1])){ // 「1月」の対応 if(in_array($dateImmutable->format("d"),[29,30,31])){ var_export($dateImmutable->modify("last day of +1 month")); } else { var_export($dateImmutable->modify("+1 month")); } } elseif (in_array($dateImmutable->format("m"),[2])){ // 「2月」の対応 if(in_array($dateImmutable->format("d"),[28,29])){ var_export($dateImmutable->modify("last day of +1 month")); } else { var_export($dateImmutable->modify("+1 month")); } } elseif (in_array($dateImmutable->format("m"),[3,5,7,8,10,12])){ // 「3月」「5月」「7月」「8月」「10月」「12月」の対応 if(in_array($dateImmutable->format("d"),[31])){ var_export($dateImmutable->modify("last day of +1 month")); } else { var_export($dateImmutable->modify("+1 month")); } } else { // 「4月」「6月」「9月」「11月」の対応 if(in_array($dateImmutable->format("d"),[30])){ var_export($dateImmutable->modify("last day of +1 month")); } else { var_export($dateImmutable->modify("+1 month")); } } |
上記は閏年の 2月は考慮されていません。
閏年の場合は、「2月28日」も「2月29日」も「3月31日」となります。それで問題がある場合は、閏年か否かを判断して「28日」の取り扱いの分岐を増やしてください。
取得パターン:1ヶ月後を取得するけど、31日の月の月末の場合のみ翌月末日を取得する
『基本的な処理は「1ヶ月後」を取得するけど、31日の月の月末の場合は翌月末を取得する』という場合は、下記のような分岐処理をするといいのではないか、と思います。
前項と何が違うかというと、「1月31日」の 1ヵ月後は「2月末」にしたいが、「4月30日」の 1ヶ月後は「5月30日」にしたい、という場合で、前項よりもちょっとだけセコイ契約になる感じでしょうか...。でも、プログラムとしてはこちらの方がシンプル!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
$dateImmutable = new DateTimeImmutable("2024-01-30"); if (in_array($dateImmutable->format("m"),[1])){ // 「1月」の対応 if(in_array($dateImmutable->format("d"),[29,30,31])){ var_export($dateImmutable->modify("last day of +1 month")); } else { var_export($dateImmutable->modify("+1 month")); } } elseif (in_array($dateImmutable->format("m"),[3,5,8,10,12])){ // 「3月」「5月」「8月」「10月」「12月」の対応 if(in_array($dateImmutable->format("d"),[31])){ var_export($dateImmutable->modify("last day of +1 month")); } else { var_export($dateImmutable->modify("+1 month")); } } else { // 「2月」「4月」「6月」「7月」「9月」「11月」の対応 var_export($dateImmutable->modify("+1 month")); } |
「7月」は 31日までありますが、翌月も 31日までありますので、「3月」などと同じ分岐に入れても、最後の elseの分岐に入れても結果は変わりません。
取得パターン:1ヶ月を 30日 or 31日と規定している場合
「1ヶ月」の定義を「30日 or 31日」と規定している場合は、下記のようにシンプルに算出できます。
1 2 3 4 5 6 7 |
$dateImmutable = new DateTimeImmutable("2024-01-31"); // 1ヶ月を 30日と規定している場合 var_export($dateImmutable->modify("+30 day")); // 1ヶ月を 31日と規定している場合 var_export($dateImmutable->modify("+31 day")); |
1ヶ月を 30日とする場合は 1年が 360日となりますので、毎年 5日ずつ短くなっていきます。
また、「1月15日」の「1ヶ月後」が「2月14日」になりますので、混乱につながる可能性はあるかと思います。
ただ、それが許容できるのであれば、システムの処理としてはシンプルになります。
ちなみに、この短くなる 5日ですが、そのまま毎年短くなっていく方法もありだと思いますが、1年継続利用した場合は、最終月に無料利用期間の 5日間を追加するので、結果的に 1年後は 365日後になる、みたいな処理(契約)でもいいのかもしれないですね。
PHPで「翌月末」や「1ヶ月後」を算出する処理のまとめ
サンプルプログラムを提示しつつ「翌月末」や「1ヶ月後」を算出しましたが、
「1月31日」の「1ヶ月後」は
「3月2日」なのか?
「2月28日」なのか?
は状況によるので(契約内容によるので)、その条件なしに「これが正解」とは言えないということですね。
それぞれ、システムを作る上で「1ヶ月後」の定義を確認したうえで、それに合わせた適切な処理を検討することになるわけですね。
日付、日時に関する関連記事をこの記事も含めて 3記事書いていますので併せて参考にしてください。
「CakePHP4のFrozenDateで1ヵ月前、先月、今月1日、来月末の日付などを算出する方法」
「PHPで1ヵ月前、先月、今月1日、来月末の日付などの算出はDateTimeImmutableを使う」
「PHPで月末から1ヶ月後「+1 month」を算出すると想定する日付にならない場合がある」
GoogleAdwords
GoogleAdwords
この記事が参考になったと思いましたらソーシャルメディアで共有していただけると嬉しいです!
関連記事
-
PHPでスクレイピング。phpQueryとphp-simple-html-dom-parserの比較と設置方法
「PHP スクレイピング」で検索すると「phpQuery」ばかりヒットするが、10年以上も放置されている。なので今も開発が続いている「PHP Simple HTML DOM Parser」をオススメする。
-
Smartyの Syntax Errorの原因はスペースかも
Smartyのなかなか原因がつかめない Syntax Errorの原因はスペースかもしれません。
-
ECCUBE mtb_constants initパラメータ設定の項目を追加する方法
ECCUBEのパラメータ設定で設定できる項目を追加する方法を説明します。
-
複数銘柄を指定して株価チャートを一覧するツール公開
入力銘柄の5日間、3か月間、6か月間、1年間、2年間の株価チャートを一覧表示しますのでチャートで売買判断をするのに最適です。
-
ECCUBEでカード決済NGの受注情報をマイページ購入履歴に表示しない方法解説
ECCUBEでカード決済に失敗しても購入履歴一覧に注文情報(受注情報)が表示される問題への対処方法を解説。受注情報レコードの作成の流れとステイタスについても解説。
-
連想配列のキーも値もまとめてhtmlspecialchars()でサニタイズする関数の作成解説
PHPの配列・連想配列のキーと値をまとめてhtmlspecialchars()関数でサニタイズ(無害化、無毒化)を行う関数を作成。連想配列のキーはarray_map()関数でのサニタイズは無理。
-
ECCUBEのポイント設定、ポイント付与率を一括で変更する方法解説
ECCUBEの商品個別に設定してあるポイントを一括で変更する方法を解説。ECCUBEには商品個別のポイントを一括して変更する機能がありません。SQLを作成して一括置換!
-
PHPのスクレイピングライブラリ「PHP Simple HTML DOM Parser」の使い方
PHPのスクレイピングライブラリ「PHP Simple HTML DOM Parser」の使い方を解説。要素を取得する方法、そこから属性を取得する方法を解説。また、マニュアルにはない注意点なども解説。
-
sleepの秒指定は整数のみなので1.5秒はsleep、usleepを組み合わせる
PHPのプログラム処理を遅延させる「sleep()」は整数秒単位。「usleep()」は1秒未満のマイクロ秒単位。では「1.5」秒はどう表現するのか。「sleep()」「usleep()」を組み合わせる。その解説。
-
Smartyの修飾子regex_replaceで正規表現の後方参照・PHPではpreg_replace
ECCUBEで使われているSmartyで文字列を正規表現で置換し後方参照で値を利用する装飾子regex_replaceの解説です。細かな条件がありますので注意が必要です。