Skip to content

Commit e76044a

Browse files
committed
Merge branch 'PHP-8.4' into PHP-8.5
* PHP-8.4: Fix timezone offset with seconds losing precision
2 parents f9df448 + ee26417 commit e76044a

4 files changed

Lines changed: 102 additions & 41 deletions

File tree

NEWS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ PHP NEWS
1919
- Date:
2020
. Fixed bug GH-20936 (DatePeriod::__set_state() cannot handle null start).
2121
(ndossche)
22+
. Fix timezone offset with seconds losing precision. (ndossche)
2223

2324
- DOM:
2425
. Fixed bug GH-21077 (Accessing Dom\Node::baseURI can throw TypeError).

ext/date/php_date.c

Lines changed: 47 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -797,13 +797,24 @@ static zend_string *date_format(const char *format, size_t format_len, const tim
797797
case TIMELIB_ZONETYPE_ABBR:
798798
length = slprintf(buffer, sizeof(buffer), "%s", offset->abbr);
799799
break;
800-
case TIMELIB_ZONETYPE_OFFSET:
801-
length = slprintf(buffer, sizeof(buffer), "%c%02d:%02d",
802-
((offset->offset < 0) ? '-' : '+'),
803-
abs(offset->offset / 3600),
804-
abs((offset->offset % 3600) / 60)
805-
);
800+
case TIMELIB_ZONETYPE_OFFSET: {
801+
int seconds = offset->offset % 60;
802+
if (seconds == 0) {
803+
length = slprintf(buffer, sizeof(buffer), "%c%02d:%02d",
804+
((offset->offset < 0) ? '-' : '+'),
805+
abs(offset->offset / 3600),
806+
abs((offset->offset % 3600) / 60)
807+
);
808+
} else {
809+
length = slprintf(buffer, sizeof(buffer), "%c%02d:%02d:%02d",
810+
((offset->offset < 0) ? '-' : '+'),
811+
abs(offset->offset / 3600),
812+
abs((offset->offset % 3600) / 60),
813+
abs(seconds)
814+
);
815+
}
806816
break;
817+
}
807818
}
808819
}
809820
break;
@@ -1894,6 +1905,32 @@ static HashTable *date_object_get_gc_timezone(zend_object *object, zval **table,
18941905
return zend_std_get_properties(object);
18951906
} /* }}} */
18961907

