PHPの日付・時刻関連をまとめてみた

目新しいものは何もないし、何番煎じだよという感じだし、そもそも公式マニュアルを読めば分かることだらけだけど、1つのページにまとまっていることが大事なんだと思うんだ（自己暗示）

# はじめに

現在、PHP には、`date()`、`strtotime()` をはじめとする関数と、PHP 5.2 から追加された `DateTime` クラスの2種類が存在します。しかし、`DateTime` クラスはまだ十分普及しているとは言えないようにも見えます。[^1]

[^1]: 自分の観測範囲が古すぎるだけかもしれません。ただ、私はまだ業務で `DateTime` クラスを使っているプログラムを見たことがありません。

そこで、この記事では、`DateTime` クラスの使い方について紹介したいと思います。また、既存のソースコードを読むときのことを考えて、`date()` をはじめとする関数の使い方についても書いておきたいと思います。

## 比較表

既存の関数が `DateTime` クラスだとどのメソッドに相当するのかを一目でわかったほうが取っ付きやすくていいのではないでしょうか。

ということで、比較表を作ってみました。あくまでも **機能が似ている** というだけで、**同じ動作をする** というわけではありません。

|       用途       |                     関数                     |    **`DateTime`** クラス    |
| --------------- | -------------------------------------------- | ---------------------- |
| 現在日時          | **`date()`**                                | **`__construct()`**   |
| 日時整形          | **`date(書式文字列)`**                        | **`format(書式文字列)`**      |
| タイムゾーン       | **`date_default_timezone_set(タイムゾーン)`** | **`setTimezone(タイムゾーン)`**    |
| 日時指定（文字列） | **`strtotime(文字列)`**                      | **`__construct(文字列)`**,**`modify(文字列)`**    |
| 日時指定（数値）   | **`mktime(時, 分, 秒, 月, 日, 年)`**          | **`setDate(年, 月, 日)`**, **`setTime(時, 分, 秒)`**    |
| 日時加減算        | （該当なし）                                  | **`modify(文字列)`**, **`add(間隔)`**,**`sub(間隔)`**      |
| 差分             | （該当なし）                                  | **`diff(比較対象)`**   |

##  DateTimeImmutable

`DateTime` クラスを実際に使いはじめる前に、知っておかなければならないことがあります。

例えば、`DateTime` クラスを使って以下のように書いたとします。

```php
// 現在日時
$now = new DateTime();
$now->format("Y-m-d H:i:s");

// 現在日時の1日後
$tomorrow = $now->modify("+1 day");
$tomorrow->format("Y-m-d H:i:s");

// 現在日時の1ヶ月後（のつもり）
$nextMonth = $now->modify("+1 month");
$nextMonth->format("Y-m-d H:i:s");
```

これを実行すると、以下のような結果になります。

```
// 現在日時
2019-01-17 13:02:40

// 現在日時の1日後
2019-01-18 13:02:40

// 現在日時の1ヶ月後・・・じゃない！
2019-02-18 13:02:40
```

`$now->modify("+1 month")` と書くことで、**現在日時の** 1ヶ月後を計算したつもりでした。しかし、実際には **現在日時の1日後の** 1ヶ月後となってしまいました。

これは、`DateTime` クラスが **ミュータブル** （変更可能）なクラスであることが原因です。

例えば、`$tomorrow = $now->modify("+1 day")` と書いたとき、実は `$tomorrow` に格納される日時だけでなく、`$now` に格納されている日時まで変更されてしまうのです。

一方、**イミュータブル** （変更不可能）なクラスとして `DateTimeImmutable` クラスが用意されています。これを使うと、先程のソースコードは以下のようになります。

```php
// 現在日時
// クラスを DateTime から DateTimeImmutable へ変更
$now = new DateTimeImmutable();
$now->format("Y-m-d H:i:s");

// 現在日時の1日後
$tomorrow = $now->modify("+1 day");
$tomorrow->format("Y-m-d H:i:s");

// 現在日時の1ヶ月後
$nextMonth = $now->modify("+1 month");
$nextMonth->format("Y-m-d H:i:s");
```

```
// 現在日時
2019-01-17 13:02:40

// 現在日時の1日後
2019-01-18 13:02:40

// 現在日時の1ヶ月後になっている！
2019-02-17 13:02:40
```

無用な混乱を避けるためにも、なるべく `DateTimeImmutable` クラスを使うべきでしょう。

以下のコード例では全て `DateTimeImmutable` クラスを使います。ただし、スペルが長いので、説明文では `DateTime` と書くことにします（両者の違いは **ミュータブル** か **イミュータブル** かだけです）。

