Skip to content

Commit 205bd3d

Browse files
committed
Fix timezone offset with seconds losing precision
There are two issues: 1. The 'e' formatter doesn't output the seconds of the timezone even if it has seconds. 2. var_dump(), (array) cast, serialization, ... don't include the timezone second offset in the output. This means that, for example, serializing and then unserializing a date object loses the seconds of the timezone. This can be observed by comparing the output of getTimezone() for `$dt` vs the unserialized object in the provided test.
1 parent e63dae2 commit 205bd3d

File tree

2 files changed

+73
-40
lines changed

2 files changed

+73
-40
lines changed

ext/date/php_date.c

Lines changed: 45 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, timelib_t
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;
@@ -1921,6 +1932,30 @@ static HashTable *date_object_get_gc_timezone(zend_object *object, zval **table,
19211932
return zend_std_get_properties(object);
19221933
} /* }}} */
19231934

1935+
static zend_string *date_create_tz_offset_str(timelib_sll offset)
1936+
{
1937+
int seconds = offset % 60;
1938+
size_t size;
1939+
const char *format;
1940+
if (seconds == 0) {
1941+
size = sizeof("+05:00");
1942+
format = "%c%02d:%02d";
1943+
} else {
1944+
size = sizeof("+05:00:01");
1945+
format = "%c%02d:%02d:%02d";
1946+
}
1947+
zend_string *tmpstr = zend_string_alloc(size - 1, 0);
1948+
1949+
/* Note: if seconds == 0, the seconds argument will be excessive and therefore ignored. */
1950+
ZSTR_LEN(tmpstr) = snprintf(ZSTR_VAL(tmpstr), size, format,
1951+
offset < 0 ? '-' : '+',
1952+
abs((int)(offset / 3600)),
1953+
abs((int)(offset % 3600) / 60),
1954+
abs(seconds));
1955+
1956+
return tmpstr;
1957+
}
1958+
19241959
static void date_object_to_hash(php_date_obj *dateobj, HashTable *props)
19251960
{
19261961
zval zv;
@@ -1938,17 +1973,8 @@ static void date_object_to_hash(php_date_obj *dateobj, HashTable *props)
19381973
case TIMELIB_ZONETYPE_ID:
19391974
ZVAL_STRING(&zv, dateobj->time->tz_info->name);
19401975
break;
1941-
case TIMELIB_ZONETYPE_OFFSET: {
1942-
zend_string *tmpstr = zend_string_alloc(sizeof("UTC+05:00")-1, 0);
1943-
int utc_offset = dateobj->time->z;
1944-
1945-
ZSTR_LEN(tmpstr) = snprintf(ZSTR_VAL(tmpstr), sizeof("+05:00"), "%c%02d:%02d",
1946-
utc_offset < 0 ? '-' : '+',
1947-
abs(utc_offset / 3600),
1948-
abs(((utc_offset % 3600) / 60)));
1949-
1950-
ZVAL_NEW_STR(&zv, tmpstr);
1951-
}
1976+
case TIMELIB_ZONETYPE_OFFSET:
1977+
ZVAL_NEW_STR(&zv, date_create_tz_offset_str(dateobj->time->z));
19521978
break;
19531979
case TIMELIB_ZONETYPE_ABBR:
19541980
ZVAL_STRING(&zv, dateobj->time->tz_abbr);
@@ -2060,29 +2086,8 @@ static void php_timezone_to_string(php_timezone_obj *tzobj, zval *zv)
20602086
case TIMELIB_ZONETYPE_ID:
20612087
ZVAL_STRING(zv, tzobj->tzi.tz->name);
20622088
break;
2063-
case TIMELIB_ZONETYPE_OFFSET: {
2064-
timelib_sll utc_offset = tzobj->tzi.utc_offset;
2065-
int seconds = utc_offset % 60;
2066-
size_t size;
2067-
const char *format;
2068-
if (seconds == 0) {
2069-
size = sizeof("+05:00");
2070-
format = "%c%02d:%02d";
2071-
} else {
2072-
size = sizeof("+05:00:01");
2073-
format = "%c%02d:%02d:%02d";
2074-
}
2075-
zend_string *tmpstr = zend_string_alloc(size - 1, 0);
2076-
2077-
/* Note: if seconds == 0, the seconds argument will be excessive and therefore ignored. */
2078-
ZSTR_LEN(tmpstr) = snprintf(ZSTR_VAL(tmpstr), size, format,
2079-
utc_offset < 0 ? '-' : '+',
2080-
abs((int)(utc_offset / 3600)),
2081-
abs((int)(utc_offset % 3600) / 60),
2082-
abs(seconds));
2083-
2084-
ZVAL_NEW_STR(zv, tmpstr);
2085-
}
2089+
case TIMELIB_ZONETYPE_OFFSET:
2090+
ZVAL_NEW_STR(zv, date_create_tz_offset_str(tzobj->tzi.utc_offset));
20862091
break;
20872092
case TIMELIB_ZONETYPE_ABBR:
20882093
ZVAL_STRING(zv, tzobj->tzi.z.abbr);

ext/date/tests/gh20764.phpt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
--TEST--
2+
GH-20764 (Timezone offset with seconds loses precision)
3+
--FILE--
4+
<?php
5+
6+
$tz = new DateTimeZone('+03:00:30');
7+
$dt = new DateTimeImmutable('2025-04-01', $tz);
8+
var_dump($dt->format('e'));
9+
var_dump($dt);
10+
var_dump(unserialize(serialize($dt))->getTimezone());
11+
12+
?>
13+
--EXPECT--
14+
string(9) "+03:00:30"
15+
object(DateTimeImmutable)#2 (3) {
16+
["date"]=>
17+
string(26) "2025-04-01 00:00:00.000000"
18+
["timezone_type"]=>
19+
int(1)
20+
["timezone"]=>
21+
string(9) "+03:00:30"
22+
}
23+
object(DateTimeZone)#4 (2) {
24+
["timezone_type"]=>
25+
int(1)
26+
["timezone"]=>
27+
string(9) "+03:00:30"
28+
}

0 commit comments

Comments
 (0)