Skip to content

Commit 081cbe6

Browse files
Merge pull request #3 from MarcusXavierr/fix/gh-21616-84
Fix/gh 21616 84
2 parents 1f4b169 + 50d8d86 commit 081cbe6

4 files changed

Lines changed: 100 additions & 1 deletion

File tree

ext/date/php_date.c

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3288,6 +3288,7 @@ static bool php_date_modify(zval *object, char *modify, size_t modify_len) /* {{
32883288
php_date_obj *dateobj;
32893289
timelib_time *tmp_time;
32903290
timelib_error_container *err = NULL;
3291+
timelib_sll rel_h, rel_i, rel_s, rel_us;
32913292

32923293
dateobj = Z_PHPDATE_P(object);
32933294

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

33573358
timelib_time_dtor(tmp_time);
33583359

3360+
/* do_adjust_relative() applies h/i/s as wall-clock, which breaks across
3361+
* DST. Strip them before timelib_update_ts and re-apply via SSE below. */
3362+
rel_h = dateobj->time->relative.h;
3363+
rel_i = dateobj->time->relative.i;
3364+
rel_s = dateobj->time->relative.s;
3365+
rel_us = dateobj->time->relative.us;
3366+
dateobj->time->relative.h = 0;
3367+
dateobj->time->relative.i = 0;
3368+
dateobj->time->relative.s = 0;
3369+
dateobj->time->relative.us = 0;
3370+
33593371
timelib_update_ts(dateobj->time, NULL);
33603372
timelib_update_from_sse(dateobj->time);
3373+
3374+
/* Normalize microseconds: fold full seconds into rel_s, keep rel_us >= 0 */
3375+
rel_s += rel_us / 1000000;
3376+
rel_us = rel_us % 1000000;
3377+
if (rel_us < 0) {
3378+
rel_s--;
3379+
rel_us += 1000000;
3380+
}
3381+
3382+
dateobj->time->sse += timelib_hms_to_seconds(rel_h, rel_i, rel_s);
3383+
dateobj->time->us += rel_us;
3384+
if (dateobj->time->us >= 1000000) {
3385+
dateobj->time->us -= 1000000;
3386+
dateobj->time->sse++;
3387+
}
3388+
timelib_update_from_sse(dateobj->time);
3389+
33613390
dateobj->time->have_relative = 0;
33623391
memset(&dateobj->time->relative, 0, sizeof(dateobj->time->relative));
33633392

ext/date/tests/date_modify-1.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@ Sun, 22 Aug 1993 00:00:00 +12
2525
Sun, 27 Mar 2005 01:59:59 CET
2626
Sun, 27 Mar 2005 03:00:00 CEST
2727
Sun, 30 Oct 2005 01:59:59 CEST
28-
Sun, 30 Oct 2005 03:00:00 CET
28+
Sun, 30 Oct 2005 02:00:00 CET

ext/date/tests/gh15880.phpt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
--TEST--
2+
Bug GH-15880 (DateTime::modify('+72 hours') incorrect across DST boundary)
3+
--FILE--
4+
<?php
5+
/* Fall back: America/Chicago, 2024-11-03 02:00 CDT -> 01:00 CST.
6+
* +72 hours from midnight Nov 1 must land at Nov 3 23:00 CST, not Nov 4 00:00. */
7+
date_default_timezone_set('America/Chicago');
8+
$tz = new DateTimeZone('America/Chicago');
9+
$start = new DateTimeImmutable('2024-11-01 00:00:00', $tz);
10+
11+
/* modify and add must agree */
12+
echo $start->modify('+72 hours')->format('Y-m-d H:i:s T U'), "\n";
13+
echo $start->add(new DateInterval('PT72H'))->format('Y-m-d H:i:s T U'), "\n";
14+
15+
/* +3 days is calendar arithmetic -- it should land on midnight Nov 4 */
16+
echo $start->modify('+3 days')->format('Y-m-d H:i:s T U'), "\n";
17+
18+
/* -72 hours backward through fall-back: 73 real hours separate Nov 1 00:00 CDT
19+
* from Nov 4 00:00 CST (the extra hour is the repeated hour), so -72h lands 1h
20+
* ahead of Nov 1 midnight */
21+
$end = new DateTimeImmutable('2024-11-04 00:00:00', $tz);
22+
echo $end->modify('-72 hours')->format('Y-m-d H:i:s T U'), "\n";
23+
?>
24+
--EXPECT--
25+
2024-11-03 23:00:00 CST 1730696400
26+
2024-11-03 23:00:00 CST 1730696400
27+
2024-11-04 00:00:00 CST 1730700000
28+
2024-11-01 01:00:00 CDT 1730440800

ext/date/tests/gh21616.phpt

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
--TEST--
2+
Bug GH-21616 (DateTime::modify() does not respect DST transitions)
3+
--FILE--
4+
<?php
5+
/* Spring forward: Europe/London, 2025-03-30 01:00 GMT -> 02:00 BST */
6+
$tz = new DateTimeZone('Europe/London');
7+
8+
/* +1s then -1s must round-trip */
9+
$dt = new DateTime('2025-03-30 00:59:59', $tz);
10+
$dt->modify('+1 second');
11+
$dt->modify('-1 second');
12+
echo $dt->format('Y-m-d H:i:s T U'), "\n";
13+
14+
/* -1s from 02:00 BST must land at 00:59:59 GMT, not 02:59:59 BST */
15+
$dt2 = new DateTime('2025-03-30 02:00:00', $tz);
16+
echo $dt2->modify('-1 second')->format('Y-m-d H:i:s T U'), "\n";
17+
18+
/* month + hours: +1 month lands before the DST boundary, so +1 hour is plain GMT */
19+
$dt3 = new DateTime('2025-02-28 00:30:00', $tz);
20+
echo $dt3->modify('+1 month +1 hour')->format('Y-m-d H:i:s T U'), "\n";
21+
22+
/* first/last day of must still work */
23+
$base = new DateTimeImmutable('2025-03-15 10:00:00', $tz);
24+
echo $base->modify('first day of next month')->format('Y-m-d H:i:s T'), "\n";
25+
echo $base->modify('last day of this month')->format('Y-m-d H:i:s T'), "\n";
26+
27+
/* +61 minutes from just before the gap -- minutes must also count as elapsed time */
28+
$dt4 = new DateTime('2025-03-30 00:59:00', $tz);
29+
echo $dt4->modify('+61 minutes')->format('Y-m-d H:i:s T U'), "\n";
30+
31+
/* DateTimeImmutable must behave the same as mutable DateTime */
32+
$dt5 = new DateTimeImmutable('2025-03-30 02:00:00', $tz);
33+
echo $dt5->modify('-1 second')->format('Y-m-d H:i:s T U'), "\n";
34+
?>
35+
--EXPECT--
36+
2025-03-30 00:59:59 GMT 1743296399
37+
2025-03-30 00:59:59 GMT 1743296399
38+
2025-03-28 01:30:00 GMT 1743125400
39+
2025-04-01 10:00:00 BST
40+
2025-03-31 10:00:00 BST
41+
2025-03-30 03:00:00 BST 1743300000
42+
2025-03-30 00:59:59 GMT 1743296399

0 commit comments

Comments
 (0)