dpmpa26468 2017-03-28 08:59
浏览 96
已采纳

DST没有在某些相对时间内计算

Last Sunday the European Union switched from CET (+0100) to CEST (+0200). I'm writing code to apply an increment to a date and it isn't working properly because time zone transition is only properly taken into account with some relative formats:

  • '+x minutes' omits the missing hour
  • '+x hours' doesn't

Here's my test code:

echo 'Time zone database: ' . timezone_version_get() . PHP_EOL;
echo PHP_EOL;

date_default_timezone_set('Europe/Madrid');

$start = new DateTime('2017-03-26 01:59:00');
$increments = array(
    '+2 minutes' => '2017-03-26 03:01:00',
    '+2 hours'   => '2017-03-26 04:59:00',
);

echo 'Start:        ' . $start->format('r') . PHP_EOL;
foreach ($increments as $increment => $expected_string) {
    echo '>>> ' . $increment . PHP_EOL;

    $expected_end = new DateTime($expected_string);
    $actual_end = clone $start;
    $actual_end->modify($increment);

    echo 'Expected end: ' . $expected_end->format('r') . PHP_EOL;
    echo 'Actual end:   ' . $actual_end->format('r') . PHP_EOL;
    echo ($expected_end->format('c')===$actual_end->format('c')  ? 'OK' : 'ERROR') . PHP_EOL;
    echo PHP_EOL;
}

(run online)

Time zone database: 2016.3

Start:        Sun, 26 Mar 2017 01:59:00 +0100
>>> +2 minutes
Expected end: Sun, 26 Mar 2017 03:01:00 +0200
Actual end:   Sun, 26 Mar 2017 03:01:00 +0200
OK

>>> +2 hours
Expected end: Sun, 26 Mar 2017 04:59:00 +0200
Actual end:   Sun, 26 Mar 2017 03:59:00 +0200
ERROR

Since relative formats are often so counter-intuitive I'm not sure whether I'm getting some documented behaviour or it's a bug.

Can you shed some light on it?

  • 写回答

