Skip to content

Commit 343cdb6

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 c929f2a commit 343cdb6

3 files changed

Lines changed: 74 additions & 41 deletions

File tree

ext/date/php_date.c

Lines changed: 45 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -795,13 +795,24 @@ static zend_string *date_format(const char *format, size_t format_len, timelib_t
795795
case TIMELIB_ZONETYPE_ABBR:
796796
length = slprintf(buffer, sizeof(buffer), "%s", offset->abbr);
797797
break;
798-
case TIMELIB_ZONETYPE_OFFSET:
799-
length = slprintf(buffer, sizeof(buffer), "%c%02d:%02d",
800-
((offset->offset < 0) ? '-' : '+'),
801-
abs(offset->offset / 3600),
802-
abs((offset->offset % 3600) / 60)
803-
);
798+
case TIMELIB_ZONETYPE_OFFSET: {
799+
int seconds = offset->offset % 60;
800+
if (seconds == 0) {
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+
);
806+
} else {
807+
length = slprintf(buffer, sizeof(buffer), "%c%02d:%02d:%02d",
808+
((offset->offset < 0) ? '-' : '+'),
809+
abs(offset->offset / 3600),
810+
abs((offset->offset % 3600) / 60),
811+
abs(seconds)
812+
);
813+
}
804814
break;
815+
}
805816
}
806817
}
807818
break;
@@ -1930,6 +1941,30 @@ static HashTable *date_object_get_gc_timezone(zend_object *object, zval **table,
19301941
return zend_std_get_properties(object);
19311942
} /* }}} */
19321943

1944+
static zend_string *date_create_tz_offset_str(timelib_sll offset)
1945+
{
1946+
int seconds = offset % 60;
1947+
size_t size;
1948+
const char *format;
1949+
if (seconds == 0) {
1950+
size = sizeof("+05:00");
1951+
format = "%c%02d:%02d";
1952+
} else {
1953+
size = sizeof("+05:00:01");
1954+
format = "%c%02d:%02d:%02d";
1955+
}
1956+
zend_string *tmpstr = zend_string_alloc(size - 1, 0);
1957+
1958+
/* Note: if seconds == 0, the seconds argument will be excessive and therefore ignored. */
1959+
ZSTR_LEN(tmpstr) = snprintf(ZSTR_VAL(tmpstr), size, format,
1960+
offset < 0 ? '-' : '+',
1961+
abs((int)(offset / 3600)),
1962+
abs((int)(offset % 3600) / 60),
1963+
abs(seconds));
1964+
1965+
return tmpstr;
1966+
}
1967+
19331968
static void date_object_to_hash(php_date_obj *dateobj, HashTable *props)
19341969
{
19351970
zval zv;
@@ -1947,17 +1982,8 @@ static void date_object_to_hash(php_date_obj *dateobj, HashTable *props)
19471982
case TIMELIB_ZONETYPE_ID:
19481983
ZVAL_STRING(&zv, dateobj->time->tz_info->name);
19491984
break;
1950-
case TIMELIB_ZONETYPE_OFFSET: {
1951-
zend_string *tmpstr = zend_string_alloc(sizeof("UTC+05:00")-1, 0);
1952-
int utc_offset = dateobj->time->z;
1953-
1954-
ZSTR_LEN(tmpstr) = snprintf(ZSTR_VAL(tmpstr), sizeof("+05:00"), "%c%02d:%02d",
1955-
utc_offset < 0 ? '-' : '+',
1956-
abs(utc_offset / 3600),
1957-
abs(((utc_offset % 3600) / 60)));
1958-
1959-
ZVAL_NEW_STR(&zv, tmpstr);
1960-
}
1985+
case TIMELIB_ZONETYPE_OFFSET:
1986+
ZVAL_NEW_STR(&zv, date_create_tz_offset_str(dateobj->time->z));
19611987
break;
19621988
case TIMELIB_ZONETYPE_ABBR:
19631989
ZVAL_STRING(&zv, dateobj->time->tz_abbr);
@@ -2069,29 +2095,8 @@ static void php_timezone_to_string(php_timezone_obj *tzobj, zval *zv)
20692095
case TIMELIB_ZONETYPE_ID:
20702096
ZVAL_STRING(zv, tzobj->tzi.tz->name);
20712097
break;
2072-
case TIMELIB_ZONETYPE_OFFSET: {
2073-
timelib_sll utc_offset = tzobj->tzi.utc_offset;
2074-
int seconds = utc_offset % 60;
2075-
size_t size;
2076-
const char *format;
2077-
if (seconds == 0) {
2078-
size = sizeof("+05:00");
2079-
format = "%c%02d:%02d";
2080-
} else {
2081-
size = sizeof("+05:00:01");
2082-
format = "%c%02d:%02d:%02d";
2083-
}
2084-
zend_string *tmpstr = zend_string_alloc(size - 1, 0);
2085-
2086-
/* Note: if seconds == 0, the seconds argument will be excessive and therefore ignored. */
2087-
ZSTR_LEN(tmpstr) = snprintf(ZSTR_VAL(tmpstr), size, format,
2088-
utc_offset < 0 ? '-' : '+',
2089-
abs((int)(utc_offset / 3600)),
2090-
abs((int)(utc_offset % 3600) / 60),
2091-
abs(seconds));
2092-
2093-
ZVAL_NEW_STR(zv, tmpstr);
2094-
}
2098+
case TIMELIB_ZONETYPE_OFFSET:
2099+
ZVAL_NEW_STR(zv, date_create_tz_offset_str(tzobj->tzi.utc_offset));
20952100
break;
20962101
case TIMELIB_ZONETYPE_ABBR:
20972102
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: 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)