Reputation: 108
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
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