-
Notifications
You must be signed in to change notification settings - Fork 113
Implement Foundation-based time zone support for Darwin systems with comprehensive DST handling #539
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
@@ -14,7 +14,7 @@ internal class TzdbInRegistry: TimeZoneDatabase { | |||
|
|||
// TODO: starting version 1703 of Windows 10, the ICU library is also bundled, with more accurate/ timezone information. | |||
// When Kotlin/Native drops support for Windows 7, we should investigate moving to the ICU. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JIC, Windows 7 support is deprecated in Kotlin 2.2.0 (https://github.com/JetBrains/kotlin-web-site/blob/c6ba517be0c45fd1c172294694701792c08ff3dc/docs/topics/whatsnew/whatsnew22.md#windows-7-target-deprecated)
1662d5c
to
bb122bb
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've only taken a cursory look so far and will add more comments later, but here are some things I've already noticed.
It does look like the implementation works well, but I'll need to dive deep.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It troubles me that this implementation strongly relies on a contract that I haven't found stated anywhere. Have I missed some note in the Apple docs?
I've rewritten the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The one significant problem here is that the fallback does not report "UTC"
as an available timezone identifier. Other than that, all I have are small stylistic suggestions. Thanks!
…oper initialization
…d standard time zones
…ict its visibility
…readability and variable reuse
…mproved readability and variable reuse
… to TimeZoneRules.kt
…tcOffset creation in TimeZoneRulesFoundation
…ance error messages
…onAfterDate rules
…ndant computations in TimeZoneRulesFoundation
…mmon` for consistency across modules
…arity in `TimeZoneNative`
…s for improved clarity in `TimeZoneNativeTest`.
…xes instead of containment.
bfc1725
to
59234f5
Compare
…BeforeTransition`.
…"; ensure "UTC" is included in `getAvailableZoneIdsFoundation` by default.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, this looks good!
…etime` functions in `TimeZoneRules` and `TimeZoneRulesFoundation`.
Summary
This PR addresses issue #485 by leveraging Apple’s native Foundation time zone data if tzdb is not available. The goal is to make time zone computations work correctly on Darwin devices if tzdb is not available for some reasons, while ensuring the behavior remains consistent with other platforms.
Solution: Abstract Time Zone Rules and Introduce Foundation Implementation
The core of the solution is to abstract time zone logic behind a new internal interface,
TimeZoneRules
. This interface (introduced in this PR) defines the essential operations needed for time zone conversions: retrieving the offset at a givenInstant
(infoAtInstant
) and obtaining detailed offset transition info for a givenLocalDateTime
(infoAtDatetime
). By extracting this interface, we can provide multiple implementations of time zone rules. The existing tzdb-based logic has been refactored into an implementation calledTimeZoneRulesCommon
(which reads from tzdb as before), and a new implementation calledTimeZoneRulesFoundation
has been added for Darwin platforms. The TimeZone API now uses these abstractions under the hood, selecting the appropriate rules implementation based on the platform. For example, on iOS or macOS,TimeZone.of(zoneId)
will use the Foundation-backed rules (if tzdb is not available), whereas on other targets it continues to use tzdb data.Implementation Details of TimeZoneRulesFoundation (Darwin)
The
TimeZoneRulesFoundation
class uses Apple’s Foundation framework (NSTimeZone
,NSCalendar
,NSDate
) to obtain time zone information. Upon initialization, it looks up theNSTimeZone
by the zone identifier. TheinfoAtInstant(instant)
function is straightforward: it converts the Instant to anNSDate
and callsNSTimeZone.secondsFromGMT(for: date)
to get the raw UTC offset in seconds, returning it as aUtcOffset
. The more complex logic resides ininfoAtDatetime(localDateTime)
, which must handle situations like Daylight Saving Time transitions (gaps and overlaps in local time). This method uses anNSCalendar
set to the specific time zone to convert a KotlinLocalDateTime
to anNSDate
. If the local datetime does not exist (!components.isValidDateInCalendar(calendar)
, this happens during a spring-forward DST gap when clocks jump forward) - the code identifies it as a gap. In a gap scenario,TimeZoneRulesFoundation
finds the next DST transition before the missing hour and constructs anOffsetInfo.Gap
object, providing the transition instant (start of the gap), as well as the offsets immediately before and after the gap. On the other hand, if thecomponents
is valid date in calendar (meaning the local time exists), the code then checks for a potential fall-back DST overlap. It does so by comparing the UTC offset of that time with the offset 24 hours later. If the offset 24 hours later is smaller (indicating that clocks were turned back, creating an overlap of local times), the code further pinpoints the exact transition moment by adding the offset difference to the NSDate. If it confirms that the local time repeats (the offset after adding the difference is still less than the original), it returns anOffsetInfo.Overlap
with the overlap’s start instant and the two different offsets before/after the overlap. If neither a gap nor overlap is detected, it returns a regular offset info (OffsetInfo.Regular
). This careful calculation ensures thatTimeZoneRulesFoundation.infoAtDatetime
can correctly distinguish normal times from “missing” hours and “repeated” hours, paralleling the behavior of tzdb. All exceptions and edge cases are handled. In summary, on Darwin the library now uses the system’s time zone database via Foundation for all conversions, meaning it will recognize all valid zone IDs and compute offsets correctly without requiring the tzdb files. Additionally, the mechanism for retrieving available time zone IDs (TimeZone.availableZoneIds
internally) was updated: on Darwin it falls back to usingNSTimeZone.knownTimeZoneNames
(through a new internalgetAvailableZoneIdsFoundation()
), ensuring the set of zone IDs is complete and up-to-date with the system’s zones.Testing and Verification
This PR includes an extensive test suite (
TimeZoneNativeTest
) to verify that the Foundation-based implementation behaves identically to the tzdb-based implementation across a wide range of scenarios. The tests create pairs ofTimeZone
objects for the same zone ID - one using the regular tzdb rules (timeZoneById
/TimeZoneRulesCommon
) and one using the Foundation rules (timeZoneByIdFoundation
/TimeZoneRulesFoundation
) - and compare their results. Key aspects tested are:America/New_York
the suite verifies the spring-forward transition in 2025 (clocks jumping from 1:59:59 to 3:00:00 on Mar 9, 2025) and the fall-back transition (clocks repeating 1:00 - 1:59 on Nov 2, 2025). It asserts that before a DST change, both implementations give the same offset, and after the change they still agree, and that the offset actually changes as expected (ensuring both detect the transition). Similar checks are done for other zones likeEurope/London
(which has a gap at 1 AM when DST starts, and an overlap in fall) andAustralia/Sydney
(opposite hemisphere DST dates). The tests confirm thatOffsetInfo.Gap
andOffsetInfo.Overlap
produced byTimeZoneRulesFoundation
exactly match those from the tzdb rules in each case (including the start instant of the gap/overlap and the offsets before/after).TimeZone.atZone(LocalDateTime)
(producing aZonedDateTime
) andTimeZone.atStartOfDay(LocalDate)
for Foundation vs tzdb implementations across many sample dates and zones. In every case, the resulting instants or zoned date-times are equal, showing that the overallTimeZone
behavior is unchanged from the user’s perspective.All these tests passed with the final implementation, confirming that
TimeZoneRulesFoundation
produces identical results to the standard tzdb-based approach. The few discrepancies initially found (for example, in handling overlaps like Venezuela’s offset change) were fixed during development - the overlap detection logic was adjusted and now aligns with tzdb’s data. The extensive coverage of edge cases gives confidence that the Foundation-based strategy is robust and accurate.Outcome
With this PR, Darwin (macOS/iOS) platforms now can use the native system time zone data for all date-time conversions, solving the tzdb access issues #485. The
TimeZoneRules
abstraction cleanly separates platform-specific implementations. On Apple devices, the library will seamlessly fall back to Foundation’sNSTimeZone
for zone offsets and transitions, while other platforms continue using the embedded tzdb as before - all behind the scenes. This change ensures that developers can rely on kotlinx-datetime on iOS/macOS without encountering missing zone IDs or crashes, and without any API changes. The solution is validated by comprehensive tests demonstrating that the new implementation’s behavior is indistinguishable from the previous one, even in tricky scenarios. In summary, this PR provides a robust fix for time zone handling on Darwin systems and maintains consistency of results across all platforms.