Repository navigation
Speed up ZonedDateTime conversions, and support years beyond 9999 - #135
Merged
BenMorel merged 6 commits intoOct 8, 2026
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #135 +/- ##
=========================================
Coverage 99.23% 99.23%
- Complexity 1139 1140 +1
=========================================
Files 48 48
Lines 2489 2499 +10
=========================================
+ Hits 2470 2480 +10
Misses 19 19 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Member
|
Hi @gnutix, I'm not really at ease with adding a method to I'd rather restrict the min/max year so that it never fails with PHP's DateTime, and improve performance of ZonedDateTime if we can? |
gnutix
force-pushed
the
feature/local-date-time-to-epoch-second
branch
from
October 6, 2026 20:56
8badf88 to
1467602
Compare
LocalDateTime::toEpochSecond(), and use it in ZonedDateTime::of() for fixed offsetsZonedDateTime::of(), and support years beyond 9999
Contributor
Author
|
PR adapted per your wishes, description updated. |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuH4dkwo3PSoj9VzBkWuxe
gnutix
force-pushed
the
feature/local-date-time-to-epoch-second
branch
from
October 6, 2026 21:05
1467602 to
f65a63d
Compare
…rlaps Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuH4dkwo3PSoj9VzBkWuxe
ZonedDateTime::of(), and support years beyond 9999ZonedDateTime::of() and toNativeDateTime(), and support years beyond 9999
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuH4dkwo3PSoj9VzBkWuxe
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuH4dkwo3PSoj9VzBkWuxe
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuH4dkwo3PSoj9VzBkWuxe
ZonedDateTime::of() and toNativeDateTime(), and support years beyond 9999ZonedDateTime conversions, and support years beyond 9999
BenMorel
approved these changes
Oct 7, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SuH4dkwo3PSoj9VzBkWuxe
Contributor
Author
|
@BenMorel done |
Member
|
Thank you, @gnutix! |
Member
|
Released as 0.10.3. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
ZonedDateTime::of(),ofInstant()andtoNativeDateTime()convert through strings and\DateTime. This is slow,and breaks outside years 0000–9999:
toNativeDateTime()also fails for negative years and years beyond 9999, and returns the wrong instant for the firstoccurrence of an ambiguous time:
Change
No new public API, and no change to the supported year range.
of():toEpochDay(),toSecondOfDay()and the offset, without\DateTime;\DateTime, so gaps and overlaps are unchanged. Years beyond 9999 get theISO 8601
+sign that\DateTimerequires, and the local date-time is no longer re-parsed: in a gap, it isshifted with
plusSeconds().ofInstant(): the local date-time is computed from the instant and the offset (TimeZone::getOffset()for regions),instead of formatting and re-parsing a
\DateTime.toNativeDateTime(): built from the instant (createFromFormat('U.u', ...)), then the time zone is set.TimeZoneOffset::toNativeDateTimeZone(): theDateTimeZoneforZis cached and cloned.new DateTimeZone('Z')takes~8 µs, as PHP looks it up among abbreviations, against ~0.1 µs for other ids or a clone.
Verification against
mainof(): identical on 1.1M date-times (every region and random offsets, years -9999 to 9999, plus every DSTtransition 1970–2040 in 12 regions, probed every 15 minutes ±26 hours).
ofInstant(): identical on 154k instants (every region and random offsets, all years, every transition since 1900in 6 regions). Instants outside the
LocalDaterange still throw aDateTimeException; forInstant::min()andmax()it is now "Invalid year" instead of aDateTimeParseExceptionon a wrapped-around year.toNativeDateTime(): identical timestamp, format, time zone andserialize()output on 128k date-times, except1,056 overlap cases where
mainreturned the wrong timestamp.of()andofInstant();far-year gaps and overlaps behave as in 2030.
Tests
providerOf(): offsets at years ±999999, -1, 0, 9999 and 10000, an offset with seconds; regions with unusualtransitions (Apia skipping a day, Lord Howe 30-minute DST, São Paulo gap at midnight, LMT offsets in seconds),
the 9999 → 10000 boundary,
min()/max(), and Paris in year 10000 with its gap and overlap (expected valuesderived from the EU rule).
ofInstant(): edge cases (offsets with seconds, ±18:00, gap and overlap edges, far years) and out-of-range instants.toNativeDateTime(): far years, and both occurrences of an overlap.toNativeDateTimeZone(): returns a new instance on each call.Benchmark
PHP 8.5, best of 7 runs over 1000 values:
LocalDateTime::atTimeZone(UTC)LocalDateTime::atTimeZone('Europe/Paris')Instant::atTimeZone(UTC)Instant::atTimeZone('+02:00')Instant::atTimeZone('Europe/Paris')Instant::__toString()LocalDateTime::toNativeDateTime()Out of scope
These behave the same on
main, and are left for separate issues:with*()/plus*(): they re-resolve the local date-time withof(), which always picks theoffset closest to UTC, so the first occurrence of an ambiguous time jumps one hour:
2024-10-27T02:30+02:00[Europe/Paris]->withNano(5)gives02:30:00.000000005+01:00.plusDays(0),plusPeriod()andwithTimeZoneSameLocal()do the same. java.time keeps the current offset when it is still valid.+sign (10000-01-01), andLocalDate::parse('+10000-01-01')throws.ISO 8601 and java.time use
+10000-01-01, and JavaScript's Temporal requires+010000.ZonedDateTime::parse()ignores the offset in the text when a region is given:2030-10-27T02:30+02:00[Europe/Paris]comes back as+01:00, and an impossible+05:00[Europe/Paris]is accepted.CET,EET,MET,WET,EST,MSTandHSTare abbreviation-type zones in PHP, so they have no DST.TimeZone::parse('Etc/GMT+12')throws, whileTimeZoneRegion::of('Etc/GMT+12')accepts it.🤖 Generated with Claude Code
https://claude.ai/code/session_01SuH4dkwo3PSoj9VzBkWuxe