1908+
static zend_string *date_create_tz_offset_str(timelib_sll offset)
1909+
{
1910+
int seconds = offset % 60;
1911+
size_t size;
1912+
const char *format;
1913+
1914+
if (seconds == 0) {
1915+
size = sizeof("+05:00");
1916+
format = "%c%02d:%02d";
1917+
} else {
1918+
size = sizeof("+05:00:01");
1919+
format = "%c%02d:%02d:%02d";
1920+
}
1921+
1922+
zend_string *tmpstr = zend_string_alloc(size - 1, 0);
1923+
1924+
/* Note: if seconds == 0, the seconds argument will be excessive and therefore ignored. */
1925+
ZSTR_LEN(tmpstr) = snprintf(ZSTR_VAL(tmpstr), size, format,
1926+
offset < 0 ? '-' : '+',
1927+
abs((int)(offset / 3600)),
1928+
abs((int)(offset % 3600) / 60),
1929+
abs(seconds));
1930+
1931+
return tmpstr;
1932+
}
1933+
18971934
static void date_object_to_hash(php_date_obj *dateobj, HashTable *props)
18981935
{
18991936
zval zv;
@@ -1911,17 +1948,8 @@ static void date_object_to_hash(php_date_obj *dateobj, HashTable *props)
19111948
case TIMELIB_ZONETYPE_ID:
19121949
ZVAL_STRING(&zv, dateobj->time->tz_info->name);
19131950
break;
1914-
case TIMELIB_ZONETYPE_OFFSET: {
1915-
zend_string *tmpstr = zend_string_alloc(sizeof("UTC+05:00")-1, 0);
1916-
int utc_offset = dateobj->time->z;
1917-
1918-
ZSTR_LEN(tmpstr) = snprintf(ZSTR_VAL(tmpstr), sizeof("+05:00"), "%c%02d:%02d",
1919-
utc_offset < 0 ? '-' : '+',
1920-
abs(utc_offset / 3600),
1921-
abs(((utc_offset % 3600) / 60)));
1922-
1923-
ZVAL_NEW_STR(&zv, tmpstr);
1924-
}
1951+
case TIMELIB_ZONETYPE_OFFSET:
1952+
ZVAL_NEW_STR(&zv, date_create_tz_offset_str(dateobj->time->z));
19251953
break;
19261954
case TIMELIB_ZONETYPE_ABBR:
19271955
ZVAL_STRING(&zv, dateobj->time->tz_abbr);
@@ -2033,29 +2061,8 @@ static void php_timezone_to_string(php_timezone_obj *tzobj, zval *zv)
20332061
case TIMELIB_ZONETYPE_ID:
20342062
ZVAL_STRING(zv, tzobj->tzi.tz->name);
20352063
break;
2036-
case TIMELIB_ZONETYPE_OFFSET: {
2037-
timelib_sll utc_offset = tzobj->tzi.utc_offset;
2038-
int seconds = utc_offset % 60;
2039-
size_t size;
2040-
const char *format;
2041-
if (seconds == 0) {
2042-
size = sizeof("+05:00");
2043-
format = "%c%02d:%02d";
2044-
} else {
2045-
size = sizeof("+05:00:01");
2046-
format = "%c%02d:%02d:%02d";
2047-
}
2048-
zend_string *tmpstr = zend_string_alloc(size - 1, 0);
2049-
2050-
/* Note: if seconds == 0, the seconds argument will be excessive and therefore ignored. */
2051-
ZSTR_LEN(tmpstr) = snprintf(ZSTR_VAL(tmpstr), size, format,
2052-
utc_offset < 0 ? '-' : '+',
2053-
abs((int)(utc_offset / 3600)),
2054-
abs((int)(utc_offset % 3600) / 60),
2055-
abs(seconds));
2056-
2057-
ZVAL_NEW_STR(zv, tmpstr);
2058-
}
2064+
case TIMELIB_ZONETYPE_OFFSET:
2065+
ZVAL_NEW_STR(zv, date_create_tz_offset_str(tzobj->tzi.utc_offset));
20592066
break;
20602067
case TIMELIB_ZONETYPE_ABBR:
20612068
ZVAL_STRING(zv, tzobj->tzi.z.abbr);

ext/date/tests/bug81565.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@ echo "\n", (new DatetimeZone('+01:45:30'))->getName();
1515
\DateTime::__set_state(array(
1616
'date' => '0021-08-21 00:00:00.000000',
1717
'timezone_type' => 1,
18-
'timezone' => '+00:49',
18+
'timezone' => '+00:49:56',
1919
))
2020
+01:45:30

ext/date/tests/gh20764.phpt

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
--TEST--
2+
GH-20764 (Timezone offset with seconds loses precision)
3+
--FILE--
4+
<?php
5+
6+
$timezones = [
7+
'+03:00:30',
8+
'-03:00:30',
9+
];
10+
11+
foreach ($timezones as $timezone) {
12+
echo "--- Testing timezone $timezone ---\n";
13+
$tz = new DateTimeZone($timezone);
14+
$dt = new DateTimeImmutable('2025-04-01', $tz);
15+
var_dump($dt->format('e'));
16+
var_dump($dt);
17+
var_dump(unserialize(serialize($dt))->getTimezone());
18+
}
19+
20+
?>
21+
--EXPECTF--
22+
--- Testing timezone +03:00:30 ---
23+
string(9) "+03:00:30"
24+
object(DateTimeImmutable)#%d (3) {
25+
["date"]=>
26+
string(26) "2025-04-01 00:00:00.000000"
27+
["timezone_type"]=>
28+
int(1)
29+
["timezone"]=>
30+
string(9) "+03:00:30"
31+
}
32+
object(DateTimeZone)#%d (2) {
33+
["timezone_type"]=>
34+
int(1)
35+
["timezone"]=>
36+
string(9) "+03:00:30"
37+
}
38+
--- Testing timezone -03:00:30 ---
39+
string(9) "-03:00:30"
40+
object(DateTimeImmutable)#%d (3) {
41+
["date"]=>
42+
string(26) "2025-04-01 00:00:00.000000"
43+
["timezone_type"]=>
44+
int(1)
45+
["timezone"]=>
46+
string(9) "-03:00:30"
47+
}
48+
object(DateTimeZone)#%d (2) {
49+
["timezone_type"]=>
50+
int(1)
51+
["timezone"]=>
52+
string(9) "-03:00:30"
53+
}

0 commit comments

Comments
 (0)