При использовании локальных значений 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
.
См. Код в действии .