Search code examples
phpdatetimetimetimezonestrtotime

How to fix this very frustrating time bug in my time code?


I have these two functions:

function time_is_older_than($timestamp, $time_string)
{
    if (strtotime($timestamp) < strtotime('-' . $time_string))
        return true;

    return false;
}

function time_is_younger_than($timestamp, $time_string)
{
    if (strtotime($timestamp) > strtotime('-' . $time_string))
        return true;

    return false;
}

They enable me to do neat things like:

if (time_is_older_than($last_time_some_action_happened, '5 minutes'))
    do_it_again();

They normally work, except for during one hour every six months, when my timezone switches over to "summer time" or "winter time". This means that the clocks are increased or put back one hour at midnight (according to this timezone).

The PHP manual states this for strtotime:

The Unix timestamp that this function returns does not contain information about time zones. In order to do calculations with date/time information, you should use the more capable DateTimeImmutable.

However, if I provide the exact same date/time string, with "+08:00" added in the end versus "+00:00", for example, I get different numbers of seconds returned. So strtotime() does understand timezones when it parses the provided time, even if the returned integer obviously doesn't contain this information. (Nor is it expected or required to by me.)

I've spent countless hours trying to debug this, testing countless things, and just sitting here thinking, but I can't figure out what exactly would make the code I have fail, specifically for one hour. And especially what about it I need to change. Setting the second parameter for strtotime() seems likely, but I just couldn't make it work correctly.

My hottest "lead" for quite some time was that the strtotime('-' . $time_string) part is ending up using a different timezone than the timestamp strings provided, but I do provide timezone data to it most of the time! An example of $last_time_some_action_happened might be something like 2020-10-28 02:22:41.123456+01.

I set the timezone with date_default_timezone_set().

I suspect that I only need to make some very minor change, but I've been experimenting so much and so long now, even taking rests in between, that my brain can no longer see this clearly. I bet the solution is something awfully simple.

Please don't tell me to use DateTimeImmutable. This would fundamentally change my entire structure and require me to do things very differently. Perhaps I should, and even will, at some point, but for now, I just wish to fix this rare but still very annoying bug in my existing code. (If it's possible at all, which I very much believe is the case.)


Solution

  • I'm able to reproduce the issue you are having:

    date_default_timezone_set('Pacific/Auckland');
    
    // Daylight saving time 2020 in New Zealand began at 2:00am on Sunday, 27 September
    $current = strtotime('2020-09-27 02:04:00');
    
    $d1 = strtotime('2020-09-27 02:05:00', $current);
    $d2 = strtotime('-5 minutes', $current);
    
    var_dump($d1 > $d2); // false
    var_dump(date('Y-m-d H:i:s', $d1)); // 2020-09-27 03:05:00
    var_dump(date('Y-m-d H:i:s', $d2)); // 2020-09-27 03:59:00
    

    This person looks to be having the same issue as you and may appear to be a bug. DateTime::modify and DST switch The solution is to convert the dates to UTC then compare:

    // Convert to UTC and compare
    $d1 = new \DateTime('2020-09-27 02:05:00', new \DateTimeZone('Pacific/Auckland'));
    
    $d2 = new \DateTime('2020-09-27 02:04:00', new \DateTimeZone('Pacific/Auckland'));
    $d2->setTimezone(new \DateTimeZone('UTC'));
    $d2->modify('-5 minutes');
    $d2->setTimezone(new \DateTimeZone('Pacific/Auckland'));
    
    var_dump($d1 > $d2); // true
    var_dump($d1->format(\DateTimeInterface::RFC3339_EXTENDED)); // 2020-09-27T03:05:00.000+13:00
    var_dump($d2->format(\DateTimeInterface::RFC3339_EXTENDED)); // 2020-09-27T01:59:00.000+12:00
    

    I've updated your functions:

    function time_is_older_than($datetime, $time_string)
    {
        $d1 = new \DateTime($datetime);
        $d1->setTimezone(new \DateTimeZone('UTC'));
        
        $d2 = new \DateTime();
        $d2->setTimezone(new \DateTimeZone('UTC'));
        $d2->modify('-' . $time_string);
        
        return $d1 < $d2;
    }
    
    function time_is_younger_than($datetime, $time_string)
    {
        $d1 = new \DateTime($datetime);
        $d1->setTimezone(new \DateTimeZone('UTC'));
        
        $d2 = new \DateTime();
        $d2->setTimezone(new \DateTimeZone('UTC'));
        $d2->modify('-' . $time_string);
        
        return $d1 > $d2;
    }