В прошлое воскресенье Европейский Союз переключился с CET (+0100) на CEST (+0200). Я пишу код для применения приращения к дате, и он не работает должным образом, потому что переход на часовой пояс учитывается только в некоторых относительных форматах:
'+x minutes'
пропускает пропущенный час'+x hours'
неВот мой тестовый код:
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;
}
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
поскольку относительные форматы часто настолько нелогичны, что я не уверен, что получаю какое-то задокументированное поведение или это ошибка.
Можете ли вы пролить свет на это?
Это не может быть относительное недопонимание формата, потому что поведение ошибочно в одном и том же формате:
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;
}
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 не обрабатывает переходы часового пояса должным образом. Там есть оформить билет что признает это и даже RFC с 2011 года, который анализирует возможные исправления.
(Кредит идет в @ Алекс Блекс для этой информации.)
Стоит отметить, что старые добрые функции, основанные на метках времени 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;
}
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;
}
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()
при отображении конечному пользователю.
Других решений пока нет …