PHP の DateTime クラスにて存在しない日付を表す文字列をパースしたらどうなるのか

## 概要

PHP の DateTime クラスにて存在しない日付を表す文字列をパースしたらどうなるのかサンプルコードを書いて調べてみた。

## 存在しない日付の例

- 2017年12月32日 (大晦日の次の日)
- 2019年2月29日 (うるう年ではない年の2月29日)

## 動作確認環境

macOS Sierra + PHP 7.2

```
$ php --version
PHP 7.2.0 (cli) (built: Dec  3 2017 21:47:51) ( NTS )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2017 Zend Technologies
```

## ソースコード

```php
<?php

class MyDateTime {

  private $name;
  private $datetime;
  private $errors;

  private const FORMAT = 'Y-m-d';
  private const TIME_ZONE = 'Asia/Tokyo';

  public function __construct($dateString) {

    $timeZone = new DateTimeZone(self::TIME_ZONE);
    if (isset($dateString)) {
      $this->name = $dateString;
      $this->datetime = DateTime::createFromFormat(self::FORMAT, $dateString, $timeZone);
    } else {
      $this->name = 'now       ';
      $this->datetime = new DateTime(NULL, $timeZone);
    }
    $this->errors = DateTime::getLastErrors();
  }

  public function getErrors() {
    if ($this->errors['warning_count'] > 0 || $this->errors['error_count'] > 0) {
      return $this->errors;
    } else {
      return NULL;
    }
  }

  public function __toString() {

    $result = $this->name . ' -> ' . $this->datetime->format('Y-m-d H:i:s (U)');

    $errorMessage = $this->getErrorMessage();
    if ($errorMessage !== '') {
      $result .= ' [' . $errorMessage . ']';
    }

    $result .= "\n";
    return $result;
  }

  private function getErrorMessage() {

    $array = [];
    if ($this->errors['warning_count'] > 0) {
      $array = array_merge_recursive($array, $this->errors['warnings']);
    }
    if ($this->errors['error_count'] > 0) {
      $array = array_merge_recursive($array, $this->errors['errors']);
    }

    if (empty($array) ) {
      return '';
    } else {
      return implode('/', $array);
    }
  }

}

$dateList = [
  NULL,
  '2017-12-31',
  '2017-12-32', // 存在しない日
  '2018-01-01',
  '2019-02-29', // うるう年じゃない
  '2019-03-01', // うるう年じゃない
  '2020-02-29', // うるう年
  '2020-03-01', // うるう年
];

foreach ($dateList as $dateString) {
  $datetime = new MyDateTime($dateString);
  echo $datetime;
  if (!is_null($errors = $datetime->getErrors())) {
    print_r($errors);
  }
}
```

## 実行結果

```
now        -> 2017-12-13 22:41:33 (1513172493)
2017-12-31 -> 2017-12-31 22:41:33 (1514727693)
2017-12-32 -> 2018-01-01 22:41:33 (1514814093) [The parsed date was invalid]
Array
(
    [warning_count] => 1
    [warnings] => Array
        (
            [10] => The parsed date was invalid
        )

    [error_count] => 0
    [errors] => Array
        (
        )

)
2018-01-01 -> 2018-01-01 22:41:33 (1514814093)
2019-02-29 -> 2019-03-01 22:41:33 (1551447693) [The parsed date was invalid]
Array
(
    [warning_count] => 1
    [warnings] => Array
        (
            [10] => The parsed date was invalid
        )

    [error_count] => 0
    [errors] => Array
        (
        )

)
2019-03-01 -> 2019-03-01 22:41:33 (1551447693)
2020-02-29 -> 2020-02-29 22:41:33 (1582983693)
2020-03-01 -> 2020-03-01 22:41:33 (1583070093)
```

- 存在しない日付の 2017-12-32 は 2018-01-01 として解釈された
- 存在しない日付の 2019-02-29 は 2019-03-01 として解釈された

パースの際にエラーは発生しないものの DateTime::getLastErrors を利用すると「The parsed date was invalid」という警告情報を取得することができる。

他に存在しない日付かどうかチェックする方法としては checkdate 関数が用意されている。

[PHP: checkdate \- Manual](http://php.net/manual/ja/function.checkdate.php)

> bool checkdate ( int \$month , int \$day , int \$year )
> 引数で指定された日付の妥当性をチェックします。 各パラメータが適切に指定されている場合に、妥当であると判断されます。

存在しない日付をパースしたときの挙動は、Java の java.util.Calendar クラスの非厳密な解釈に似ている。

[Calendar \(Java SE 9 & JDK 9 \)](https://docs.oracle.com/javase/jp/9/docs/api/java/util/Calendar.html)

> public void setLenient​(boolean lenient)
> 日付/時間の解釈を厳密に行うかどうかを設定します。 厳密でない解釈では、「1996年2月942日」のような日付は、1996年2月1日から第941日目と同じこととみなされます。 厳密な解釈では、このような日付の場合、例外がスローされます。 デフォルトは非厳密です。

## 参考資料

- [PHP: Date/Time \- Manual](http://php.net/manual/ja/book.datetime.php)
- [Calendar \(Java SE 9 & JDK 9 \)](https://docs.oracle.com/javase/jp/9/docs/api/java/util/Calendar.html)