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?

doudi5291
doudi5291 是的......此时我很确定这是一个PHP错误。添加60分钟渲染03:59:00+0200,添加61渲染03:00:00+0200。
3 年多之前 回复
doudang8824
doudang8824 有很多DST过渡错误报告bugs.php.net/...我相信你可以找到相关的东西。
3 年多之前 回复

1个回答

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.

Csdn user default icon
上传中...
上传图片
插入图片
抄袭、复制答案,以达到刷声望分或其他目的的行为,在CSDN问答是严格禁止的,一经发现立刻封号。是时候展现真正的技术了!
立即提问