PHP's DateTime::Diff gets it wrong? PHP's DateTime::Diff gets it wrong? php php

PHP's DateTime::Diff gets it wrong?


You're right, PHP currently doesn't handle DST transitions...

Bug reports #51051 (still open) and #55253 (fixed in PHP 5.3.9) describe the problems you're having.

Daniel Convissor has written an RFC trying to address the issue a while back but the change-logs don't suggest this has been addressed. I was hoping this would fixed in 5.4 but I don't see any evidence it has been.

When/if it is implemented, it looks like you'll have to append "DST" or "ST" to the time string.

Best practice is to do all your date calculations in UTC, which avoids this problem.

This DST best practices post is very informative too.


Alright I got a wrapper class working. It calculates real time passed. First it compares the offsets from UTC and add or subtract this time difference to the datetime-object passed as an argument. Thereafter it need not do anything more than to call parent::diff. Well ok I needed to introduce a one-liner to hack what could be yet another bug in PHP (see source code below). The DateTimeDiff:diff method calculates REAL time passed. In order to understand what that means, I advise you to test this class using various different dates and times and to aid your workload I also included at the bottom of this comment a rather simple HTML-page I wrote. This link could be a good starting point to get some ideas for date and time combinations:

https://wiki.php.net/rfc/datetime_and_daylight_saving_time

Moreover, take note that when we have a backward transition in DST, some date/time combinations can belong to both timezones. This ambiguity can make the results of this class differ from what was expected. Thus if you're seriously thinking about using this class, develop it further and ask for user clarification in these cases.

Here you are, the class:

<?phpclass DateTimeDiff extends DateTime{    public function diff($datetime, $absolute = false)    {    // Future releases could fix this bug and if so, this method would become counterproductive.    if (version_compare(PHP_VERSION, '5.4.0') > 0)        trigger_error("You are using a PHP version that might have adressed the problems of DateTime::diff", E_USER_WARNING);    // Have the clock changed?    $offset_start = $this->getOffset();    $offset_end   = $datetime->getOffset();    if ($offset_start != $offset_end)    {        // Remember the difference.        $clock_moved = $offset_end - $offset_start;        // We wouldn't wanna mess things up for our caller; thus work on a clone.        $copy = clone $datetime;        if ($clock_moved > 0)        {            $timestamp_beforesub = $copy->getTimestamp();            // Subtract timedifference from end-datetime should make parent::diff produce accurate results.            $copy->sub( DateInterval::createFromDateString("$clock_moved seconds") );            // No change occured; sometimes sub() fails. This is a workable hack.            if ($timestamp_beforesub == $copy->getTimestamp())                $copy->setTimezone(new DateTimeZone("UTC"));        }        else // ..else < 0 and its a negative.        {            $clock_moved *= -1;            // Adding that timedifference to end-datetime should make parent::diff produce accurate results.            $copy->add( DateInterval::createFromDateString("$clock_moved seconds") );        }        return parent::diff($copy, $absolute);    } // <-- END "if ($offset_start != $offset_end)"    return parent::diff($datetime, $absolute);    }}?>

And a page for testing (will display results using both DateTime::diff and DateTimeDiff::diff):

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><title>DateTimeDiff-class</title><?phpif (! (empty($_GET['identifier']) && empty($_GET['start']) && empty($_GET['end']))){    $dt1_new = new DateTimeDiff("{$_GET['start']} {$_GET['identifier']}");    $dt1_old = new DateTime("{$_GET['start']} {$_GET['identifier']}");    $dt2 = new DateTime("{$_GET['end']} {$_GET['identifier']}");    $di_new = $dt1_new->diff($dt2);    $di_old = $dt1_old->diff($dt2);    // Extract UNIX timestamp and transitional data    $timezone_start = $dt1_new->getTimezone();    $timezone_end = $dt2->getTimezone();    $timestamp_start = $dt1_new->getTimeStamp();    $timestamp_end = $dt2->getTimeStamp();    $transitions_start = $timezone_start->getTransitions($timestamp_start, $timestamp_start);    $transitions_end = $timezone_end->getTransitions($timestamp_end, $timestamp_end);    echo <<<BUILDCONTAINER    <script type='text/javascript'>        function Container() { }        var c_new = new Container;        var c_old = new Container;        var t_start = new Container;        var t_end = new Container;    </script>BUILDCONTAINER;    echo <<<SETTRANSITIONS    <script type='text/javascript'>        t_start.ts = '{$transitions_start[0]['ts']}';        t_start.time = '{$transitions_start[0]['time']}';        t_start.offset = '{$transitions_start[0]['offset']}';        t_end.ts = '{$transitions_end[0]['ts']}';        t_end.time = '{$transitions_end[0]['time']}';        t_end.offset = '{$transitions_end[0]['offset']}';    </script>SETTRANSITIONS;    foreach ($di_new as $property => $value)        echo "<script type='text/javascript'>c_new.$property = $value</script>";    foreach ($di_old as $property => $value)        echo "<script type='text/javascript'>c_old.$property = $value</script>";}?><script type='text/javascript'>window.onload = function(){    if (c_new != null) // <-- em assume everything else is valid too.    {        // Update page with the results        for (var prop in c_new)            addtext(prop + ": " + c_new[prop] + " (" + c_old[prop] + ")");        addtext("Read like so..");        addtext("PROPERTY of DateInterval: VALUE using DateTimeDiff::diff  (  VALUE using DateTime::diff  )");        // Restore values sent/recieved        <?php            foreach ($_GET as $key => $value)                echo "document.getElementById('$key').value = '$value';";        ?>        // Display transitiondata (For DateTime start)        var p_start = document.getElementById('p_start');        var appendstring = "TS: " + t_start.ts + ", Time: " + t_start.time + ", Offset: " + t_start.offset;        p_start.appendChild(document.createTextNode(appendstring));        // Display transitiondata (For DateTime end)        var p_end = document.getElementById('p_end');        appendstring = "TS: " + t_end.ts + ", Time: " + t_end.time + ", Offset: " + t_end.offset;        p_end.appendChild(document.createTextNode(appendstring));    }}function addtext(){    var p = document.createElement("p");    p.appendChild(document.createTextNode(arguments[0]));    document.forms[0].appendChild(p);}</script></head><body><form action="test2.php" method="get">    <p>Identifier: <input type="text" name="identifier" id="identifier" value="Europe/Stockholm" /></p>    <p id="p_start">Start: <input type="text" name="start" id="start" /></p>    <p id="p_end">End: <input type="text" name="end" id="end" /></p>    <p><input type="submit" /></p></form></body></html>