Manipulating Date/Time timestamps in PHP around DST zone changes reveals inconsistencies.
I've been trying to understand this for a few hours and figured someone else might have a better idea of what's going wrong.
Basically I was subtracting a few seconds from the timestamp and incrementing over the DST change period and I found it to be hit and miss. One second back would sometimes work. 3 seconds would reveal other problems. It seems most of the time American timezone changes are reported correctly. But Others are never correct.
I have left most of my working in the code below and it is made to comment out some parts and uncomment others.
The output format is verbose to reveal as much about the DateTime object as possible.
The code is commented but essentially we inspect several timezones and in each timezone we check the transition data. This is all done at the top of the first foreach.
$time ~~ the time around which we wan to check timezone transitions (initially set to UTC, yes I know timestamps are only UTC, I'm talking about the DTZ component). $time is only set once at the top of the file.
$timezone ~~ DateTimeZone object loaded with the relevant timezone and was initially only set once at the top of the foreach, it appears in various other places to expand on the issues of the errors I was finding.
$transitions ~~ you guessed it, transitions of the timezone object around the timestamp (time).
$slice is an array variable used to break down the transitions into named components to make it easier to read.
$tz... Timezone transition data:
$tzCurrent the current abbreviated timezone name (like BST, AEDT, PDR, etc)
$tzCurrentDST is it in DST or not right now. 1 for yes.
$tzNew the new timezone abbreviation after transition
$tzNewTimestamp when the transition happens
$tzNewDST is the new transition DST or not
$tzNewTimeString a string output of time in UTC of the transition
$changover this is the version of the DateTime object I am using to run the tests on the transition data.
The first thing I do is set the $changover object. Since it's not possible to set a unix timestamp with a timezone when declaring it, I simply declare it with a timestamp of zero at UTC. I then explicitly set the timestamp and timezone. REVERSING the order of the timezone and timestamp lines will change the output (neither of which are totally correct anyway):
$changeover = new DateTime("@0"); // *** force DTZ to UTC (just to be safe)
$changeover->setTimestamp($tzNewTimestamp) - $secondsBack);
$changeover->setTimezone($timezone);
next I output some data for info and then we get into the loop around the transition time. This is where the output really starts to get interesting.
The most odd of findings seems to be the position of the setTimezone method. For example, enabling the last one in the inner foreach can yield different results if you use the third method of timestamp increments. It has no effect on the first two methods.
The first time I tried iterating over the timestamp I used the following code:
$changeover->setTimestamp($changeover->getTimestamp()+1);
Then I changed that to:
$changeover->modify("+1 second");
which gave basically the same result:
< < date and time > > ZONE UTC+/- listIdentifier DST OFFSET TimeZone object info
2018-04-01 02:59:57 AM AEDT +1100 Australia/Sydney 1 39600Australia/Sydney
2018-04-01 02:59:58 AM AEST +1000 Australia/Sydney 0 36000Australia/Sydney
2018-04-01 02:59:59 AM AEST +1000 Australia/Sydney 0 36000Australia/Sydney
2018-04-01 03:00:00 AM AEST +1000 Australia/Sydney 0 36000Australia/Sydney
2018-04-01 03:00:01 AM AEST +1000 Australia/Sydney 0 36000Australia/Sydney
2018-04-01 03:00:02 AM AEST +1000 Australia/Sydney 0 36000Australia/Sydney
(only the first line is correct, the first three should be one timezone, then the remaining three should be the new timezone).
I then setup a DateInterval and used that:
$changeover->add(new DateInterval('PT1S'));
The above DateInterval gives noticable different results to the first two attempts. If you run and view the output it can be seen that TWO lines of correct value may appear (there should be 3).
2018-04-01 02:59:57 AM AEDT +1100 Australia/Sydney 1 39600Australia/Sydney
2018-04-01 02:59:58 AM AEDT +1100 Australia/Sydney 1 39600Australia/Sydney
2018-04-01 02:59:59 AM AEST +1000 Australia/Sydney 0 36000Australia/Sydney
2018-04-01 03:00:00 AM AEST +1000 Australia/Sydney 0 36000Australia/Sydney
2018-04-01 03:00:01 AM AEST +1000 Australia/Sydney 0 36000Australia/Sydney
2018-04-01 03:00:02 AM AEST +1000 Australia/Sydney 0 36000Australia/Sydney
IF I reset the timezone on ever loop, then it gives a kind of* better result:
2018-04-01 02:59:57 AM AEDT +1100 Australia/Sydney 1 39600Australia/Sydney
2018-04-01 02:59:58 AM AEDT +1100 Australia/Sydney 1 39600Australia/Sydney
2018-04-01 02:59:59 AM AEDT +1100 Australia/Sydney 1 39600Australia/Sydney
2018-04-01 02:00:00 AM AEST +1000 Australia/Sydney 0 36000Australia/Sydney
2018-04-01 02:00:01 AM AEST +1000 Australia/Sydney 0 36000Australia/Sydney
2018-04-01 02:00:02 AM AEST +1000 Australia/Sydney 0 36000Australia/Sydney
There are three correct entries, but this does make other entries incorrect, for example Los Angeles:
2017-11-05 01:59:57 AM PDT -0700 America/Los_angeles 1 -25200America/Los_angeles
2017-11-05 01:59:58 AM PDT -0700 America/Los_angeles 1 -25200America/Los_angeles
2017-11-05 01:59:59 AM PDT -0700 America/Los_angeles 1 -25200America/Los_angeles
2017-11-05 01:00:00 AM PST -0800 America/Los_angeles 0 -28800America/Los_angeles
2017-11-05 01:00:01 AM PDT -0700 America/Los_angeles 1 -25200America/Los_angeles
2017-11-05 01:00:02 AM PDT -0700 America/Los_angeles 1 -25200America/Los_angeles
If you uncomment the listIdentifiers at the top of the file you can print out all timezones that have DST data. You will most often find that American DST transitions are correct, but other countries are hit and miss. It's even hit and miss in Australia.
I have tried this on Online PHP test sites in New York, US/Pacific, and servers located in Sydney and Melbourne, as well as locally on my local dev server and a digital ocean server in Singapore.
I have mostly used PHP7 (although one of the server allowed testing of a range of PHP7 minor revisions all the way to 7.1.0), and 5.6 and 5.5
The full code is below if anyone would care to pick it to pieces and there is a saved online version here:
http://sandbox.onlinephpfunctions.com/code/bc085c977f419f9db9f137ca81ad3a733e849e49
The question is basically why can't I get reliable DST/TimeZone transition data from PHP? What am I doing wrong?
Thanks for reading
<?php
echo '<pre>';
$f = "Y-m-d H:i:s A T O e I ";
$secondsBack = 3; // how many seconds to go back
$secondsWatch = 6;
$tzTests=Array('Australia/North','Australia/Sydney','Australia/Lord_Howe','America/Los_angeles','America/Chicago','Europe/Berlin','Asia/Jerusalem','Pacific/Auckland');
//$tzTests=DateTimeZone::listIdentifiers();
//$time = new DateTime('2017-09-20 00:00:01', new DateTimeZone('UTC'));
$time = new DateTime('now', new DateTimeZone('UTC'));
foreach ($tzTests as $tzTest){
//// This is the TimeZone we are going to test.
$timezone = new DateTimeZone($tzTest);
date_default_timezone_set($tzTest); // just in case
//// using the time we want to look around ($time), load the transitions for that $timezone
$transitions = $timezone->getTransitions($time->getTimestamp());
$slice = array_slice($transitions, 0, 3);
//print_r($slice);
// if there is no DST info, we're not going to show it
if (count($slice)>1){
//// break out individual parts of the transition information
$tzCurrent = $slice[0]['abbr'] ;
$tzCurrentDST = $slice[0]['isdst'];
$tzNew = $slice[1]['abbr'] ;
$tzNewTimestamp = $slice[1]['ts'] ;
$tzNewDST = $slice[1]['isdst'];
$tzNewTimeString = $slice[1]['time'] ;
echo $tzTest . PHP_EOL;
echo "currently: " . $tzCurrent. (($tzCurrentDST==1)?" (DST)":" (NORMAL)") ;
echo PHP_EOL;
$changeover = new DateTime("@0"); // *** force DTZ to UTC (just to be safe)
$changeover->setTimestamp($tzNewTimestamp);// - $secondsBack);
$changeover->modify("-$secondsBack seconds");
///* The following line can change the result if moved up two lines *///
$changeover->setTimezone($timezone);
echo "Next Change: " . $tzNew . (($tzNewDST == 1)?" (DST)":" (NORMAL)") . PHP_EOL;
echo " @ " . $tzNewTimeString . PHP_EOL;
for( $i = 0 ; $i < $secondsWatch; $i++){
// for each loop, print the current time format
echo $changeover->format($f) . $changeover->getOffset() . $changeover->getTimezone()->getName() . PHP_EOL;
////*** First increment style ***////
//$changeover->setTimestamp($changeover->getTimestamp()+1);
////*** Second increment style ***////
$changeover->modify("+1 second");
////*** Third and only one that works! (as long as you update the timezone!!!!) ***////
//$changeover->add(new DateInterval('PT1S'));
////*** This has varying effects, works best with method 3 ***////
//$changeover->setTimezone($timezone);
}
echo PHP_EOL . PHP_EOL;
} else {
//echo "No DST Information" . PHP_EOL . PHP_EOL;
}
} ?>