-
-
Notifications
You must be signed in to change notification settings - Fork 35
Description
Hi,
This bug post is written off the back of: https://phpfashion.com/en/100-minutes-is-less-than-50-php-paradoxes-during-time-changes
Description
When adding relative time (e.g., '+100 minutes') to a ZonedDateTime object during a period that crosses a Daylight Saving Time (DST) transition, PHP and brick/date-time incorrectly(?) calculates the resulting time.
Specifically, when the base time is before a "spring forward" DST transition, adding a larger amount of time sometimes results in an earlier time than adding a smaller amount.
I checked the behaviour against PostgreSQL and Java, both of which handle the same scenario correctly(?).
I am therefore asserting going forward that php and brick/date-time are incorrect, and java and PostgreSQL are correct.
PHP Code with Incorrect Behavior
<?php
date_default_timezone_set('Europe/Prague');
$now = new DateTimeImmutable('2025-03-30 01:30:00');
echo 'Original: '. $now->format('Y-m-d H:i:s T (P)') . PHP_EOL;
$dt50 = $now->modify('+50 minutes');
echo 'Plus 50 minutes: '. $dt50->format('Y-m-d H:i:s T (P)').PHP_EOL;
$dt100 = $now->modify('+100 minutes');
echo 'Plus 100 minutes: '. $dt100->format('Y-m-d H:i:s T (P)').PHP_EOL;
// Original: 2025-03-30 01:30:00 CET (+01:00)
// Plus 50 minutes: 2025-03-30 03:20:00 CEST (+02:00)
// Plus 100 minutes: 2025-03-30 03:10:00 CEST (+02:00)brick/date-time Code with Incorrect Behavior
<?php
declare(strict_types=1);
use Brick\DateTime\ZonedDateTime;
require_once __DIR__.'/vendor/autoload.php';
date_default_timezone_set('Europe/Prague');
$now = ZonedDateTime::fromNativeDateTime(new DateTimeImmutable('2025-03-30 01:30:00'));
echo 'Original: '. $now->toNativeDateTimeImmutable()->format('Y-m-d H:i:s T (P)') . PHP_EOL;
$dt50 = $now->plusMinutes(50);
echo 'Plus 50 minutes: '. $dt50->toNativeDateTimeImmutable()->format('Y-m-d H:i:s T (P)').PHP_EOL;
$dt100 = $now->plusMinutes(100);
echo 'Plus 100 minutes: '. $dt100->toNativeDateTimeImmutable()->format('Y-m-d H:i:s T (P)').PHP_EOL;
// Original: 2025-03-30 01:30:00 CET (+01:00)
// Plus 50 minutes: 2025-03-30 03:20:00 CEST (+02:00)
// Plus 100 minutes: 2025-03-30 03:10:00 CEST (+02:00)Correct behavior in PostgreSQL
SET timezone = 'Europe/Prague';
SELECT
'2025-03-30 01:30:00'::timestamptz AS original_time,
'2025-03-30 01:30:00'::timestamptz + INTERVAL '50 minutes' AS time_plus_50_min,
'2025-03-30 01:30:00'::timestamptz + INTERVAL '100 minutes' AS time_plus_100_min;
-- Output:
-- | original_time | time_plus_50_min | time_plus_100_min |
-- |------------------------|------------------------|------------------------|
-- | 2025-03-30 01:30:00+01 | 2025-03-30 03:20:00+02 | 2025-03-30 04:10:00+02 |Correct behavior in Java
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
public class DSTExample {
public static void main(String[] args) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z (O)");
ZoneId zoneId = ZoneId.of("Europe/Prague");
ZonedDateTime baseTime = ZonedDateTime.of(2025, 3, 30, 1, 30, 0, 0, zoneId);
System.out.println("Original time: " + baseTime.format(formatter));
ZonedDateTime plusFiftyMinutes = baseTime.plus(50, ChronoUnit.MINUTES);
System.out.println("Plus 50 minutes: " + plusFiftyMinutes.format(formatter));
ZonedDateTime plusHundredMinutes = baseTime.plus(100, ChronoUnit.MINUTES);
System.out.println("Plus 100 minutes: " + plusHundredMinutes.format(formatter));
}
}
// Output:
// Original time: 2025-03-30 01:30:00 CET (GMT+1)
// Plus 50 minutes: 2025-03-30 03:20:00 CEST (GMT+2)
// Plus 100 minutes: 2025-03-30 04:10:00 CEST (GMT+2)Expected vs Actual Results
Expected (as shown by PostgreSQL and Java):
- Original time: 2025-03-30 01:30:00 CET
- +50 minutes: 2025-03-30 03:20:00 CEST
- +100 minutes: 2025-03-30 04:10:00 CEST
Actual in PHP and Brick
- Original time: 2025-03-30 01:30:00 CET
- +50 minutes: 2025-03-30 03:20:00 CEST
- +100 minutes: 2025-03-30 03:10:00 CEST (incorrect - should be 04:10:00)
Analysis
The Europe/Prague DST transition is at 2am.
This appears to be an error in PHP's DateTime handling.
When adding 100 minutes (1 hour 40 minutes) to 01:30, the correct result should be:
- 01:30 + 1h40m = 04:10
- because at 02:00 a.m. clocks turned to 3:00 a.m.
However, PHP incorrectly computes this as 03:10, suggesting it's not properly accounting for the DST transition.
PHP does correctly handle the +50 minute transition, because it lands at 02:20:00, adding a further 60 minutes for the DST transition, resulting in 03:20:00.