如何在 PHP 中检测不明确和无效的 DateTime? [英] How to detect Ambiguous and Invalid DateTime in PHP?

查看:21
本文介绍了如何在 PHP 中检测不明确和无效的 DateTime?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在处理用户提供的本地 DateTime 值时,由于夏令时转换,很可能会出现无效或不明确的时间.

When dealing with local DateTime values provided by a user, it's quite possible to have a time that is either invalid or ambiguous, due to Daylight Saving Time transitions.

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

In other languages and frameworks, there are often methods such as isAmbiguous and isValid, on some representation of the time zone. For example in .NET, there is TimeZoneInfo.IsAmbiguousTime and TimeZoneInfo.IsInvalidTime.

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

Plenty of other time zone implementations have similar methods, or functionality to address this concern. For example, in Python, the pytz library will throw an AmbiguousTimeError or InvalidTimeError exception that you can trap.

PHP 具有出色的时区支持,但我似乎找不到任何解决此问题的方法.我能找到的最接近的是 DateTimeZone::getTransitions.这提供了原始数据,所以我可以看到可以在此基础上编写一些方法.但它们是否已经存在于某个地方?如果没有,任何人都可以提供一个好的实现吗?我希望他们能像这样工作:

PHP has excellent time zone support, but I can't seem to find anything to address this. The closest thing I can find is DateTimeZone::getTransitions. That provides the raw data, so I can see that some methods could be written on top of this. But do they exist already somewhere? If not, can anyone provide a good implementation? I would expect them to work something like this:

$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

推荐答案

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

I 'm not aware of any existing implementations and I haven't had cause to use advanced date/time features such as these as yet, so here is a clean room implementation.

为了启用问题中说明的语法,我们将扩展 DateTimeZone 如下:

To enable the syntax illustrated in the question we are going to extend DateTimeZone as follows:

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 参数已使用正确的时区创建;这与问题中给出的示例代码形成对比.

To keep distracting details from cluttering the implementation, I am going to assume that the $date arguments have been created with the proper time zone; this is in contrast to the example code given in the question.

也就是说,这样不会产生正确的结果:

That is to say, the correct result will not be produced by this:

$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,因此应该很容易扩展方法以便消除此要求.无论如何,使界面超级用户友好超出了这个答案的范围;今后我将专注于技术细节.

Of course since $tz is already known to the object as $this, it should be easy to extend the methods so that this requirement is removed. In any case, making the interface super user friendly is out of the scope of this answer; going forward I will focus on the technical details.

这里的想法是使用 getTransitions 查看我们感兴趣的日期/时间周围是否有任何转换. getTransitions 将返回一个包含一个或两个元素的数组;开始"时间戳的时区情况将始终存在,如果在其后不久发生转换,则将存在另一个元素.MAX_DST_SHIFT 的值足够小,不可能获得第二个转换/第三个元素.

The idea here is to use getTransitions to see if there are any transitions around the date/time we are interested in. getTransitions will return an array with either one or two elements; the timezone situation for the "begin" timestamp will always be there, and another element will exist if a transition occurs shortly after it. The value of MAX_DST_SHIFT is small enough that there is no chance of getting a second transition/third element.

让我们看看代码:

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 计算的时间戳在纽约时区将相同.

The final point of the code depends on the fact that PHP's date functions calculate timestamps for invalid date/times as if wall clock time had not shifted. That is, the timestamps calculated for 2013-03-10 02:30:00 and 2013-03-10 03:30:00 will be identical on the New York timezone.

不难看出如何利用这一事实:创建一个与输入 $date 相等的新 DateTime 实例,然后将其向前移动 in挂钟时间项等于以秒为单位的 DST 偏移量(必须不考虑 DST 以进行此调整).如果结果的时间戳(这里 DST 规则起作用)等于输入的时间戳,则输入是无效的日期/时间.

It's not difficult to see how to take advantage of this fact: create a new DateTime instance equal to the input $date, then shift it forward in wall clock time terms an amount equal to the DST shift in seconds (it is imperative that DST not be taken into account to make this adjustment). If the timestamp of the result (here the DST rules come into play) is equal to the timestamp of the input, then the input is an invalid date/time.

实现与isValidTime很相似,只是有一些细节变化:

The implementation is quite similar to isValidTime, only a few details change:

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为绝对值).

The final point depends on another implementation detail of PHP's date functions: when asked to produce the timestamp for an ambiguous date/time, PHP produces the timestamp of the first (in absolute time terms) occurrence. This means that the timestamps of the latest ambiguous time and the earliest non-ambiguous time for a given DST change will differ by an amount larger than the DST offset (specifically, the difference will be in the range [offset + 1, 2 * offset], where offset is an absolute value).

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

The implementation takes advantage of this by again doing a "wall clock shift" forward and checking the timestamp difference between the result and the input $date.

查看实际代码.

这篇关于如何在 PHP 中检测不明确和无效的 DateTime?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