PHP7.3時点ではDateTimeImmutable一択。
DateTimeImmutable
日付操作で問題になるのは『31日の一ヶ月後』の定義です。
その定義さえしっかりしておけば、どの手段を使おうが正しい結果を得ることはできます。
プログラマ的には「10月31日の一ヶ月後」は「12月1日」ですが、一般人に「10月31日の一ヶ月後は?」と聞いたら「11月30日」って返ってくると思うので、以下は11月30日であるという前提で話を進めます。
$dt = \DateTimeImmutable::createFromFormat('Y-m-d', '2018-10-31');
// 月初
echo $dt->modify('first day of next month')->format('Y-m-d'); // 2018-11-01
echo $dt->modify('first day of last month')->format('Y-m-d'); // 2018-09-01
// 月末
echo $dt->modify('first day of next month')->modify('last day of')->format('Y-m-d'); // 2018-11-30
echo $dt->modify('first day of last month')->modify('last day of')->format('Y-m-d'); // 2018-09-30
// 来月の今日
echo $dt->format('d') === $dt->modify('next month')->format('d')
? $dt->modify('next month')->format('Y-m-d')
: $dt->modify('last day of next month')->format('Y-m-d'); // 2018-11-30
PHP7.3現在、最も自然な書き方ができるのはDateTimeImmutableです。
PHPの日付操作は全て『文字通りの一ヶ月後』で計算を行う仕様となっているので、10月31日の一ヶ月後は11月31日、つまり12月1日になります。
何も考えず単純にmodify('next month')
とすると、場合によっては一般的感覚ではない日付になってしまうことがあるわけです。
そのため、計算の最初に'first day of'と入れ、日付を当月1日に変更しています。
これによって、月ごとの日数が異なることによる混乱を防ぐことが可能です。
なお、来月の今日を求めるすっきりした方法は思いつきませんでした。
何かいい書き方はないのかな?
DateInterval
日時間隔を表すためにDateIntervalというクラスがあります。
これを使って一ヶ月後を求めることもできるのですが、やめておいたほうがよいでしょう。
手間は増えるし『文字通りの一ヶ月後』になるしでいいことはありません。
$dt = \DateTimeImmutable::createFromFormat('Y-m-d', '2018-10-31');
echo $dt->add(new \DateInterval('P1M'))->format('Y-m-d'); // 2018-12-01
ではDateIntervalは何に使うのかというと、繰り返し処理です。
『10時間5分おきに一ヶ月間繰り返す』みたいなことを簡単に行うことができます。
単独で使う意味はあまりないクラスです。
foreach(new \DatePeriod(
new \DateTimeImmutable('2018-10-01 00:00:00'),
new \DateInterval('PT10H5M'),
new \DateTimeImmutable('2018-11-01 00:00:00')
) as $dt){
echo $dt->format('Y-m-d H:i:s'); // 10時間5分おきに一ヶ月
}
DateTime
DateTimeはPHPerにとって非常になじみの薄い動作をするので、できる限り使用を避けるべきです。
$dt = \DateTime::createFromFormat('Y-m-d', '2018-10-31');
// 月初
echo $dt->modify('first day of next month')->format('Y-m-d'); // 2018-11-01
echo $dt->modify('first day of last month')->format('Y-m-d'); // 2018-10-01 ←
DateTimeは、DateTimeImmutableと返り値は同じですが、値を返すと同時に自分自身も変更します。
最初のechoをした地点で、$dt
自体も2018-11-01 00:00:00
になってしまっているということです。
DateTimeを使っていると必ず引っかかる事故です。
$dt = \DateTime::createFromFormat('Y-m-d', '2018-10-31');
$dt2 = clone $dt;
// 月初
echo $dt->modify('first day of next month')->format('Y-m-d'); // 2018-11-01
echo $dt2->modify('first day of last month')->format('Y-m-d'); // 2018-09-01
単一日付から別の日付を計算したい場合は、別のインスタンスを作るかcloneを使いましょう。
cloneを使わず、うっかり$dt2 = $dt;
とか書くとやっぱり死にます。
これはオブジェクトの仕様なのでそうなるのは当然なのですが、PHPerはオブジェクトのコピーなんてしないからうっかりしがちなのだ。
date / mktime / strtotime
文字列だったりUNIXタイムスタンプだったりで汎用性に欠けるため、なるべく使わない方がよいでしょう。
strtotimeのなんでもかんでも突っ込むだけでタイムスタンプにしてくれる驚異の能力は、とても便利なんですけどね。
$ts = strtotime('2018-10-31'); // タイムスタンプ
// 月初
echo date('Y-m-d', mktime(date('H', $ts), date('i', $ts), date('s', $ts), date('n', $ts) + 1, 1, date('Y', $ts)) ); // 2018-11-01
echo date('Y-m-d', mktime(date('H', $ts), date('i', $ts), date('s', $ts), date('n', $ts) - 1, 1, date('Y', $ts)) ); // 2018-09-01
// 月末
echo date('Y-m-t', mktime(date('H', $ts), date('i', $ts), date('s', $ts), date('n', $ts) + 1, 1, date('Y', $ts)) ); // 2018-11-30
echo date('Y-m-t', mktime(date('H', $ts), date('i', $ts), date('s', $ts), date('n', $ts) - 1, 1, date('Y', $ts)) ); // 2018-09-30
// 来月の今日
echo $t1 === date('d', $t2 = mktime(0, 0, 0, $t3 = date('m', $ts) + 1, $t1 = date('d', $ts), $t4 = date('Y', $ts)))
? date('Y-m-d', $t2)
: date('Y-m-t', mktime(0, 0, 0, $t3, 1, $t4)); // 2018-11-30
来月の今日なんだこれ。
絶対もっとマシな書き方あるだろ。
おまけ:言語別『来月の今日』
PHPでは10月31日の一ヶ月後は12月1日ですが、他の言語はどうでしょうか。
言語によってバラバラですが、末日に丸め込まれるほうが多いようです。
PHP
上記のとおり、文字通り。
\DateTimeImmutable::createFromFormat('Y-m-d', '2018-10-31')->modify('next month'); // 2018-12-01
JavaScript
文字通り。
dt = new Date("2018/10/31 00:00:00");
dt.setMonth(dt.getMonth() + 1 ); // 2018-12-01
Bash
文字通り。
date -d "2018/10/31 00:00:00 1 month" // 2018-12-01
Perl
文字通り。
use Time::Piece;
Time::Piece->strptime("2018/10/31", "%Y/%m/%d")->add_months(1); // 2018-12-01
なでしこ
文字通り。
「2018/10/31」に「+0/1/0」を日付加算。
それを表示。 // 2018-12-01
Ruby
末日に丸め込み。
require 'date'
Date.parse('2018-10-31').next_month(1); // 2018-11-30
Python
末日に丸め込み。
from dateutil.relativedelta import relativedelta
from datetime import datetime
datetime(2018,10,31) + relativedelta(months=1) // 2018-11-30
Java
末日に丸め込み。
import java.time.LocalDate;
LocalDate.of(2018, 10, 31).plusMonths(1); // 2018-11-30
Csharp
末日に丸め込み。
DateTime.Parse("2018/10/31").AddMonths(1); // 2018-11-30
VisualBasic
末日に丸め込み。
Dim dt As DateTime = DateTime.Parse("2018/10/31").AddMonths(1) // 2018-11-30
MySQL
末日に丸め込み。
SELECT '2018/10/31 00:00:00' + INTERVAL 1 MONTH // 2018-11-30
PostgreSQL
末日に丸め込み。
SELECT timestamp '2018-10-31 00:00:00' + INTERVAL '1 MONTH' // 2018-11-30
Oracle
末日に丸め込み。
INTERVALでは指定日が存在しないとエラーになる。
SELECT ADD_MONTHS(DATE '2018-10-31', 1) FROM DUAL // 2018-11-30
SELECT DATE '2018-10-31' + INTERVAL '1' MONTH FROM DUAL // ORA-01839: date not valid for month specified
民法
末日に丸め込み。
ただし、月又は年によって期間を定めた場合において、最後の月に応当する日がないときは、その月の末日に満了する。
感想
通貨や単位系はいまだ世界中バラバラなのに、曜日や時間の仕組みは全世界でほぼ共通なのは不思議。