mirror of
https://github.com/php/php-src.git
synced 2026-03-24 00:02:20 +01:00
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. Closes GH-20764.
This commit is contained in:
1
NEWS
1
NEWS
@@ -18,6 +18,7 @@ PHP NEWS
|
||||
- Date:
|
||||
. Fixed bug GH-20936 (DatePeriod::__set_state() cannot handle null start).
|
||||
(ndossche)
|
||||
. Fix timezone offset with seconds losing precision. (ndossche)
|
||||
|
||||
- DOM:
|
||||
. Fixed bug GH-21077 (Accessing Dom\Node::baseURI can throw TypeError).
|
||||
|
||||
@@ -795,13 +795,24 @@ static zend_string *date_format(const char *format, size_t format_len, timelib_t
|
||||
case TIMELIB_ZONETYPE_ABBR:
|
||||
length = slprintf(buffer, sizeof(buffer), "%s", offset->abbr);
|
||||
break;
|
||||
case TIMELIB_ZONETYPE_OFFSET:
|
||||
length = slprintf(buffer, sizeof(buffer), "%c%02d:%02d",
|
||||
((offset->offset < 0) ? '-' : '+'),
|
||||
abs(offset->offset / 3600),
|
||||
abs((offset->offset % 3600) / 60)
|
||||
);
|
||||
case TIMELIB_ZONETYPE_OFFSET: {
|
||||
int seconds = offset->offset % 60;
|
||||
if (seconds == 0) {
|
||||
length = slprintf(buffer, sizeof(buffer), "%c%02d:%02d",
|
||||
((offset->offset < 0) ? '-' : '+'),
|
||||
abs(offset->offset / 3600),
|
||||
abs((offset->offset % 3600) / 60)
|
||||
);
|
||||
} else {
|
||||
length = slprintf(buffer, sizeof(buffer), "%c%02d:%02d:%02d",
|
||||
((offset->offset < 0) ? '-' : '+'),
|
||||
abs(offset->offset / 3600),
|
||||
abs((offset->offset % 3600) / 60),
|
||||
abs(seconds)
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -1930,6 +1941,32 @@ static HashTable *date_object_get_gc_timezone(zend_object *object, zval **table,
|
||||
return zend_std_get_properties(object);
|
||||
} /* }}} */
|
||||
|
||||
static zend_string *date_create_tz_offset_str(timelib_sll offset)
|
||||
{
|
||||
int seconds = offset % 60;
|
||||
size_t size;
|
||||
const char *format;
|
||||
|
||||
if (seconds == 0) {
|
||||
size = sizeof("+05:00");
|
||||
format = "%c%02d:%02d";
|
||||
} else {
|
||||
size = sizeof("+05:00:01");
|
||||
format = "%c%02d:%02d:%02d";
|
||||
}
|
||||
|
||||
zend_string *tmpstr = zend_string_alloc(size - 1, 0);
|
||||
|
||||
/* Note: if seconds == 0, the seconds argument will be excessive and therefore ignored. */
|
||||
ZSTR_LEN(tmpstr) = snprintf(ZSTR_VAL(tmpstr), size, format,
|
||||
offset < 0 ? '-' : '+',
|
||||
abs((int)(offset / 3600)),
|
||||
abs((int)(offset % 3600) / 60),
|
||||
abs(seconds));
|
||||
|
||||
return tmpstr;
|
||||
}
|
||||
|
||||
static void date_object_to_hash(php_date_obj *dateobj, HashTable *props)
|
||||
{
|
||||
zval zv;
|
||||
@@ -1947,17 +1984,8 @@ static void date_object_to_hash(php_date_obj *dateobj, HashTable *props)
|
||||
case TIMELIB_ZONETYPE_ID:
|
||||
ZVAL_STRING(&zv, dateobj->time->tz_info->name);
|
||||
break;
|
||||
case TIMELIB_ZONETYPE_OFFSET: {
|
||||
zend_string *tmpstr = zend_string_alloc(sizeof("UTC+05:00")-1, 0);
|
||||
int utc_offset = dateobj->time->z;
|
||||
|
||||
ZSTR_LEN(tmpstr) = snprintf(ZSTR_VAL(tmpstr), sizeof("+05:00"), "%c%02d:%02d",
|
||||
utc_offset < 0 ? '-' : '+',
|
||||
abs(utc_offset / 3600),
|
||||
abs(((utc_offset % 3600) / 60)));
|
||||
|
||||
ZVAL_NEW_STR(&zv, tmpstr);
|
||||
}
|
||||
case TIMELIB_ZONETYPE_OFFSET:
|
||||
ZVAL_NEW_STR(&zv, date_create_tz_offset_str(dateobj->time->z));
|
||||
break;
|
||||
case TIMELIB_ZONETYPE_ABBR:
|
||||
ZVAL_STRING(&zv, dateobj->time->tz_abbr);
|
||||
@@ -2069,29 +2097,8 @@ static void php_timezone_to_string(php_timezone_obj *tzobj, zval *zv)
|
||||
case TIMELIB_ZONETYPE_ID:
|
||||
ZVAL_STRING(zv, tzobj->tzi.tz->name);
|
||||
break;
|
||||
case TIMELIB_ZONETYPE_OFFSET: {
|
||||
timelib_sll utc_offset = tzobj->tzi.utc_offset;
|
||||
int seconds = utc_offset % 60;
|
||||
size_t size;
|
||||
const char *format;
|
||||
if (seconds == 0) {
|
||||
size = sizeof("+05:00");
|
||||
format = "%c%02d:%02d";
|
||||
} else {
|
||||
size = sizeof("+05:00:01");
|
||||
format = "%c%02d:%02d:%02d";
|
||||
}
|
||||
zend_string *tmpstr = zend_string_alloc(size - 1, 0);
|
||||
|
||||
/* Note: if seconds == 0, the seconds argument will be excessive and therefore ignored. */
|
||||
ZSTR_LEN(tmpstr) = snprintf(ZSTR_VAL(tmpstr), size, format,
|
||||
utc_offset < 0 ? '-' : '+',
|
||||
abs((int)(utc_offset / 3600)),
|
||||
abs((int)(utc_offset % 3600) / 60),
|
||||
abs(seconds));
|
||||
|
||||
ZVAL_NEW_STR(zv, tmpstr);
|
||||
}
|
||||
case TIMELIB_ZONETYPE_OFFSET:
|
||||
ZVAL_NEW_STR(zv, date_create_tz_offset_str(tzobj->tzi.utc_offset));
|
||||
break;
|
||||
case TIMELIB_ZONETYPE_ABBR:
|
||||
ZVAL_STRING(zv, tzobj->tzi.z.abbr);
|
||||
|
||||
@@ -15,6 +15,6 @@ echo "\n", (new DatetimeZone('+01:45:30'))->getName();
|
||||
\DateTime::__set_state(array(
|
||||
'date' => '0021-08-21 00:00:00.000000',
|
||||
'timezone_type' => 1,
|
||||
'timezone' => '+00:49',
|
||||
'timezone' => '+00:49:56',
|
||||
))
|
||||
+01:45:30
|
||||
|
||||
53
ext/date/tests/gh20764.phpt
Normal file
53
ext/date/tests/gh20764.phpt
Normal file
@@ -0,0 +1,53 @@
|
||||
--TEST--
|
||||
GH-20764 (Timezone offset with seconds loses precision)
|
||||
--FILE--
|
||||
<?php
|
||||
|
||||
$timezones = [
|
||||
'+03:00:30',
|
||||
'-03:00:30',
|
||||
];
|
||||
|
||||
foreach ($timezones as $timezone) {
|
||||
echo "--- Testing timezone $timezone ---\n";
|
||||
$tz = new DateTimeZone($timezone);
|
||||
$dt = new DateTimeImmutable('2025-04-01', $tz);
|
||||
var_dump($dt->format('e'));
|
||||
var_dump($dt);
|
||||
var_dump(unserialize(serialize($dt))->getTimezone());
|
||||
}
|
||||
|
||||
?>
|
||||
--EXPECTF--
|
||||
--- Testing timezone +03:00:30 ---
|
||||
string(9) "+03:00:30"
|
||||
object(DateTimeImmutable)#%d (3) {
|
||||
["date"]=>
|
||||
string(26) "2025-04-01 00:00:00.000000"
|
||||
["timezone_type"]=>
|
||||
int(1)
|
||||
["timezone"]=>
|
||||
string(9) "+03:00:30"
|
||||
}
|
||||
object(DateTimeZone)#%d (2) {
|
||||
["timezone_type"]=>
|
||||
int(1)
|
||||
["timezone"]=>
|
||||
string(9) "+03:00:30"
|
||||
}
|
||||
--- Testing timezone -03:00:30 ---
|
||||
string(9) "-03:00:30"
|
||||
object(DateTimeImmutable)#%d (3) {
|
||||
["date"]=>
|
||||
string(26) "2025-04-01 00:00:00.000000"
|
||||
["timezone_type"]=>
|
||||
int(1)
|
||||
["timezone"]=>
|
||||
string(9) "-03:00:30"
|
||||
}
|
||||
object(DateTimeZone)#%d (2) {
|
||||
["timezone_type"]=>
|
||||
int(1)
|
||||
["timezone"]=>
|
||||
string(9) "-03:00:30"
|
||||
}
|
||||
Reference in New Issue
Block a user