PHP

【PHP】翌月、前月の日付を取得する方法(ズレなく正確な日付の取得)

前回、過去記事でPHPのファイル削除機能のサンプルを紹介しました。

その中で日付の取得するstrtotime関数の部分で、正確な日付を取れないものになっていたのでやり方を調べてみました。

最終的な完成形を紹介する前に、まずは正確な日付けが取れない原因を理解するため、サンプルで出力結果を確認しながら調査しました。

今回も引き続き検証したPHPのバージョン「7.2.12」になります。

見出しテキスト

・strtotime関数で日付けがズレる理由
  strtotime関数の正常パターン
  翌月の日付けでズレるパターン
  前月の日付でズレるパターン
・翌月と前日の正確な日付を取得する

strtotime関数で日付けがズレる理由

strtotime関数の正常パターン

まず下記のサンプルを見て下さい。

sample1.php

<?php
// タイムゾーンの設定
date_default_timezone_set('Asia/Tokyo');

print "[正常系]=================================================\n";
$target_date = "2018/11/10 00:00:00";
print "対象日:".date('Y-m-d', strtotime($target_date)) ."\n";
print "1か月後:".date('Y-m-d', strtotime($target_date." +1 month")) ."\n";
print "1か月前:".date('Y-m-d', strtotime($target_date." -1 month")) ."\n";
print "=========================================================\n";
$target_date = "2018/10/30 00:00:00";
print "対象日:".date('Y-m-d', strtotime($target_date)) ."\n";
print "1か月後:".date('Y-m-d', strtotime($target_date." +1 month")) ."\n";
print "1か月前:".date('Y-m-d', strtotime($target_date." -1 month")) ."\n";
print "=========================================================\n";

?>

strtotime関数を使って翌月と前月の日付けを取得したサンプルです。
sample1.phpを実行した結果が以下となります。

[正常系]=================================================
対象日:2018-11-10
1か月後:2018-12-10
1か月前:2018-10-10
=========================================================
対象日:2018-10-30
1か月後:2018-11-30
1か月前:2018-09-30
=========================================================

このケースだと翌月・前月共に正確に日付を取得できているかと思います。

翌月の日付けでズレるパターン

続いて、日付がズレるケースです。

翌月を取得した際に日付けがズレるケースが以下のものになります。

sample2.php

<?php
// タイムゾーンの設定
date_default_timezone_set('Asia/Tokyo');

print "[翌月の日付で誤差が出るパターン]=========================\n";
$target_date = "2019/01/31 00:00:00";
print "対象日:".date('Y-m-d', strtotime($target_date)) ."\n";
print "1か月後:".date('Y-m-d', strtotime($target_date." +1 month")) ." <-誤差発生\n";    
print "1か月前:".date('Y-m-d', strtotime($target_date." -1 month")) ."\n";
print "=========================================================\n";
$target_date = "2019/01/30 00:00:00";
print "対象日:".date('Y-m-d', strtotime($target_date)) ."\n";
print "1か月後:".date('Y-m-d', strtotime($target_date." +1 month")) ." <-誤差発生\n";
print "1か月前:".date('Y-m-d', strtotime($target_date." -1 month")) ."\n";
print "=========================================================\n";

?>

sample2.phpの実行結果が下記となります。

[翌月の日付で誤差が出るパターン]=========================
対象日:2019-01-31
1か月後:2019-03-03 <-誤差発生
1か月前:2018-12-31
=========================================================
対象日:2019-01-30
1か月後:2019-03-02 <-誤差発生
1か月前:2018-12-30
=========================================================

「2018年1月31日」と「2018年1月30日」の翌月を取得した際に日付の誤差が発生しています。

この原因は対象日に対して、翌月の日付が存在しないために発生します。
例えば、「1月31日」の翌月として単純に1ヶ月分足すと「2月31日」、「1月30日」は「2月30日」になりますが、2月は28日、閏年でも29日までしか存在せず30日と31日は存在しない日付となります。

strtotime関数の仕様として、存在しない日付だった場合はその月の末日の差分を加算した日付が返ってきます。つまり「1月31日」の翌月だと2月の末日は「2月28日」なので、差分は「31日 - 28日 = 3日」となります。そこから2月の末日から溢れた分を加算して「2月28日 + 3日間 = 3月3日 となるため3月の日付けが取得されるのです。

これは閏年以外にも、30日までしか無い月にも発生し得ることになります。

前月の日付でズレるパターン

翌月と同じく前月の日付を取得するケースでも同じようなことが発生します。

sample3.php

<?php
// タイムゾーンの設定
date_default_timezone_set('Asia/Tokyo');

