【问题标题】:How to detect Ambiguous and Invalid DateTime in PHP?如何在 PHP 中检测不明确和无效的 DateTime?
【发布时间】:2013-08-26 21:26:13
【问题描述】:

在处理用户提供的本地 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

【问题讨论】:

    标签: php datetime timezone dst


    【解决方案1】:

    我不知道任何现有的实现,我还没有理由使用诸如此类的高级日期/时间功能,所以这里是一个无尘室实现。

    为了启用问题中说明的语法,我们将扩展 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 计算的时间戳在纽约时区将是相同的。

    不难看出如何利用这一事实:创建一个等于输入 $date 的新 DateTime 实例,然后将其向前移动以挂钟时间的形式等于以秒为单位的 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 + 1, 2 * offset],其中offset 是绝对值)。

    实现通过再次向前执行“挂钟移位”并检查结果与输入 $date 之间的时间戳差异来利用这一点。

    See the code in action.

    【讨论】:

    • 非常好的和完整的答案!我希望所有的答案都经过深思熟虑和描述!只要 StackOverflow 允许,我就会奖励赏金。
    • 这正是我想要的。赏金。再次感谢!
    • @MattJohnson:谢谢,很高兴为您提供帮助。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-08-19
    • 1970-01-01
    • 1970-01-01
    • 2010-11-02
    • 1970-01-01
    • 1970-01-01
    • 2022-01-25
    相关资源
    最近更新 更多