日付文字列の検証（バリデーション）

たとえば、内部的にDateTimeオブジェクトなどを使って日時を扱っているとき、日時や日付を表す値を文字列として受け取ったとき、どのように検証（バリデーション）していますか？

フレームワークによっては、フォームからの入力値を自動的に検証して、DateTimeオブジェクトに変換してくれるものもありますが、たとえば、GETパラメータで受け取ったときや、CSVを使って一括で処理する場合などには、より柔軟なフォーマット定義が必要な場合があります。

```php
$dateTime = new \DateTime('hoge');
// Exception: DateTime::__construct(): Failed to parse time string (hoge)
```

私の場合は、次のような日付文字列を検証するためのメソッドを作成しました。

```php:DateTimeUtil.php
class DateTimeUtil
{
    /**
     * 日時文字列を DateTime オブジェクトに変換
     *
     * @param string $format 日付フォーマット
     * @param string $string 日付文字列
     * @return \DateTime|null
     */
    public static function convertStringToDateTime($format, $string)
    {
        $dateTime = \DateTime::createFromFormat($format, $string);

        if (!$dateTime instanceof \DateTime || $dateTime->format($format) !== $string || $dateTime->getTimestamp() < 0) {
            return null;
        }

        return $dateTime;
    }

    /**
     * 日付文字列を検証します
     *
     * @param string $string
     * @param array $formats
     * @return bool
     */
    public static function validateDateString($string, $formats = array())
    {
        if (empty($formats)) {
            $formats = array(
                'Y-m-d',    // 2012-01-01
                'Y/m/d',    // 2012/01/01
                'Y/n/j',    // 2012/1/1
                'Y年n月j日', // 2012年1月1日
            );
        }

        foreach ($formats as $format) {
            $datetime = self::convertStringToDateTime($format, $string);

            if ($datetime instanceof \DateTime) {
                return true;
            }
        }

        return false;
    }

    /**
     * 日時文字列を検証します

     * @param string $string
     * @param array $formats
     * @return bool
     */
    public static function validateDateTimeString($string, $formats = array())
    {
        if (empty($formats)) {
            $formats = array(
                'Y-m-d H:i:s',   // 2012-01-01 01:01:01
                'Y/m/d H:i:s',   // 2012/01/01 01:01:01
                'Y/n/j G:i:s',   // 2012/1/1 1:01:01
                'Y-m-d\TH:i:sP', // 2012-01-01T01:01:01+00:00
            );
        }

        foreach ($formats as $format) {
            $datetime = self::convertStringToDateTime($format, $string);

            if ($datetime instanceof \DateTime) {
                return true;
            }
        }

        return false;
    }
}
```

```php:DateTimeUtilTest.php
class StringUtilTest extends \PHPUnit_Framework_TestCase
{
    public function testValidateDateString()
    {
        // 正常系
        $this->assertEquals(StringUtil::validateDateString('2014-01-01'), true);
        $this->assertEquals(StringUtil::validateDateString('2014/01/01'), true);
        $this->assertEquals(StringUtil::validateDateString('2014/1/1'), true);
        $this->assertEquals(StringUtil::validateDateString('2014年1月1日'), true);

        // 不正なフォーマット
        $this->assertEquals(StringUtil::validateDateTimeString('2014/01/01', array('Y/n/j')), false);

        // 不正な年
        $this->assertEquals(StringUtil::validateDateString('/1/1'), false);
        $this->assertEquals(StringUtil::validateDateString('0/1/1'), false);
        $this->assertEquals(StringUtil::validateDateString('1/1/1'), false);
        $this->assertEquals(StringUtil::validateDateString('10/1/1'), false);
        $this->assertEquals(StringUtil::validateDateString('100/1/1'), false);
        $this->assertEquals(StringUtil::validateDateString('20a1/1/1'), false);
        $this->assertEquals(StringUtil::validateDateString('0100/1/1'), false);
        $this->assertEquals(StringUtil::validateDateString('20121/1/1'), false);

        // 不正な月
        $this->assertEquals(StringUtil::validateDateString('2012//1'), false);
        $this->assertEquals(StringUtil::validateDateString('2012/0/1'), false);
        $this->assertEquals(StringUtil::validateDateString('2012/13/1'), false);
        $this->assertEquals(StringUtil::validateDateString('2012/100/1'), false);
        $this->assertEquals(StringUtil::validateDateString('2012/1a/1'), false);
        $this->assertEquals(StringUtil::validateDateString('2012/01/1'), false);

        // 不正な日
        $this->assertEquals(StringUtil::validateDateString('2012/1/'), false);
        $this->assertEquals(StringUtil::validateDateString('2012/1/0'), false);
        $this->assertEquals(StringUtil::validateDateString('2012/1/01'), false);
        $this->assertEquals(StringUtil::validateDateString('2012/1/32'), false);
        $this->assertEquals(StringUtil::validateDateString('2012/1/1a'), false);
        $this->assertEquals(StringUtil::validateDateString('2012/1/100'), false);

        // うるう年の判定
        $this->assertEquals(StringUtil::validateDateString('2012/2/29'), true);
        $this->assertEquals(StringUtil::validateDateString('2014/2/29'), false);
    }

    public function testValidateDateTimeString()
    {
        // 正常系
        $this->assertEquals(StringUtil::validateDateTimeString('2014-01-01 01:01:01'), true);
        $this->assertEquals(StringUtil::validateDateTimeString('2014/01/01 01:01:01'), true);
        $this->assertEquals(StringUtil::validateDateTimeString('2014/1/1 1:01:01'), true);
        $this->assertEquals(StringUtil::validateDateTimeString('2014-01-01T01:01:01+00:00'), true);

        // 不正なフォーマット
        $this->assertEquals(StringUtil::validateDateTimeString('2014-01-01T01:01:01+00:00', array('Y/n/j G:i:s')), false);

        // 不正な時
        $this->assertEquals(StringUtil::validateDateTimeString('2014/1/1 25:00:00'), false);
        $this->assertEquals(StringUtil::validateDateTimeString('2014/1/1 100:00:00'), false);
        $this->assertEquals(StringUtil::validateDateTimeString('2014/1/1 :00:00'), false);

        // 不正な分
        $this->assertEquals(StringUtil::validateDateTimeString('2014/1/1 1'), false);
        $this->assertEquals(StringUtil::validateDateTimeString('2014/1/1 1:60:00'), false);
        $this->assertEquals(StringUtil::validateDateTimeString('2014/1/1 1:100:00'), false);
        $this->assertEquals(StringUtil::validateDateTimeString('2014/1/1 1::00'), false);

        // 不正な秒
        $this->assertEquals(StringUtil::validateDateTimeString('2014/1/1 1:01'), false);
        $this->assertEquals(StringUtil::validateDateTimeString('2014/1/1 1:01:60'), false);
        $this->assertEquals(StringUtil::validateDateTimeString('2014/1/1 1:01:100'), false);
        $this->assertEquals(StringUtil::validateDateTimeString('2014/1/1 1:01:'), false);
    }
}
```

ただ、もっといい方法があるような気が…