1条回答 默认 最新

  • dpppic5186 2017-03-28 09:21
    关注

    It can't be a relative format misunderstanding because behaviour is erratic within the same format:

    date_default_timezone_set('Europe/Madrid');
    
    $start = new DateTime('2017-03-26 01:59:00');
    $increments = array(
        '+60 minutes'   => '2017-03-26 03:59:00',
        '+61 minutes'   => '2017-03-26 04:00:00',
    );
    
    echo 'Start:        ' . $start->format('r') . PHP_EOL;
    foreach ($increments as $increment => $expected_string) {
        echo '>>> ' . $increment . PHP_EOL;
    
        $expected_end = new DateTime($expected_string);
        $actual_end = clone $start;
        $actual_end->modify($increment);
    
        echo 'Expected end: ' . $expected_end->format('r') . PHP_EOL;
        echo 'Actual end:   ' . $actual_end->format('r') . PHP_EOL;
        echo ($expected_end->format('c')===$actual_end->format('c')  ? 'OK' : 'ERROR') . PHP_EOL;
        echo PHP_EOL;
    }
    

    (run online)

    Start:        Sun, 26 Mar 2017 01:59:00 +0100
    >>> +60 minutes
    Expected end: Sun, 26 Mar 2017 03:59:00 +0200
    Actual end:   Sun, 26 Mar 2017 03:59:00 +0200
    OK
    
    >>> +61 minutes
    Expected end: Sun, 26 Mar 2017 04:00:00 +0200
    Actual end:   Sun, 26 Mar 2017 03:00:00 +0200
    ERROR
    

    In other words, adding 61 minutes produces an earlier date than adding 60.

    In short, PHP does not handle time zone transitions properly. There's an issue ticket that acknowledges it and even an RFC from 2011 that analyses possible fixes.

    (Credit goes to @Alex Blex for this information.)

    It's worth noting that good old Unix-timestamp based functions are affected too:

    <?php
    
    date_default_timezone_set('Europe/Madrid');
    
    $start = strtotime('2017-03-26 01:59:00');
    $increments = array(
        '+60 minutes'   => '2017-03-26 03:59:00',
        '+61 minutes'   => '2017-03-26 04:00:00',
    );
    
    echo 'Start:        ' . date('r', $start) . PHP_EOL;
    foreach ($increments as $increment => $expected_string) {
        echo '>>> ' . $increment . PHP_EOL;
    
        $expected_end = strtotime($expected_string);
        $actual_end = strtotime($increment, $start);
    
        echo 'Expected end: ' . date('r', $expected_end) . PHP_EOL;
        echo 'Actual end:   ' . date('r', $actual_end) . PHP_EOL;
        echo ($expected_end===$actual_end ? 'OK' : 'ERROR') . PHP_EOL;
        echo PHP_EOL;
    }
    

    (run online)

    Start:        Sun, 26 Mar 2017 01:59:00 +0100
    >>> +60 minutes
    Expected end: Sun, 26 Mar 2017 03:59:00 +0200
    Actual end:   Sun, 26 Mar 2017 03:59:00 +0200
    OK
    
    >>> +61 minutes
    Expected end: Sun, 26 Mar 2017 04:00:00 +0200
    Actual end:   Sun, 26 Mar 2017 03:00:00 +0200
    ERROR
    

    Workaround

    Use UTC, of course :)

    You can either use UTC internally for all calculations or switch to UTC before performing date maths. The latter (the most verbose case) implies something like:

    <?php
    
    date_default_timezone_set('Europe/Madrid');
    
    $start = new DateTime('2017-03-26 01:59:00');
    $increments = array(
        '+60 minutes'   => '2017-03-26 03:59:00',
        '+61 minutes'   => '2017-03-26 04:00:00',
    );
    
    echo 'Start:        ' . $start->format('r') . PHP_EOL;
    $local = $start->getTimezone();
    $utc = new DateTimeZone('UTC');
    foreach ($increments as $increment => $expected_string) {
        echo '>>> ' . $increment . PHP_EOL;
    
        $expected_end = new DateTime($expected_string);
        $actual_end = clone $start;
        $actual_end->setTimezone($utc);
        $actual_end->modify($increment);
        $actual_end->setTimezone($local);
    
        echo 'Expected end: ' . $expected_end->format('r') . PHP_EOL;
        echo 'Actual end:   ' . $actual_end->format('r') . PHP_EOL;
        echo ($expected_end->format('c')===$actual_end->format('c')  ? 'OK' : 'ERROR') . PHP_EOL;
        echo PHP_EOL;
    }
    

    (run online)

    Start:        Sun, 26 Mar 2017 01:59:00 +0100
    >>> +60 minutes
    Expected end: Sun, 26 Mar 2017 03:59:00 +0200
    Actual end:   Sun, 26 Mar 2017 03:59:00 +0200
    OK
    
    >>> +61 minutes
    Expected end: Sun, 26 Mar 2017 04:00:00 +0200
    Actual end:   Sun, 26 Mar 2017 04:00:00 +0200
    OK
    

    If you use UTC everywhere you don't need any of this, just a final ->setTimezone() when displaying to end user.

    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论

报告相同问题?

悬赏问题

  • ¥15 求NPF226060磁芯的详细资料
  • ¥15 使用R语言marginaleffects包进行边际效应图绘制
  • ¥20 usb设备兼容性问题
  • ¥15 错误(10048): “调用exui内部功能”库命令的参数“参数4”不能接受空数据。怎么解决啊
  • ¥15 安装svn网络有问题怎么办
  • ¥15 Python爬取指定微博话题下的内容,保存为txt
  • ¥15 vue2登录调用后端接口如何实现
  • ¥65 永磁型步进电机PID算法
  • ¥15 sqlite 附加(attach database)加密数据库时,返回26是什么原因呢?
  • ¥88 找成都本地经验丰富懂小程序开发的技术大咖