1+ /*
2+ * Copyright 2019-2025 JetBrains s.r.o. and contributors.
3+ * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+ */
5+
6+ package kotlinx.datetime.internal
7+
8+ import kotlinx.cinterop.ExperimentalForeignApi
9+ import kotlinx.cinterop.UnsafeNumber
10+ import kotlinx.cinterop.convert
11+ import kotlinx.datetime.LocalDateTime
12+ import kotlinx.datetime.UtcOffset
13+ import kotlinx.datetime.toKotlinInstant
14+ import kotlinx.datetime.toLocalDateTime
15+ import kotlinx.datetime.toNSDate
16+ import kotlinx.datetime.toNSDateComponents
17+ import platform.Foundation.NSCalendar
18+ import platform.Foundation.NSCalendarIdentifierISO8601
19+ import platform.Foundation.NSCalendarUnitYear
20+ import platform.Foundation.NSDate
21+ import platform.Foundation.NSTimeZone
22+ import platform.Foundation.timeZoneWithName
23+ import kotlin.time.Instant
24+
25+ internal class TimeZoneRulesFoundation (private val zoneId : String ) : TimeZoneRules {
26+ private val nsTimeZone: NSTimeZone = NSTimeZone .timeZoneWithName(zoneId)
27+ ? : throw IllegalArgumentException (" Unknown timezone: $zoneId " )
28+
29+ override fun infoAtInstant (instant : Instant ): UtcOffset =
30+ infoAtNsDate(instant.toNSDate())
31+
32+ /* *
33+ * IMPORTANT: mirrors the logic in [RecurringZoneRules.infoAtLocalDateTime].
34+ * Any update to offset calculations, transition handling, or edge cases
35+ * must be duplicated there to maintain consistent behavior across
36+ * all platforms.
37+ */
38+ @OptIn(UnsafeNumber ::class , ExperimentalForeignApi ::class )
39+ override fun infoAtDatetime (localDateTime : LocalDateTime ): OffsetInfo {
40+ val calendar = NSCalendar .calendarWithIdentifier(NSCalendarIdentifierISO8601 )
41+ ?.apply { timeZone = nsTimeZone }
42+
43+ val year = localDateTime.year
44+ val startOfTheYear = calendar?.dateFromComponents(LocalDateTime (year, 1 , 1 , 0 , 0 ).toNSDateComponents())
45+ check(startOfTheYear != null ) { " Failed to get the start of the year for $localDateTime , timezone: $zoneId " }
46+
47+ var currentDate: NSDate = startOfTheYear
48+ var offset = infoAtNsDate(startOfTheYear)
49+ do {
50+ val transitionDateTime = nsTimeZone.nextDaylightSavingTimeTransitionAfterDate(currentDate)
51+ if (transitionDateTime == null ) break
52+
53+ val yearOfNextDate = calendar.component(NSCalendarUnitYear .convert(), fromDate = transitionDateTime)
54+ val transitionDateTimeInstant = transitionDateTime.toKotlinInstant()
55+
56+ val offsetBefore = infoAtNsDate(currentDate)
57+ val ldtBefore = transitionDateTimeInstant.toLocalDateTime(offsetBefore)
58+ val offsetAfter = infoAtNsDate(transitionDateTime)
59+ val ldtAfter = transitionDateTimeInstant.toLocalDateTime(offsetAfter)
60+
61+ return if (localDateTime < ldtBefore && localDateTime < ldtAfter) {
62+ OffsetInfo .Regular (offsetBefore)
63+ } else if (localDateTime >= ldtBefore && localDateTime >= ldtAfter) {
64+ offset = offsetAfter
65+ currentDate = transitionDateTime
66+ continue
67+ } else if (ldtAfter < ldtBefore) {
68+ OffsetInfo .Overlap (transitionDateTimeInstant, offsetBefore, offsetAfter)
69+ } else {
70+ OffsetInfo .Gap (transitionDateTimeInstant, offsetBefore, offsetAfter)
71+ }
72+ } while (yearOfNextDate <= year)
73+
74+ return OffsetInfo .Regular (offset)
75+ }
76+
77+ @OptIn(UnsafeNumber ::class , ExperimentalForeignApi ::class )
78+ private fun infoAtNsDate (nsDate : NSDate ): UtcOffset {
79+ val offsetSeconds = nsTimeZone.secondsFromGMTForDate(nsDate)
80+ return UtcOffset (seconds = offsetSeconds.convert())
81+ }
82+ }
0 commit comments