php / php-src

The PHP Interpreter
https://www.php.net
Other
38.31k stars 7.76k forks source link

Wrong DateInterval when going from summer time to winter time #16547

Open Geekimo opened 1 month ago

Geekimo commented 1 month ago

Description

The following code:

<?php

$timezone = new \DateTimeZone('Europe/Paris');

$periodStart = (new \DateTimeImmutable('2024-10-21'))->setTimezone($timezone)->setTime(00, 00, 00);
$periodEnd = (new \DateTimeImmutable('2024-10-27'))->setTimezone($timezone)->setTime(23, 59, 59);

var_dump($periodStart);
var_dump($periodEnd);

var_dump($timezone->getOffset($periodStart));
var_dump($timezone->getOffset($periodEnd));

var_dump($periodStart->diff($periodEnd)->format('%y years %m months %a days %H hours %I minutes %s seconds'));

Resulted in this output:

object(DateTimeImmutable)#2 (3) {
  ["date"]=>
  string(26) "2024-10-21 00:00:00.000000"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(12) "Europe/Paris"
}
object(DateTimeImmutable)#3 (3) {
  ["date"]=>
  string(26) "2024-10-27 23:59:59.000000"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(12) "Europe/Paris"
}
int(7200) // UTC+2
int(3600) // UTC+1
string(54) "0 years 0 months 6 days 23 hours 59 minutes 59 seconds"

But I expected this output instead:

object(DateTimeImmutable)#2 (3) {
  ["date"]=>
  string(26) "2024-10-21 00:00:00.000000"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(12) "Europe/Paris"
}
object(DateTimeImmutable)#3 (3) {
  ["date"]=>
  string(26) "2024-10-27 23:59:59.000000"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(12) "Europe/Paris"
}
int(7200)
int(3600)
string(54) "0 years 0 months 7 days 0 hours 59 minutes 59 seconds"

I did the same test using winter to summer time switch and the result is good.

<?php

$timezone = new \DateTimeZone('Europe/Paris');

$periodStart = (new \DateTimeImmutable('2024-03-25'))->setTimezone($timezone)->setTime(00, 00, 00);
$periodEnd = (new \DateTimeImmutable('2024-03-31'))->setTimezone($timezone)->setTime(23, 59, 59);

var_dump($periodStart);
var_dump($periodEnd);

var_dump($timezone->getOffset($periodStart));
var_dump($timezone->getOffset($periodEnd));

var_dump($periodStart->diff($periodEnd)->format('%y years %m months %a days %H hours %I minutes %s seconds'));

Output:

object(DateTimeImmutable)#2 (3) {
  ["date"]=>
  string(26) "2024-03-25 00:00:00.000000"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(12) "Europe/Paris"
}
object(DateTimeImmutable)#3 (3) {
  ["date"]=>
  string(26) "2024-03-31 23:59:59.000000"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(12) "Europe/Paris"
}
int(3600)
int(7200)
string(54) "0 years 0 months 6 days 22 hours 59 minutes 59 seconds"

3v4l.org links:

PHP Version

PHP 8.2.21 and up

Operating System

No response

hormus commented 1 month ago

It's not a bug. You can set manual convert to UTC (DST or ST Europe/Paris +2 or +1)

<?php

$timezone = new \DateTimeZone('UTC');

$periodStart = (new \DateTimeImmutable('2024-10-20 22:00:00', $timezone));
$periodEnd = (new \DateTimeImmutable('2024-10-27 22:59:59', $timezone));

$timezone = new \DateTimeZone('Europe/Paris');
$periodStart = $periodStart->setTimezone($timezone);
$periodEnd = $periodEnd->setTimezone($timezone);

var_dump($periodStart->diff($periodEnd)->format('%y years %m months %a days %H hours %I minutes %s seconds'), $periodEnd->setTimezone(new DateTimeZone('UTC')));
?>

From PHP >= 8.2.6 expected result:

string(54) "0 years 0 months 6 days 23 hours 59 minutes 59 seconds"
object(DateTimeImmutable)#5 (3) {
  ["date"]=>
  string(26) "2024-10-27 22:59:59.000000"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(3) "UTC"
}

Or from PHP < 8.2.6 expected result:

string(54) "0 years 0 months 6 days 24 hours 59 minutes 59 seconds"
object(DateTimeImmutable)#5 (3) {
  ["date"]=>
  string(26) "2024-10-27 22:59:59.000000"
  ["timezone_type"]=>
  int(3)
  ["timezone"]=>
  string(3) "UTC"
}

It's never 7 days.

Geekimo commented 1 month ago

@hormus I don't get your point. With the same timezone. If I set a period, which has between his bounds a transition from winter to summertime, calculation is correct. If I do the same transition, but from summertime to winter time, calculation is incorrect. How could this not be a bug ? Europe/Paris is a type 3 timezone, which according to https://wiki.php.net/rfc/datetime_and_daylight_saving_time has proper calculation rules, which should be applied in the current case.

hormus commented 1 month ago

Hi @Geekimo, the interval from 00:00:00 to 23:59:59 is 23:59:59 if with UTC time zone. If with forward transition "start daylight saving time" 02:00:00 is 03:00:00 local time.

02:00:00 does not exist for "daylight saving time". So applying this formula from 00:00:00 to 23:59:59 "24:59:59 hours DST end" -01:00:00 is 23:59:59.

Geekimo commented 1 month ago

@hormus Ok, so you didn't get my point. Let me reformulate, it may be clearer this way. 😅

I have a recurring task, each week, where I need to have the exact number of years, months, days, hours, minutes and seconds between monday 00:00:00 and sunday 23:59:59, because it matters to my task.

On traditional weeks, like the previous one, which has no DST transition, I have 0 years 0 months 6 days 23 hours 59 minutes 59 seconds, which is perfectly normal, and is the result that I await. When, I have a DST transition, from winter time to summer time, the same exact code will return 0 years 0 months 6 days 22 hours 59 minutes 59 seconds. This is what I expect this code to do and it's perfectly normal, as we remove one hour in this particular week.

Then, when I have the opposite DST transitoin, from summer time to winter time, like this week, the same exact code will return 0 years 0 months 6 days 23 hours 59 minutes 59 seconds, like there was no transition. This is not what I expect this code to do, because we add one hour in this particular week. (At 3 o'clock on sunday morning, it's 2am again).

What should happen is that my code should return 0 years 0 months 7 days 0 hours 59 minutes 59 seconds. If you look closely at the code I provided, I apply the timezone before changing time, and setting timezone as seconde parameter of \DateTimeImmutable::__construct(...) has no impact.