print "[前月の日付で誤差が出るパターン]=========================\n";
$target_date = "2018/12/31 00:00:00";
print "対象日:".date('Y-m-d', strtotime($target_date)) ."\n";
print "1か月後:".date('Y-m-d', strtotime($target_date." +1 month")) ."\n";
print "1か月前:".date('Y-m-d', strtotime($target_date." -1 month")) ." <-誤差発生\n";
print "=========================================================\n";
$target_date = "2019/03/30 00:00:00";
print "対象日:".date('Y-m-d', strtotime($target_date)) ."\n";
print "1か月後:".date('Y-m-d', strtotime($target_date." +1 month")) ."\n";
print "1か月前:".date('Y-m-d', strtotime($target_date." -1 month")) ." <-誤差発生\n";
print "=========================================================\n";

?>

実行結果

[前月の日付で誤差が出るパターン]=========================
対象日:2018-12-31
1か月後:2019-01-31
1か月前:2018-12-01 <-誤差発生
=========================================================
対象日:2019-03-30
1か月後:2019-04-30
1か月前:2019-03-02 <-誤差発生
=========================================================

「12月31日」の前月は「11月31日」、「3月30日」の前月は「2月30日」ですが、それぞれ存在しない日付のためズレが発生しているのが分かるかと思います。

【補足】

翌月と前月の両方でズレが生じるパターン

<?php
// タイムゾーンの設定
date_default_timezone_set('Asia/Tokyo');

print "[翌月と前月の両方日付で誤差が出るパターン]================\n";
$target_date = "2018/10/31 00:00:00";
print "対象日:".date('Y-m-d', strtotime($target_date)) ."\n";
print "1か月後:".date('Y-m-d', strtotime($target_date." +1 month")) ." <-誤差発生\n";
print "1か月前:".date('Y-m-d', strtotime($target_date." -1 month")) ." <-誤差発生\n";
print "=========================================================\n";
$target_date = "2019/03/31 00:00:00";
print "対象日:".date('Y-m-d', strtotime($target_date)) ."\n";
print "1か月後:".date('Y-m-d', strtotime($target_date." +1 month")) ." <-誤差発生\n";
print "1か月前:".date('Y-m-d', strtotime($target_date." -1 month")) ." <-誤差発生\n";
print "=========================================================\n";

?>

/**
* 出力結果
*/
[翌月と前月の両方日付で誤差が出るパターン]================
対象日:2018-10-31
1か月後:2018-12-01 <-誤差発生
1か月前:2018-10-01 <-誤差発生
=========================================================
対象日:2019-03-31
1か月後:2019-05-01 <-誤差発生
1か月前:2019-03-03 <-誤差発生
=========================================================

存在しない日付のパターン

<?php
// タイムゾーンの設定
date_default_timezone_set('Asia/Tokyo');

print "[存在しない日付のパターン]===============================\n";
$target_date = "2018/09/29 00:00:00";
print date('Y-m-d', strtotime($target_date)) ."\n";
print "1か月後:".date('Y-m-d', strtotime($target_date." +1 month")) ."\n";
print "1か月前:".date('Y-m-d', strtotime($target_date." -1 month")) ."\n";
print "=========================================================\n";
$target_date = "2018/14/20 00:00:00";
print date('Y-m-d', strtotime($target_date)) ."\n";
print "1か月後:".date('Y-m-d', strtotime($target_date." +1 month")) ."\n";
print "1か月前:".date('Y-m-d', strtotime($target_date." -1 month")) ."\n";
print "=========================================================\n";

?>

/**
* 出力結果
*/[存在しない日付のパターン]===============================
2018-09-29
1か月後:2018-10-29
1か月前:2018-08-29
=========================================================
1970-01-01
1か月後:1970-01-01
1か月前:1970-01-01
=========================================================

翌月と前日の正確な日付を取得する

対処法

上記の例で調べた通り、日付のズレが発生するパターンは対象日が末日付近の場合のみ発生します。

そこで対処法として、例えば対象日の前月の日付を取得る際、対象日の前月末日と比較して対象日の前月日前月末日を超えた場合に前月末日を返すようにすることで正確な日付を取れるようにしました。

サンプルソース

サンプルを作成しました。

sample4.php

