Как обнаружить неоднозначное и неверное значение DateTime в PHP?

При использовании локальных значений DateTime предоставленных пользователем, вполне возможно иметь время, которое является либо недопустимым, либо неоднозначным из-за перехода на летнее время.

В других языках и isAmbiguous часто используются такие методы, как isAmbiguous и isValid , при некотором представлении часового пояса. Например, в .NET существует TimeZoneInfo.IsAmbiguousTime и TimeZoneInfo.IsInvalidTime .

Множество других реализаций часовых поясов имеют сходные методы или функциональные возможности для решения этой проблемы. Например, в Python библиотека InvalidTimeError исключение AmbiguousTimeError или InvalidTimeError которое вы можете захватить.

PHP имеет отличную поддержку часового пояса, но я не могу найти ничего, чтобы решить эту проблему. Самое близкое, что я могу найти, это DateTimeZone :: getTransitions . Это обеспечивает исходные данные, поэтому я могу видеть, что над этим можно было бы написать некоторые методы. Но они уже существуют где-то? Если нет, может ли кто-нибудь обеспечить хорошую реализацию? Я ожидаю, что они будут работать примерно так:

 $tz = new DateTimeZone('America/New_York'); echo $tz->isValidTime(new DateTime('2013-03-10 02:00:00')); # false echo $tz->isAmbiguousTime(new DateTime('2013-11-03 01:00:00')); # true 

Я не знаю каких-либо существующих реализаций, и у меня не было причин использовать расширенные функции даты и времени, такие как эти, пока что это чистая реализация комнаты.

Чтобы включить синтаксис, проиллюстрированный в вопросе, мы будем расширять DateTimeZone следующим образом:

 class DateTimeZoneEx extends DateTimeZone { const MAX_DST_SHIFT = 7200; // let's be generous // DateTime instead of DateTimeInterface for PHP < 5.5 public function isValidTime(DateTimeInterface $date); public function isAmbiguousTime(DateTimeInterface $date); } 

Чтобы отвлекать детали от загромождения реализации, я собираюсь предположить, что аргументы $date были созданы с соответствующим часовым поясом; это в отличие от кода примера, заданного в вопросе.

То есть, правильный результат не будет производиться этим:

 $tz = new DateTimeZoneEx('America/New_York'); echo $tz->isValidTime(new DateTime('2013-03-10 02:00:00')); 

но вместо этого:

 $tz = new DateTimeZoneEx('America/New_York'); echo $tz->isValidTime(new DateTime('2013-03-10 02:00:00', $tz)); 

Конечно, поскольку $tz уже известен объекту как $this , его следует легко расширить, чтобы это требование было удалено. В любом случае, создание интерфейса супер-дружественный интерфейс выходит за рамки этого ответа; В будущем я сосредоточусь на технических деталях.

isValidTime

Идея здесь заключается в использовании getTransitions чтобы увидеть, есть ли какие-либо переходы вокруг интересующей нас даты / времени. getTransitions вернет массив с одним или двумя элементами; ситуация временной зоны для отметки «начать» всегда будет там, и другой элемент будет существовать, если переход произойдет вскоре после него. Значение MAX_DST_SHIFT достаточно мало, чтобы не было возможности получить второй переходный / третий элемент.

Посмотрим код:

 public function isValidTime(DateTime $date) { $ts = $date->getTimestamp(); $transitions = $this->getTransitions( $ts - self::MAX_DST_SHIFT, $ts + self::MAX_DST_SHIFT ); if (count($transitions) == 1) { // No DST changes around here, so obviously $date is valid return true; } $shift = $transitions[1]['offset'] - $transitions[0]['offset']; if ($shift < 0) { // The clock moved backward, so obviously $date is valid // (although it might be ambiguous) return true; } $compare = new DateTime($date->format('Ymd H:i:s'), $this); return $compare->modify("$shift seconds")->getTimestamp() != $ts; } 

Конечная точка кода зависит от того, что функции даты PHP вычисляют временные метки для недопустимой даты / времени, как если бы часы настенных часов не сдвигались. То есть, отметки времени, рассчитанные на 2013-03-10 02:30:00 и 2013-03-10 03:30:00 будут идентичны по часовой 2013-03-10 03:30:00 в Нью-Йорке.

Нетрудно понять, как воспользоваться этим фактом: создать новый экземпляр DateTime равный входной $date , а затем сдвинуть его вперед в режиме времени настенных часов на сумму, равную сдвигу DST в секундах (необходимо, чтобы DST не быть принята во внимание, чтобы внести эту корректировку). Если временная метка результата (здесь вступают в действие правила DST) равна отметке времени ввода, то вход является неверной датой / временем.

isAmbiguousTime

Реализация очень похожа на isValidTime , изменение лишь нескольких деталей:

 public function isAmbiguousTime(DateTime $date) { $ts = $date->getTimestamp(); $transitions = $this->getTransitions( $ts - self::MAX_DST_SHIFT, $ts + self::MAX_DST_SHIFT); if (count($transitions) == 1) { return false; } $shift = $transitions[1]['offset'] - $transitions[0]['offset']; if ($shift > 0) { // The clock moved forward, so obviously $date is not ambiguous // (although it might be invalid) return false; } $shift = -$shift; $compare = new DateTime($date->format('Ymd H:i:s'), $this); return $compare->modify("$shift seconds")->getTimestamp() - $ts > $shift; } 

Конечная точка зависит от другой детали реализации функций даты PHP: когда ее просят создать временную метку для двусмысленной даты / времени, PHP создает временную метку первого (в абсолютном времени) возникновения. Это означает, что временные метки последнего двусмысленного времени и самое раннее недвусмысленное время для данного изменения DST будут отличаться на величину, большую, чем смещение DST (в частности, разница будет в диапазоне [ offset + 1 , 2 * offset ], где offset является абсолютным значением).

Реализация воспользуется этим, снова сделав «сдвиг часов на стене» вперед и проверив разницу между меткой времени и результатом ввода $date .

См. Код в действии .