如何在PHP中检测不明确和无效的DateTime?

时间:2013-08-23 18:22:12

标签: php datetime timezone dst

当处理用户提供的本地DateTime值时,由于夏令时转换,很可能会产生无效或不明确的时间。

在其他语言和框架中,对于时区的某些表示,通常有isAmbiguousisValid等方法。例如,在.NET中,有TimeZoneInfo.IsAmbiguousTimeTimeZoneInfo.IsInvalidTime

许多其他时区实现都有类似的方法或功能来解决这个问题。例如,在Python中,pytz library将抛出您可以捕获的AmbiguousTimeErrorInvalidTimeError异常。

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

1 个答案:

答案 0 :(得分:7)

我不知道任何现有的实现,我还没有理由使用这些高级日期/时间功能,所以这里是一个干净的房间实现。

要启用问题中说明的语法,我们将按如下方式扩展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('Y-m-d H:i:s'), $this);

    return $compare->modify("$shift seconds")->getTimestamp() != $ts;
}

代码的最后一点取决于PHP的日期函数计算无效日期/时间的时间戳这一事实,就好像挂钟时间没有移位一样。也就是说,为2013-03-10 02:30:002013-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('Y-m-d H:i:s'), $this);
    return $compare->modify("$shift seconds")->getTimestamp() - $ts > $shift;
}

最后一点取决于PHP日期函数的另一个实现细节:当被要求生成模糊日期/时间的时间戳时,PHP会生成第一个(绝对时间项)出现的时间戳。这意味着最新模糊时间的时间戳和给定DST变化的最早非模糊时间将相差大于DST偏移的数量(具体地,差异将在范围内[ offset + 12 * offset],其中offset是绝对值。)

该实现通过再次向前执行“挂钟转换”并检查结果与输入$date之间的时间戳差异来利用此功能。

<强> See the code in action