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