PHPで来月やら前月やらを出すやつ：完全版

PHP7.3時点ではDateTimeImmutable一択。


# DateTimeImmutable

日付操作で問題になるのは『31日の一ヶ月後』の定義です。
その定義さえしっかりしておけば、どの手段を使おうが正しい結果を得ることはできます。

プログラマ的には「10月31日の一ヶ月後」は「12月1日」ですが、一般人に「10月31日の一ヶ月後は？」と聞いたら「11月30日」って返ってくると思うので、以下は11月30日であるという前提で話を進めます。

```php
$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](http://php.net/manual/ja/class.datetimeimmutable.php)です。

PHPの[日付操作](http://php.net/manual/ja/datetime.formats.relative.php#datetime.formats.relative)は全て『文字通りの一ヶ月後』で計算を行う仕様となっているので、10月31日の一ヶ月後は11月31日、つまり12月1日になります。
何も考えず単純に`modify('next month')`とすると、場合によっては一般的感覚ではない日付になってしまうことがあるわけです。
そのため、計算の最初に'first day of'と入れ、日付を当月1日に変更しています。
これによって、月ごとの日数が異なることによる混乱を防ぐことが可能です。

なお、来月の今日を求めるすっきりした方法は思いつきませんでした。
何かいい書き方はないのかな？

## DateInterval

日時間隔を表すために[DateInterval](http://php.net/manual/ja/class.dateinterval.php)というクラスがあります。
これを使って一ヶ月後を求めることもできるのですが、やめておいたほうがよいでしょう。
手間は増えるし『文字通りの一ヶ月後』になるしでいいことはありません。

```php
$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分おきに一ヶ月間繰り返す』みたいなことを簡単に行うことができます。
単独で使う意味はあまりないクラスです。

```php
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](http://php.net/manual/ja/class.datetime.php)はPHPerにとって非常になじみの薄い動作をするので、できる限り使用を避けるべきです。

```php
$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を使っていると必ず引っかかる事故です。

```php
$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](https://secure.php.net/manual/ja/language.oop5.cloning.php)を使いましょう。
cloneを使わず、うっかり`$dt2 = $dt;`とか書くとやっぱり[死にます](https://3v4l.org/LtaNq)。
これはオブジェクトの仕様なのでそうなるのは当然なのですが、PHPerはオブジェクトのコピーなんてしないからうっかりしがちなのだ。


# date / mktime / strtotime

文字列だったりUNIXタイムスタンプだったりで汎用性に欠けるため、なるべく使わない方がよいでしょう。
[strtotime](http://php.net/manual/ja/function.strtotime.php)のなんでもかんでも突っ込むだけでタイムスタンプにしてくれる驚異の能力は、とても便利なんですけどね。

```php
$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

上記のとおり、文字通り。

```php
\DateTimeImmutable::createFromFormat('Y-m-d', '2018-10-31')->modify('next month'); // 2018-12-01
```

## JavaScript

文字通り。

```js
dt = new Date("2018/10/31 00:00:00");
dt.setMonth(dt.getMonth() + 1 ); // 2018-12-01
```

## Bash

文字通り。

```bash
date -d "2018/10/31 00:00:00 1 month" // 2018-12-01
```

## Perl

文字通り。

```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

末日に丸め込み。

```ruby
require 'date'
Date.parse('2018-10-31').next_month(1); // 2018-11-30
```

## Python

末日に丸め込み。

```python
from dateutil.relativedelta import relativedelta
from datetime import datetime
datetime(2018,10,31) + relativedelta(months=1) // 2018-11-30
```

## Java

末日に丸め込み。

```java
import java.time.LocalDate;
LocalDate.of(2018, 10, 31).plusMonths(1); // 2018-11-30
```

## Csharp

末日に丸め込み。

```csharp
DateTime.Parse("2018/10/31").AddMonths(1); // 2018-11-30
```

## VisualBasic

末日に丸め込み。

```vb
Dim dt As DateTime = DateTime.Parse("2018/10/31").AddMonths(1) // 2018-11-30
```

## MySQL

末日に丸め込み。

```sql
SELECT '2018/10/31 00:00:00' + INTERVAL 1 MONTH // 2018-11-30
```

## PostgreSQL

末日に丸め込み。

```sql
SELECT timestamp '2018-10-31 00:00:00' + INTERVAL '1 MONTH' // 2018-11-30
```

## Oracle

末日に丸め込み。
INTERVALでは指定日が存在しないとエラーになる。

```sql
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
```

## 民法

末日に丸め込み。

```text

ただし、月又は年によって期間を定めた場合において、最後の月に応当する日がないときは、その月の末日に満了する。
```


# 感想

通貨や単位系はいまだ世界中バラバラなのに、曜日や時間の仕組みは全世界でほぼ共通なのは不思議。