DateTime alters timestamp during DST winter change

2019-05-08 19:06发布

问题:

Today encountered interesting feature (?) in PHP DateTime::setTimestamp() behavior. During winter DST changes, when 1 hour repeats twice, PHP converts timestamp to always be the second hour. Consider following example:

<?php

$date = new \DateTime("now", new \DateTimeZone("UTC"));
//2018-10-28T01:30:00 UTC, in London DST happens
$date->setTimestamp(1540686600);

echo $date->getTimestamp() . "\n";
//1540686600
echo $date->format('c') . "\n";
//2018-10-28T00:30:00+00:00

$date->setTimezone(new \DateTimeZone("Europe/London"));

echo $date->getTimestamp() . "\n";
//1540690200

$date->setTimezone(new \DateTimeZone("UTC"));

echo $date->getTimestamp() . "\n";
//1540690200

echo $date->format('c') . "\n";
//2018-10-28T01:30:00+00:00

Looking at source PHP tries to convert timestamp to local time (o_O). So two questions:

  • why there's conversion of timestamp? Timestamp is always in UTC and should be local time/timezone agnostic
  • is there any simple solution to keep both timezone id and intended timestamp in DateTime object? (I am aware of possibility to keep everything in UTC and convert to local timezone only when showing)

Found possible bug filed in PHP

UPD 2016-01-04 (in UTC):

Narrowed down problem to DateTime::getTimestamp(). Consider following code:

<?php

$date = new \DateTime("now", new \DateTimeZone("UTC"));
//2018-10-28T00:30:00 UTC
$date->setTimestamp(1540686600);
echo $date->format('c') . "\n"; //2018-10-28T00:30:00+00:00

$date->setTimezone(new \DateTimeZone("Europe/London"));
echo $date->format('c') . "\n"; //2018-10-28T01:30:00+01:00

$date->setTimezone(new \DateTimeZone("UTC"));
echo $date->format('c') . "\n"; //2018-10-28T00:30:00+00:00
echo $date->getTimestamp() . "\n"; //1540686600

Here timestamp is not altered and code works as expected. Following code that is similar and was given in original example is broken:

<?php

$date = new \DateTime("now", new \DateTimeZone("UTC"));
//2018-10-28T00:30:00 UTC
$date->setTimestamp(1540686600);
echo $date->format('c') . "\n"; //2018-10-28T00:30:00+00:00

$date->setTimezone(new \DateTimeZone("Europe/London"));
echo $date->format('c') . "\n"; //2018-10-28T01:30:00+01:00
//-------------- the only line that was added ------------------
echo $date->getTimestamp() . "\n"; //1540690200
//-------------- end of added line -----------------------------

$date->setTimezone(new \DateTimeZone("UTC"));
echo $date->format('c') . "\n"; //2018-10-28T01:30:00+00:00
echo $date->getTimestamp() . "\n"; //1540690200

回答1:

You said:

Timestamp is always in UTC and should be local time, timezone agnostic

That statement is in conflict with itself. Timestamps are in UTC, yes, but that is not local time or time zone agnostic.

Put another way, a timestamp always refers to a specific instant in time. A local time might be ambiguous, but a timestamp is not. If you wanted the first instance of the ambiguous local time, then specify a timestamp that is an hour earlier.

However, your data is slightly incorrect. The timestamp you specified, 1540686600, is actually not 1:30 UTC but rather it corresponds to 2018-10-28T01:30:00+01:00. Therefore, it is indeed the first occurrence of 1:30 on the day of the fall-back DST transition. The second occurrence would be 2018-10-28T01:30:00+00:00, which corresponds to 1540690200.

Regarding your second question:

is there any simple solution to keep both timezone id and intended timestamp in DateTime object?

That's already how DateTime works in PHP. It consists of an internal timestamp, and an associated time zone. That's exactly what you're showing when you call setTimezone.



标签: php dst