Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions ext/date/php_date.c
Original file line number Diff line number Diff line change
Expand Up @@ -3288,6 +3288,7 @@ static bool php_date_modify(zval *object, char *modify, size_t modify_len) /* {{
php_date_obj *dateobj;
timelib_time *tmp_time;
timelib_error_container *err = NULL;
timelib_sll rel_h, rel_i, rel_s, rel_us;

dateobj = Z_PHPDATE_P(object);

Expand Down Expand Up @@ -3356,8 +3357,36 @@ static bool php_date_modify(zval *object, char *modify, size_t modify_len) /* {{

timelib_time_dtor(tmp_time);

/* do_adjust_relative() applies h/i/s as wall-clock, which breaks across
* DST. Strip them before timelib_update_ts and re-apply via SSE below. */
rel_h = dateobj->time->relative.h;
rel_i = dateobj->time->relative.i;
rel_s = dateobj->time->relative.s;
rel_us = dateobj->time->relative.us;
dateobj->time->relative.h = 0;
dateobj->time->relative.i = 0;
dateobj->time->relative.s = 0;
dateobj->time->relative.us = 0;

timelib_update_ts(dateobj->time, NULL);
timelib_update_from_sse(dateobj->time);

/* Normalize microseconds: fold full seconds into rel_s, keep rel_us >= 0 */
rel_s += rel_us / 1000000;
rel_us = rel_us % 1000000;
if (rel_us < 0) {
rel_s--;
rel_us += 1000000;
}

dateobj->time->sse += timelib_hms_to_seconds(rel_h, rel_i, rel_s);
dateobj->time->us += rel_us;
if (dateobj->time->us >= 1000000) {
dateobj->time->us -= 1000000;
dateobj->time->sse++;
}
timelib_update_from_sse(dateobj->time);

dateobj->time->have_relative = 0;
memset(&dateobj->time->relative, 0, sizeof(dateobj->time->relative));

Expand Down
2 changes: 1 addition & 1 deletion ext/date/tests/date_modify-1.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ Sun, 22 Aug 1993 00:00:00 +12
Sun, 27 Mar 2005 01:59:59 CET
Sun, 27 Mar 2005 03:00:00 CEST
Sun, 30 Oct 2005 01:59:59 CEST
Sun, 30 Oct 2005 03:00:00 CET
Sun, 30 Oct 2005 02:00:00 CET
28 changes: 28 additions & 0 deletions ext/date/tests/gh15880.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
--TEST--
Bug GH-15880 (DateTime::modify('+72 hours') incorrect across DST boundary)
--FILE--
<?php
/* Fall back: America/Chicago, 2024-11-03 02:00 CDT -> 01:00 CST.
* +72 hours from midnight Nov 1 must land at Nov 3 23:00 CST, not Nov 4 00:00. */
date_default_timezone_set('America/Chicago');
$tz = new DateTimeZone('America/Chicago');
$start = new DateTimeImmutable('2024-11-01 00:00:00', $tz);

/* modify and add must agree */
echo $start->modify('+72 hours')->format('Y-m-d H:i:s T U'), "\n";
echo $start->add(new DateInterval('PT72H'))->format('Y-m-d H:i:s T U'), "\n";

/* +3 days is calendar arithmetic -- it should land on midnight Nov 4 */
echo $start->modify('+3 days')->format('Y-m-d H:i:s T U'), "\n";

/* -72 hours backward through fall-back: 73 real hours separate Nov 1 00:00 CDT
* from Nov 4 00:00 CST (the extra hour is the repeated hour), so -72h lands 1h
* ahead of Nov 1 midnight */
$end = new DateTimeImmutable('2024-11-04 00:00:00', $tz);
echo $end->modify('-72 hours')->format('Y-m-d H:i:s T U'), "\n";
?>
--EXPECT--
2024-11-03 23:00:00 CST 1730696400
2024-11-03 23:00:00 CST 1730696400
2024-11-04 00:00:00 CST 1730700000
2024-11-01 01:00:00 CDT 1730440800
42 changes: 42 additions & 0 deletions ext/date/tests/gh21616.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
--TEST--
Bug GH-21616 (DateTime::modify() does not respect DST transitions)
--FILE--
<?php
/* Spring forward: Europe/London, 2025-03-30 01:00 GMT -> 02:00 BST */
$tz = new DateTimeZone('Europe/London');

/* +1s then -1s must round-trip */
$dt = new DateTime('2025-03-30 00:59:59', $tz);
$dt->modify('+1 second');
$dt->modify('-1 second');
echo $dt->format('Y-m-d H:i:s T U'), "\n";

/* -1s from 02:00 BST must land at 00:59:59 GMT, not 02:59:59 BST */
$dt2 = new DateTime('2025-03-30 02:00:00', $tz);
echo $dt2->modify('-1 second')->format('Y-m-d H:i:s T U'), "\n";

/* month + hours: +1 month lands before the DST boundary, so +1 hour is plain GMT */
$dt3 = new DateTime('2025-02-28 00:30:00', $tz);
echo $dt3->modify('+1 month +1 hour')->format('Y-m-d H:i:s T U'), "\n";

/* first/last day of must still work */
$base = new DateTimeImmutable('2025-03-15 10:00:00', $tz);
echo $base->modify('first day of next month')->format('Y-m-d H:i:s T'), "\n";
echo $base->modify('last day of this month')->format('Y-m-d H:i:s T'), "\n";

/* +61 minutes from just before the gap -- minutes must also count as elapsed time */
$dt4 = new DateTime('2025-03-30 00:59:00', $tz);
echo $dt4->modify('+61 minutes')->format('Y-m-d H:i:s T U'), "\n";

/* DateTimeImmutable must behave the same as mutable DateTime */
$dt5 = new DateTimeImmutable('2025-03-30 02:00:00', $tz);
echo $dt5->modify('-1 second')->format('Y-m-d H:i:s T U'), "\n";
?>
--EXPECT--
2025-03-30 00:59:59 GMT 1743296399
2025-03-30 00:59:59 GMT 1743296399
2025-03-28 01:30:00 GMT 1743125400
2025-04-01 10:00:00 BST
2025-03-31 10:00:00 BST
2025-03-30 03:00:00 BST 1743300000
2025-03-30 00:59:59 GMT 1743296399
Loading