fix: emit full seconds in Instant and ZonedDateTime toISOString (RFC 3339)

Instant::toISOString() and ZonedDateTime::toISOString() emitted a compact
time that dropped zero seconds (e.g. 2023-11-14T22:14Z), which is valid
ISO 8601 but not valid RFC 3339. Section 5.6 of RFC 3339 requires the full
HH:MM:SS time even when the seconds are zero. These types are meant for
interoperability, so they must always emit RFC 3339 compliant output.

Build the time part of ZonedDateTime::toISOString() explicitly with a
mandatory seconds component instead of delegating to LocalTime's compact
formatter. Instant delegates to ZonedDateTime, so it is fixed transitively;
jsonSerialize() and __toString() route through toISOString(). LocalTime's
documented compact format is intentional and left unchanged.

This changes the serialized string representation and is therefore a BC
break; fixtures that pinned the compact output are updated accordingly.
This commit is contained in:
dualfroz committed 2026-09-04 08:53:18 +02:00
1 parent 104867f051
commit 6bb686d4af
6 files changed
+163 -36

No files matched your search

+33 -30
View File
@@ -436,7 +436,7 @@ class ZonedDateTimeTest extends AbstractTestCase
{
return [
['2018-07-21 14:09:10.23456', 'America/Los_Angeles', '2018-07-21T14:09:10.23456-07:00[America/Los_Angeles]'],
['2019-01-21 17:59', 'America/Los_Angeles', '2019-01-21T17:59-08:00[America/Los_Angeles]'],
['2019-01-21 17:59', 'America/Los_Angeles', '2019-01-21T17:59:00-08:00[America/Los_Angeles]'],
['2019-01-23 09:10:11.123', '+05:30', '2019-01-23T09:10:11.123+05:30'],
];
}
@@ -753,12 +753,12 @@ class ZonedDateTimeTest extends AbstractTestCase
['2000-01-01T12:34:56.123456789-08:00[America/Los_Angeles]', 25, '2000-06-24T12:34:56.123456789-07:00[America/Los_Angeles]'],
// https://github.com/brick/date-time/issues/115
['2025-03-23T01:00+01:00[Europe/Prague]', 1, '2025-03-30T01:00+01:00[Europe/Prague]'],
['2025-03-23T01:30+01:00[Europe/Prague]', 1, '2025-03-30T01:30+01:00[Europe/Prague]'],
['2025-03-23T02:00+01:00[Europe/Prague]', 1, '2025-03-30T03:00+02:00[Europe/Prague]'],
['2025-03-23T02:30+01:00[Europe/Prague]', 1, '2025-03-30T03:30+02:00[Europe/Prague]'],
['2025-03-23T03:00+01:00[Europe/Prague]', 1, '2025-03-30T03:00+02:00[Europe/Prague]'],
['2025-03-23T03:30+01:00[Europe/Prague]', 1, '2025-03-30T03:30+02:00[Europe/Prague]'],
['2025-03-23T01:00+01:00[Europe/Prague]', 1, '2025-03-30T01:00:00+01:00[Europe/Prague]'],
['2025-03-23T01:30+01:00[Europe/Prague]', 1, '2025-03-30T01:30:00+01:00[Europe/Prague]'],
['2025-03-23T02:00+01:00[Europe/Prague]', 1, '2025-03-30T03:00:00+02:00[Europe/Prague]'],
['2025-03-23T02:30+01:00[Europe/Prague]', 1, '2025-03-30T03:30:00+02:00[Europe/Prague]'],
['2025-03-23T03:00+01:00[Europe/Prague]', 1, '2025-03-30T03:00:00+02:00[Europe/Prague]'],
['2025-03-23T03:30+01:00[Europe/Prague]', 1, '2025-03-30T03:30:00+02:00[Europe/Prague]'],
];
}
@@ -782,12 +782,12 @@ class ZonedDateTimeTest extends AbstractTestCase
['2000-04-03T12:34:56.123456789-07:00[America/Los_Angeles]', -2, '2000-04-01T12:34:56.123456789-08:00[America/Los_Angeles]'],
// https://github.com/brick/date-time/issues/115
['2025-03-29T01:00+01:00[Europe/Prague]', 1, '2025-03-30T01:00+01:00[Europe/Prague]'],
['2025-03-29T01:30+01:00[Europe/Prague]', 1, '2025-03-30T01:30+01:00[Europe/Prague]'],
['2025-03-29T02:00+01:00[Europe/Prague]', 1, '2025-03-30T03:00+02:00[Europe/Prague]'],
['2025-03-29T02:30+01:00[Europe/Prague]', 1, '2025-03-30T03:30+02:00[Europe/Prague]'],
['2025-03-29T03:00+01:00[Europe/Prague]', 1, '2025-03-30T03:00+02:00[Europe/Prague]'],
['2025-03-29T03:30+01:00[Europe/Prague]', 1, '2025-03-30T03:30+02:00[Europe/Prague]'],
['2025-03-29T01:00+01:00[Europe/Prague]', 1, '2025-03-30T01:00:00+01:00[Europe/Prague]'],
['2025-03-29T01:30+01:00[Europe/Prague]', 1, '2025-03-30T01:30:00+01:00[Europe/Prague]'],
['2025-03-29T02:00+01:00[Europe/Prague]', 1, '2025-03-30T03:00:00+02:00[Europe/Prague]'],
['2025-03-29T02:30+01:00[Europe/Prague]', 1, '2025-03-30T03:30:00+02:00[Europe/Prague]'],
['2025-03-29T03:00+01:00[Europe/Prague]', 1, '2025-03-30T03:00:00+02:00[Europe/Prague]'],
['2025-03-29T03:30+01:00[Europe/Prague]', 1, '2025-03-30T03:30:00+02:00[Europe/Prague]'],
];
}
@@ -815,12 +815,12 @@ class ZonedDateTimeTest extends AbstractTestCase
['2000-04-02T04:12:34.123456789-07:00[America/Los_Angeles]', 1, '2000-04-02T05:12:34.123456789-07:00[America/Los_Angeles]'],
// https://github.com/brick/date-time/issues/115
['2025-03-29T01:00+01:00[Europe/Prague]', 24, '2025-03-30T01:00+01:00[Europe/Prague]'],
['2025-03-29T01:30+01:00[Europe/Prague]', 24, '2025-03-30T01:30+01:00[Europe/Prague]'],
['2025-03-29T02:00+01:00[Europe/Prague]', 24, '2025-03-30T03:00+02:00[Europe/Prague]'],
['2025-03-29T02:30+01:00[Europe/Prague]', 24, '2025-03-30T03:30+02:00[Europe/Prague]'],
['2025-03-29T03:00+01:00[Europe/Prague]', 24, '2025-03-30T04:00+02:00[Europe/Prague]'],
['2025-03-29T03:30+01:00[Europe/Prague]', 24, '2025-03-30T04:30+02:00[Europe/Prague]'],
['2025-03-29T01:00+01:00[Europe/Prague]', 24, '2025-03-30T01:00:00+01:00[Europe/Prague]'],
['2025-03-29T01:30+01:00[Europe/Prague]', 24, '2025-03-30T01:30:00+01:00[Europe/Prague]'],
['2025-03-29T02:00+01:00[Europe/Prague]', 24, '2025-03-30T03:00:00+02:00[Europe/Prague]'],
['2025-03-29T02:30+01:00[Europe/Prague]', 24, '2025-03-30T03:30:00+02:00[Europe/Prague]'],
['2025-03-29T03:00+01:00[Europe/Prague]', 24, '2025-03-30T04:00:00+02:00[Europe/Prague]'],
['2025-03-29T03:30+01:00[Europe/Prague]', 24, '2025-03-30T04:30:00+02:00[Europe/Prague]'],
];
}
@@ -848,10 +848,10 @@ class ZonedDateTimeTest extends AbstractTestCase
['2000-04-02T03:01:01.654321-07:00[America/Los_Angeles]', 1, '2000-04-02T03:02:01.654321-07:00[America/Los_Angeles]'],
// https://github.com/brick/date-time/issues/115
['2025-03-30T01:30+01:00[Europe/Prague]', 50, '2025-03-30T03:20+02:00[Europe/Prague]'],
['2025-03-30T01:30+01:00[Europe/Prague]', 100, '2025-03-30T04:10+02:00[Europe/Prague]'],
['2025-03-30T03:20+02:00[Europe/Prague]', -50, '2025-03-30T01:30+01:00[Europe/Prague]'],
['2025-03-30T04:10+02:00[Europe/Prague]', -100, '2025-03-30T01:30+01:00[Europe/Prague]'],
['2025-03-30T01:30:00+01:00[Europe/Prague]', 50, '2025-03-30T03:20:00+02:00[Europe/Prague]'],
['2025-03-30T01:30:00+01:00[Europe/Prague]', 100, '2025-03-30T04:10:00+02:00[Europe/Prague]'],
['2025-03-30T03:20:00+02:00[Europe/Prague]', -50, '2025-03-30T01:30:00+01:00[Europe/Prague]'],
['2025-03-30T04:10:00+02:00[Europe/Prague]', -100, '2025-03-30T01:30:00+01:00[Europe/Prague]'],
];
}
@@ -878,10 +878,10 @@ class ZonedDateTimeTest extends AbstractTestCase
['2000-04-02T03:00:01.654321-07:00[America/Los_Angeles]', 1, '2000-04-02T03:00:02.654321-07:00[America/Los_Angeles]'],
// https://github.com/brick/date-time/issues/115
['2025-03-30T01:30+01:00[Europe/Prague]', 3000, '2025-03-30T03:20+02:00[Europe/Prague]'],
['2025-03-30T01:30+01:00[Europe/Prague]', 6000, '2025-03-30T04:10+02:00[Europe/Prague]'],
['2025-03-30T03:20+02:00[Europe/Prague]', -3000, '2025-03-30T01:30+01:00[Europe/Prague]'],
['2025-03-30T04:10+02:00[Europe/Prague]', -6000, '2025-03-30T01:30+01:00[Europe/Prague]'],
['2025-03-30T01:30:00+01:00[Europe/Prague]', 3000, '2025-03-30T03:20:00+02:00[Europe/Prague]'],
['2025-03-30T01:30:00+01:00[Europe/Prague]', 6000, '2025-03-30T04:10:00+02:00[Europe/Prague]'],
['2025-03-30T03:20:00+02:00[Europe/Prague]', -3000, '2025-03-30T01:30:00+01:00[Europe/Prague]'],
['2025-03-30T04:10:00+02:00[Europe/Prague]', -6000, '2025-03-30T01:30:00+01:00[Europe/Prague]'],
];
}
@@ -1086,6 +1086,9 @@ class ZonedDateTimeTest extends AbstractTestCase
return [
['2000-01-20T12:34:56.123456789', 'America/Los_Angeles', '2000-01-20T12:34:56.123456789-08:00[America/Los_Angeles]'],
['2000-01-20T12:34:56.123456789', '-07:00', '2000-01-20T12:34:56.123456789-07:00'],
// whole minute and whole hour must keep the full ":00" seconds (RFC 3339 section 5.6)
['2000-01-20T12:34', '-07:00', '2000-01-20T12:34:00-07:00'],
['2000-01-20T12:00', 'America/Los_Angeles', '2000-01-20T12:00:00-08:00[America/Los_Angeles]'],
];
}
@@ -1121,13 +1124,13 @@ class ZonedDateTimeTest extends AbstractTestCase
public static function providerGetIntervalTo(): array
{
return [
['2023-01-01T10:00:00Z', '2023-01-01T10:00:00Z', '2023-01-01T10:00Z/2023-01-01T10:00Z'],
['2023-01-01T10:00:00Z', '2023-01-01T10:00:10Z', '2023-01-01T10:00Z/2023-01-01T10:00:10Z'],
['2023-01-01T10:00:00Z', '2023-01-01T10:00:00Z', '2023-01-01T10:00:00Z/2023-01-01T10:00:00Z'],
['2023-01-01T10:00:00Z', '2023-01-01T10:00:10Z', '2023-01-01T10:00:00Z/2023-01-01T10:00:10Z'],
['2023-01-01T10:00:00.001Z', '2023-01-01T10:00:10.002Z', '2023-01-01T10:00:00.001Z/2023-01-01T10:00:10.002Z'],
['2023-01-01T10:00:00.001Z', '2023-01-01T13:00:10.002+03:00', '2023-01-01T10:00:00.001Z/2023-01-01T10:00:10.002Z'],
['2023-01-01T10:00:00.001+03:00', '2023-01-01T13:00:10.002+03:00', '2023-01-01T07:00:00.001Z/2023-01-01T10:00:10.002Z'],
['2023-01-01T10:00:00.000000001Z', '2023-01-01T10:00:00.000000009Z', '2023-01-01T10:00:00.000000001Z/2023-01-01T10:00:00.000000009Z'],
['2023-01-01T10:00:00Z', '2023-01-02T10:00:00Z', '2023-01-01T10:00Z/2023-01-02T10:00Z'],
['2023-01-01T10:00:00Z', '2023-01-02T10:00:00Z', '2023-01-01T10:00:00Z/2023-01-02T10:00:00Z'],
];
}