// 基準日
$target_date = "2019/03/31";
$term = 1;
print $target_date."\n";
// 前月の末日
print "前月の末日\r\n";
$last_date = date('Ymd', strtotime($target_date." last day of -{$term} month"));
print $last_date."\n";
// 前月日
print "前月日\r\n";
$prev_date = date('Ymd', strtotime($target_date." -{$term} month"));
print $prev_date."\n";
print "----------------------------\n";
if ($last_date < $prev_date) {
  echo "最終出力結果:".$last_date."\n";
} else {
  echo "最終出力結果:".$prev_date."\n";

実行結果

2019/03/31
前月の末日
 20190228
前月日
 20190303
----------------------------
最終出力結果:20190228

上記は対象日「2018年3月31日」の前月を取るサンプルです。

単純にstrtotime関数で前月を取ると「3月3日」になってしまいますが、それを補完した最終出力結果は「2月28日」と正常な翌月日を取れてるかと思います。

このサンプルを元に、共通関数化したものが以下となります。

sample5.php

<?php
// タイムゾーンの設定
date_default_timezone_set('Asia/Tokyo');

function getNextDate($target_date=NULL, $term=1) {
  if (empty($target_date)) $target_date = date('Ymd');
  // 翌月末日を取得...(1)
  $last_date = date('Ymd', strtotime($target_date." last day of +{$term} month"));
  // 対象日の翌月日を取得...(2)
  $prev_date = date('Ymd', strtotime($target_date." +{$term} month"));
  // (1)と(2)を比較し、(2)の方が未来日の時とみ(1)を出力する
  if ($prev_date > $last_date) {
    return $last_date;
  } else {
    return $prev_date;
  }
}

function getPrevDate($target_date=NULL, $term=1) {
  if (empty($target_date)) $target_date = date('Ymd');
  // 前月末日を取得...(1)
  $last_date = date('Ymd', strtotime($target_date." last day of -{$term} month"));
  // 対象日の前月日を取得...(2)
  $prev_date = date('Ymd', strtotime($target_date." -{$term} month"));
  // (1)と(2)を比較し、(2)の方が未来日の時とみ(1)を出力する
  if ($prev_date > $last_date) {
    return $last_date;
  } else {
    return $prev_date;
  }
}

print '対象日:20181231 -> 翌月:'.getNextDate('20181231',1)."\n";
print '対象日:20181231 -> 前月:'.getPrevDate('20181231',1)."\n";
print '対象日:20181230 -> 翌月:'.getNextDate('20181230',1)."\n";
print '対象日:20181230 -> 前月:'.getPrevDate('20181230',1)."\n";
print "----------------------------------------\n";
print '対象日:20181131 -> 翌月:'.getNextDate('20181131',1)."\n";
print '対象日:20181131 -> 前月:'.getPrevDate('20181131',1)."\n";
print '対象日:20181130 -> 翌月:'.getNextDate('20181130',1)."\n";
print '対象日:20181130 -> 前月:'.getPrevDate('20181130',1)."\n";
print "----------------------------------------\n";
// 2ヶ月先も試す
print '対象日:20181231 -> 2ヶ月後:'.getNextDate('20181231',2)."\n";
print '対象日:20181231 -> 2ヶ月前:'.getPrevDate('20181231',2)."\n";
print '対象日:20181230 -> 2ヶ月後:'.getNextDate('20181230',2)."\n";
print '対象日:20181230 -> 2ヶ月前:'.getPrevDate('20181230',2)."\n";
print "----------------------------------------\n";
print '対象日:20181131 -> 2ヶ月後:'.getNextDate('20181131',2)."\n";
print '対象日:20181131 -> 2ヶ月前:'.getPrevDate('20181131',2)."\n";
print '対象日:20181130 -> 2ヶ月後:'.getNextDate('20181130',2)."\n";
print '対象日:20181130 -> 2ヶ月前:'.getPrevDate('20181130',2)."\n";
print "----------------------------------------\n";

?>

以下がsample5.phpの実行結果となります。

対象日:20181231 -> 翌月:20190131
対象日:20181231 -> 前月:20181130
対象日:20181230 -> 翌月:20190130
対象日:20181230 -> 前月:20181130
----------------------------------------
対象日:20181131 -> 翌月:20181231
対象日:20181131 -> 前月:20181031
対象日:20181130 -> 翌月:20181230
対象日:20181130 -> 前月:20181030
----------------------------------------
対象日:20181231 -> 2ヶ月後:20190228
対象日:20181231 -> 2ヶ月前:20181031
対象日:20181230 -> 2ヶ月後:20190228
対象日:20181230 -> 2ヶ月前:20181030
----------------------------------------
対象日:20181131 -> 2ヶ月後:20190131
対象日:20181131 -> 2ヶ月前:20180930
対象日:20181130 -> 2ヶ月後:20190130
対象日:20181130 -> 2ヶ月前:20180930
----------------------------------------

これで、ちゃんと翌月と前月の日付を取ることができます!

前記事で作成したファイル削除のサンプルにも、組み込んで使いたいと思います!!

-PHP