Ousret
Ousret

Reputation: 108

Why do DateTime method diff behave differently in PHP (timezone related)?

I am looking to understand why this simple DateTime operation (2020-07-01)->diff(2020-07-31) cannot be trusted.

<?php

$timeZones = ['GMT', 'Europe/Paris', 'America/Denver', 'Asia/Qatar', 'Australia/Sydney'];

echo('Doing (2020-07-01)->diff(2020-07-31)' . "\n\n");

foreach ($timeZones as $timeZone) {

    date_default_timezone_set($timeZone);

    $startDate = new \DateTime('2020-07-01');

    $calcDateA = (new \DateTime('2020-07-01'))->modify('+1 month -1 day');
    $calcDateB = (new \DateTime('2020-07-01'))->modify('+30 days');
    $calcDateC = new \DateTime('2020-07-31');

    $diffA = $startDate->diff($calcDateA);
    $diffB = $startDate->diff($calcDateB);
    $diffC = $startDate->diff($calcDateC);

    echo("Using $timeZone\n");

    echo('days (A): ' . $diffA->d . "\n");
    echo('months (A): ' . $diffA->m . "\n");

    echo('days (B): ' . $diffB->d . "\n");
    echo('months (B): ' . $diffB->m . "\n");

    echo('days (C): ' . $diffC->d . "\n");
    echo('months (C): ' . $diffC->m . "\n\n");
}

This code, when executed in any PHP version, output the following :

Doing (2020-07-01)->diff(2020-07-31)

Using GMT

days (A): 30
months (A): 0

days (B): 30
months (B): 0

days (C): 30
months (C): 0

Using Europe/Paris

days (A): 0
months (A): 1

days (B): 0
months (B): 1

days (C): 0
months (C): 1

Using America/Denver

days (A): 30
months (A): 0

days (B): 30
months (B): 0

days (C): 30
months (C): 0

Using Asia/Qatar

days (A): 0
months (A): 1

days (B): 0
months (B): 1

days (C): 0
months (C): 1

Using Australia/Sydney

days (A): 0
months (A): 1

days (B): 0
months (B): 1

days (C): 0
months (C): 1

I have the feeling that internal calculation is made with UTC and someone forgot to compensate the timezone. Anyone can confirm?

Thank.

Upvotes: 1

Views: 39

Answers (1)

&#193;lvaro Gonz&#225;lez
&#193;lvaro Gonz&#225;lez

Reputation: 146370

Short answer: yes, it's a bug.

I've simplified your test case to make it more obvious:

$timeZones = ['GMT', 'Europe/Paris'];
foreach ($timeZones as $timeZone) {
    date_default_timezone_set($timeZone);
    $startDate = new \DateTime('2020-07-01');
    $calcDateA = (new \DateTime('2020-07-01'))->modify('+1 month -1 day');
    $diffA = $startDate->diff($calcDateA);
    echo("Using $timeZone\n");
    echo $startDate->format('r'), "\n";
    echo $calcDateA->format('r'), "\n";
    echo $diffA->format('Days: %d / Months: %m'), "\n\n";
}
Using GMT
Wed, 01 Jul 2020 00:00:00 +0000
Fri, 31 Jul 2020 00:00:00 +0000
Days: 30 / Months: 0

Using Europe/Paris
Wed, 01 Jul 2020 00:00:00 +0200
Fri, 31 Jul 2020 00:00:00 +0200
Days: 0 / Months: 1

My guess was that, as you suggest, calculations are done in UTC:

$utc = new DateTimeZone('UTC');
foreach ($timeZones as $timeZone) {
    date_default_timezone_set($timeZone);
    $startDate = new \DateTime('2020-07-01');
    $startDate->setTimezone($utc);
    $calcDateA = (new \DateTime('2020-07-01'))->modify('+1 month -1 day');
    $calcDateA->setTimezone($utc);
    $diffA = $startDate->diff($calcDateA);
    echo("Using $timeZone\n");
    echo $startDate->format('r'), "\n";
    echo $calcDateA->format('r'), "\n";
    echo $diffA->format('Days: %d / Months: %m'), "\n\n";
}

That causes that some context is lost:

Using GMT
Wed, 01 Jul 2020 00:00:00 +0000
Fri, 31 Jul 2020 00:00:00 +0000
Days: 30 / Months: 0

Using Europe/Paris
Tue, 30 Jun 2020 22:00:00 +0000
Thu, 30 Jul 2020 22:00:00 +0000
Days: 0 / Months: 1

There're several tickets in PHP bug tracker. In one of them a contributor explains:

it's not related to DST changeovers (for once). But what happens is that PHP converts it first to UTC time, which ends up comparing 2016-12-31 23:00 to 2017-09-30 22:00 - and then the algorithm gets even more confused and messes up.

This code is complex, and needs to be rewritten sadly. It's going to take some time.

Upvotes: 2

Related Questions