# 現在日時
## DateTime

コンストラクターの第1引数に何も指定しない場合は、現在日時になります。
また、`format()` メソッドで、引数に指定した書式文字列で整形しています。

```php
$date = new DateTimeImmutable();
$date->format("Y-m-d H:i:s");    // 2019-01-17 13:44:18
```

## date()

`date()` 関数の第2引数に何も指定しない場合は、現在日時になります。
また、第1引数に指定した書式文字列で整形しています。

```php
date("Y-m-d H:i:s");             // 2019-01-17 13:44:18
```

# 書式文字列

`DateTime::format()` メソッドと `date()` 関数で引数に指定できる書式文字列については、`date()` 関数の [マニュアル](http://php.net/manual/ja/function.date.php) を参照してください。

代表的なものは以下のとおりです。

|       書式文字列       |                         説明                          |                例                 |
| --------------------- | ----------------------------------------------------- | --------------------------------- |
| **`d`**               | 日（数値2桁、先頭0埋めする）                              | **`01`** から **`31`**            |
| **`j`**               | 日（数値1〜2桁、先頭0埋めしない）                         | **`1`** から **`31`**             |
| **`D`**               | 曜日名（英語略称）                                      | **`Mon`** から **`Sun`**          |
| **`l`** (小文字の 'L') | 曜日名（英語名称）                                      | **`Sunday`** から **`Saturday`**  |
| **`w`**               | 曜日（数値1桁）                                         | **`0`** (日曜)から **`6`** (土曜)  |
| **`m`**               | 月（数値2桁、先頭0埋めする）                              | **`01`** から **`12`**            |
| **`n`**               | 月（数値1〜2桁、先頭0埋めしない）                         | **`1`** から **`12`**             |
| **`M`**               | 月名（英語略称）                                        | **`Jan`** から **`Dec`**          |
| **`F`**               | 月名（英語名称）                                        | **`January`** から **`December`** |
| **`Y`**               | 年（数値4桁）                                          | 例: **`1999`** または **`2003`**                              |
| **`y`**               | 年（数値2桁）                                          | 例: **`99`** または **`03`**                              |
| **`a`**               | 午前または午後（小文字）                                 | **`am`** または **`pm`**           |
| **`A`**               | 午前または午後（大文字）                                 | **`AM`** または **`PM`**           |
| **`g`**               | 時（12時間単位、数値1〜2桁、先頭0埋めしない）              | **`1`** から **`12`**             |
| **`G`**               | 時（24時間単位、数値1〜2桁、先頭0埋めしない）              | **`0`** から **`23`**             |
| **`h`**               | 時（12時間単位、数値2桁、先頭0埋めする）                   | **`01`** から **`12`**            |
| **`H`**               | 時（24時間単位、数値2桁、先頭0埋めする）                   | **`00`** から **`23`**            |
| **`i`**               | 分（数値2桁、先頭0埋めする）                              | **`00`** から **`59`**            |
| **`s`**               | 秒（数値2桁、先頭0埋めする）                              | **`00`** から **`59`**            |
| **`u`**               | マイクロ秒（PHP 5.2.2 以降）。 **`date()`** 関数では常に **`000000`** となる。**`DateTime`** クラスでは正常に表示される。    | 例: **`654321`**                  |
| **`v`**               | ミリ秒（PHP 7.0.0 以降）。**`date()`** 関数では常に **`000`** となる。**`DateTime`** クラスでは正常に表示される。     | 例: **`654`**                     |


## 例

日本語を書式文字列に含めることもできます。

```php
$date = new DateTimeImmutable();
$date->format("Y年n月j日 H時i分s秒");     // 2019年1月17日 21時16分02秒
```

書式文字列 `u` でマイクロ秒を表示することができますが、`date()` では表示できないことに注意しましょう。

```php
// DateTime クラスではマイクロ秒も表示可能
$date = new DateTimeImmutable();
$date->format("Y-m-d H:i:s.u");         // 2019-01-17 21:16:02.285594

// date() ではマイクロ秒は "000000" になってしまう
date("Y-m-d H:i:s.u");                  // 2019-01-17 21:16:02.000000
```

書式指定 `w` は曜日を数値で表します。日本語で曜日を表示したいときに便利です。

```php
$weekdays = ["日", "月", "火", "水", "木", "金", "土"];
$date = new DateTimeImmutable();
$dayOfWeek = $date->format("w");
echo "今日は" . $weekdays[$dayOfWeek] . "曜日です。\n";
// --> 今日は木曜日です。
```

# タイムゾーン

`DateTime` クラスや `date()` 関数を使う際には、タイムゾーンを適切に設定しておく必要があります。
基本的には `php.ini` の `date.timezone` でタイムゾーンを設定し、必要に応じて `DateTime::setTimezone()` メソッドや `date_default_timezone_set()` 関数を使います。[^2]

[^2]: タイムゾーン設定の優先順位については `date_default_timezone_get()` の [マニュアル](http://php.net/manual/ja/function.date-default-timezone-get.php) を参照。

設定できるタイムゾーンについては、[サポートされるタイムゾーンのリスト](http://php.net/manual/ja/timezones.php) を参照してください。

## php.ini

以下のように `date.timezone` が設定されていない場合には、[協定世界時](https://ja.wikipedia.org/wiki/%E5%8D%94%E5%AE%9A%E4%B8%96%E7%95%8C%E6%99%82) （UTC）が使われます。

```
[Date]
; Defines the default timezone used by the date functions
; http://php.net/date.timezone
;date.timezone =
```

日本標準時にする場合は `"Asia/Tokyo"` とします。

```
[Date]
; Defines the default timezone used by the date functions
; http://php.net/date.timezone
date.timezone = "Asia/Tokyo"
```

## DateTime::setTimezone()

`setTimezone()` メソッドの引数にタイムゾーンを指定することができます。
また、コンストラクターの第2引数にタイムゾーンを指定することもできます。

```php
// setTimezone() メソッドで米国東部標準時を指定する場合
$date = new DateTimeImmutable();
$date->setTimezone(new DateTimeZone("America/New_York"));
$date->format("Y-m-d H:i:s");       // 2019-01-18 09:17:09

// コンストラクターで米国東部標準時を指定する場合
$date = new DateTimeImmutable(NULL, new DateTimeZone("America/New_York"));
$date->format("Y-m-d H:i:s");       // 2019-01-18 09:17:09
```

## date_default_timezone_set()

`date_default_timezone_set()` 関数の引数にタイムゾーンを指定することができます。

```php
date_default_timezone_set("America/New_York");
date("Y-m-d H:i:s");                // 2019-01-18 09:17:09
```

# 日時指定
## その1

英文形式の文字列を使って日時を指定する方法です。

### DateTime

#### コンストラクター

コンストラクターの引数に文字列を指定することができます。

```php
// 2月1日 7時00分30秒
$date = new DateTimeImmutable("2019/2/1 7:00:30");
$date->format("Y-m-d H:i:s");
// --> 2019-02-01 07:00:30

// 一番近い未来の月曜日 9時30分
$date = new DateTimeImmutable("Monday 9:30");
$date->format("Y-m-d H:i:s");
// --> 2019-01-21 09:30:00

// 翌日 10時05分
$date = new DateTimeImmutable("+1 day 10:05");
$date->format("Y-m-d H:i:s");
// --> 2019-01-20 10:05:00
```

#### modify()

`DateTime` オブジェクトの日時をあとから変更する場合は、`modify()` メソッドを使うことができます。

```php
// 2月1日 7時00分30秒
$date = new DateTimeImmutable();
$date->modify("2019/2/1 7:00:30")->format("Y-m-d H:i:s");
// --> 2019-02-01 07:00:30

// 一番近い未来の月曜日 9時30分
$date = new DateTimeImmutable();
$date->modify("Monday 9:30")->format("Y-m-d H:i:s");
// --> 2019-01-21 09:30:00

// 翌日 10時05分
$date = new DateTimeImmutable();
$date->modify("Monday 9:30")->format("Y-m-d H:i:s");
// --> 2019-01-20 10:05:00
```

### strtotime()

`strtotime()` 関数を使って、英文形式の文字列を UNIX タイムスタンプへ変換します。
変換後のタイムスタンプを `date()` 関数の第2引数に指定します。

```php
date("Y-m-d H:i:s", strtotime("2019/2/1 7:00:30"));
// --> 2019-02-01 07:00:30

date("Y-m-d H:i:s", strtotime("Monday 9:30"));
// --> 2019-01-21 09:30:00

date("Y-m-d H:i:s", strtotime("+1 day  10:05"));
// --> 2019-01-20 10:05:00
```

### 書式

`DateTime` クラスのコンストラクター、`modify()` メソッド、そして `strtotime()` で指定できる書式は共通しています。詳しくは、[サポートする日付と時刻の書式](http://php.net/manual/ja/datetime.formats.php) を参照してください。

**2019年1月20日 12:59:00** を基準にして、それぞれの書式を指定したときにどのような日時になるかを以下にまとめました。

|             文字列              |                説明                 |       例        |
| ------------------------------ | ---------------------------------- | -------------- |
| **`+1 year`**                 | 1年後                               | **`2020-01-20 12:59:00`** |
| **`-1 year`**                 | 1年前                               | **`2018-01-20 12:59:00`**             |
| **`+1 week`**                 | 1週間後                             | **`2019-01-27 12:59:00`** |
| **`-1 week`**                 | 1週間前                             | **`2019-01-13 12:59:00`** |
| **`next Monday`**             | 一番近い未来の月曜日（時刻は00:00固定） | **`2019-01-21 00:00:00`**            |
| **`last Monday`**             | 一番近い過去の月曜日（時刻は00:00固定） | **`2019-01-14 00:00:00`**            |
| **`tomorrow`**                | 1日後（時刻は00:00固定）              | **`2019-01-21 00:00:00`**             |
| **`yesterday`**               | 1日前（時刻は00:00固定）              | **`2019-01-19 00:00:00`**             |
| **`+1 day`**                  | 1日後                               | **`2019-01-21 12:59:00`** |
| **`-1 day`**                  | 1日前                               | **`2019-01-19 12:59:00`**             |
| **`+1 hour`**                 | 1時間後                             | **`2019-01-20 13:59:00 `**             |
| **`-1 hour`**                 | 1時間前                             | **`2019-01-20 11:59:00`**             |
| **`first day of this month`** | 月の最初の日                         | **`2019-01-01 11:59:00`**             |
| **`last day of this month`**  | 月の最後の日                         | **`2019-01-31 12:59:00`**             |

## その2

日時の情報を引数に渡すことで、日時を指定する方法です。

### DateTime::setDate()、setTime()

`setDate()` メソッドで **年月日** を、`setTime()` メソッドで **時分秒** をそれぞれ指定します。

```php
$date = new DateTimeImmutable();
$lastday = $date->setDate(2019, 1, 31)->setTime(12, 59, 0);
$lastday->format("Y-m-d H:i:s");
// --> 2019-01-31 12:59:00
```

### mktime()

`mktime()` 関数を使って、日時情報を UNIX タイムスタンプへ変換します。
変換後のタイムスタンプを `date()` 関数の第2引数に指定します。

```php
// mktime()の引数の順番は以下のとおり:
// 時, 分, 秒, 月, 日, 年
date("Y-m-d H:i:s", mktime(12, 59, 0, 1, 31, 2019)); // 2019-01-31 12:59:00
```

## その3
### DateTime::add()、sub()

間隔を表す `DateInterval` オブジェクトを作成し、`DateTime::add()` または `DateTime::sub()` に指定することで、日時の加減算を行う方法です。

日時を加算するときは `add()` メソッドを、減算するときは `sub()` メソッドをそれぞれ使います。

書式については `DateInterval` クラスのコンストラクターの [マニュアル](http://php.net/manual/ja/dateinterval.construct.php) を参照してください。最初が `P` からはじまることと、日付と時間のあいだに `T` を置く必要があることに注意が必要です。[^3]

[^3]: `M` は **月** (Month) と **分** （Minute）の両方で使われています。全体的に見づらいですが、ISO 規格らしいので仕方ありませんね・・・

以下に **2019年1月20日 12:59:00** を基準にしたときに、それぞれどのような日時になるかを示します。

```php
$date = new DateTimeImmutable("2019/1/20 12:59:00");

// 1年後
$date->add(new DateInterval("P1Y"))->format("Y-m-d H:i:s");
// --> 2020-01-20 12:59:00

// 3週間前
$date->sub(new DateInterval("P3W"))->format("Y-m-d H:i:s");
// --> 2018-12-30 12:59:00

// 1日12時間後
$date->add(new DateInterval("P1DT12H"))->format("Y-m-d H:i:s");
// --> 2019-01-22 00:59:00

// 1時間30分前
$date->sub(new DateInterval("PT1H30M"))->format("Y-m-d H:i:s");
// --> 2019-01-20 11:29:00

// 5分30秒後
$date->add(new DateInterval("PT5M30S"))->format("Y-m-d H:i:s");
// --> 2019-01-20 13:04:30
```

## 注意事項

以下のように、1ヶ月後や1ヶ月前の日付を指定しようとすると、期待通りの結果にならないことがあります。

詳しくは、別記事 [PHPで1ヶ月前や1ヶ月後の日付を求めるには](https://qiita.com/aminevsky/items/7f13c7676277c9ddc91f) を参照してください。

```php
$date = new DateTimeImmutable("2019/1/31 10:00");
$date->modify("+1 months")->format("Y-m-d H:i:s");
// --> 2019-03-03 10:00:00
```

# 日時比較
## 比較演算子
`DateTime` オブジェクトは比較演算子（宇宙船演算子を除く）で比較することができます。
また、等しいことを確認するときは `===` 演算子ではなく `==` 演算子を使います。

```php
$date1 = new DateTimeImmutable("2019-01-20 10:00:00");
$date2 = new DateTimeImmutable("2019-01-20 10:00:00");
$date3 = new DateTimeImmutable("2019-01-21 12:00:00");

var_dump($date1 == $date2);  // bool(true)
var_dump($date1 != $date2);  // bool(false)

// ===演算子はfalseになる
var_dump($date1 === $date2); // bool(false)

var_dump($date1 < $date2);   // bool(false)
var_dump($date1 > $date2);   // bool(false)
var_dump($date1 <= $date2);  // bool(true)
var_dump($date1 >= $date2);  // bool(true)

// 宇宙船演算子を使うとエラーになる
//var_dump($data1 <=> $data2); 


var_dump($date1 == $date3);  // bool(false)
var_dump($date1 != $date3);  // bool(true)
var_dump($date1 === $date3); // bool(false)
var_dump($date1 < $date3);   // bool(true)
var_dump($date1 > $date3);   // bool(false)
var_dump($date1 <= $date3);  // bool(true)
var_dump($date1 >= $date3);  // bool(false)


// 同一インスタンスを参照している場合
// ===演算子はtrueになる
$date4 = $date1;
var_dump($date4 === $date1); // bool(true)
```

## DateTime::diff()

`DateTime::diff()` を使って、日時の差を計算することができます。
`DateTime::diff()` は `DateInterval` オブジェクトを返すので、`DateInterval::format()` を使って整形します。

`DateInterval::format()` の引数に指定できる書式文字列については、[マニュアル](http://php.net/manual/ja/dateinterval.format.php) を参照してください。

例えば、東京オリンピック開催日までの日数を計算する場合は、以下のようになります。

```php
$today = new DateTimeImmutable("2019-1-20");
$olymicStartDay = new DateTimeImmutable("2020-7-24");

// オリンピック開催日までの日数
// %a（総日数）を使っていることに注意
$interval = $today->diff($olymicStartDay);
print $interval->format("オリンピックまで あと %a 日") . "\n";
```

```
オリンピックまで あと 551 日
```

ここで書式文字列に `%a` （総日数）を指定していることに注意してください。
`%d` （日）を指定した場合は、2019年1月**20**日 の「日」（ **20** ）と 2020年7月**24**日の「日」（**24**）の差を計算して、`4` という結果が返ってきてしまいます。

その他の書式文字列の使い方は、以下のようになります。

```php
$today = new DateTimeImmutable("2019/1/20 18:00:00");
$future = new DateTimeImmutable("2019/2/22 19:30:45");
print $today->format("Y-m-d H:i:s") . "\n";
print $future->format("Y-m-d H:i:s") . "\n";
print $today->diff($future)->format("間隔: %R%y年%m月%d日%h時%i分%s秒");
```

```
2019-01-20 18:00:00
2019-02-22 19:30:45
間隔: +0年1月2日1時30分45秒
```


# 参考

- 全般
    - [PHP 公式マニュアル](http://php.net/manual/ja/book.datetime.php)
    - [DateTime クラスのまとめメモ](https://qiita.com/re-24/items/c3ed814f2e1ee0f8e811)
    - [日付時刻関連のクラスを活用しよう](https://qiita.com/mpyw/items/e42fa173e5a26220a785)

- 書式指定
    - 全般
        - [日付時刻関連で使われる書式の調査](https://qiita.com/mpyw/items/55df438d12ef8e94dbc5)
    - 書式指定（日時出力）のマイクロ秒について
        - [[PHP] ミリ秒やマイクロ秒を含む現在時間を取得するクラスと関数](https://joyplot.com/documents/2018/07/16/php-microtime-millisecond/)


# 最後に

今まで `DateTime` クラスを理解していなかった（使うこともなかった）のですが、良い勉強になりました。

# 更新履歴
- 2019/1/20
    - 参考リンク追加
- 2020/8/6
    - 書式文字列 `v` について修正（編集リクエストありがとうございました）