在某些相对时间不考虑 DST

DST not accounted for in certain relative times

上周日,欧盟从 CET (+0100) 切换到 CEST (+0200)。我正在编写代码以将增量应用到日期,但它无法正常工作,因为时区转换仅在某些相关格式中得到适当考虑:

这是我的测试代码:

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

由于 relative formats 通常如此违反直觉,我不确定我是否得到了一些记录在案的行为或者这是一个错误。

你能解释一下吗?

这不可能是相对格式的误解,因为在相同格式下行为是不稳定的:

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

换句话说,相加 61 分钟比相加 60 得到更早的日期。

简而言之,PHP 没有正确处理时区转换。 2011 年的 issue ticket that acknowledges it and even an RFC 分析了可能的修复方法。

(此信息归功于 @Alex Blex。)

值得注意的是,基于 Unix 时间戳的旧函数也受到影响:

<?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

解决方法

当然是使用 UTC :)

您可以在内部使用 UTC 进行所有计算,也可以在执行日期数学运算之前切换到 UTC。后者(最冗长的情况)意味着类似:

<?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

如果您在任何地方都使用 UTC,则不需要任何这些,只需要在向最终用户显示时最后 ->setTimezone()