Skip to content

Commit 352ef6d

Browse files
committed
WIP
1 parent e7c1318 commit 352ef6d

File tree

5 files changed

+77
-160
lines changed

5 files changed

+77
-160
lines changed

core/darwin/src/TimeZoneNative.kt

Lines changed: 4 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -7,164 +7,10 @@
77

88
package kotlinx.datetime
99

10+
import kotlinx.datetime.internal.Path
11+
import kotlinx.datetime.internal.TzdbOnFilesystem
1012
import platform.Foundation.*
1113

12-
private fun dateWithTimeIntervalSince1970Saturating(epochSeconds: Long): NSDate {
13-
val date = NSDate.dateWithTimeIntervalSince1970(epochSeconds.toDouble())
14-
return when {
15-
date.timeIntervalSinceDate(NSDate.distantPast) < 0 -> NSDate.distantPast
16-
date.timeIntervalSinceDate(NSDate.distantFuture) > 0 -> NSDate.distantFuture
17-
else -> date
18-
}
19-
}
20-
21-
private fun systemDateByLocalDate(zone: NSTimeZone, localDate: NSDate): NSDate? {
22-
val iso8601 = NSCalendar.calendarWithIdentifier(NSCalendarIdentifierISO8601)!!
23-
val utc = NSTimeZone.timeZoneForSecondsFromGMT(0)
24-
/* Now, we say that the date that we initially meant is `date`, only with
25-
the context of being in a timezone `zone`. */
26-
val dateComponents = iso8601.componentsInTimeZone(utc, localDate)
27-
dateComponents.timeZone = zone
28-
return iso8601.dateFromComponents(dateComponents)
29-
}
30-
31-
internal actual class RegionTimeZone(private val value: NSTimeZone, actual override val id: String): TimeZone() {
32-
actual companion object {
33-
actual fun of(zoneId: String): RegionTimeZone {
34-
val abbreviations = NSTimeZone.abbreviationDictionary
35-
val trueZoneId = abbreviations[zoneId] as String? ?: zoneId
36-
val zone = NSTimeZone.timeZoneWithName(trueZoneId)
37-
?: throw IllegalTimeZoneException("No timezone found with zone ID '$zoneId'")
38-
return RegionTimeZone(zone, zoneId)
39-
}
40-
41-
actual fun currentSystemDefault(): RegionTimeZone {
42-
/* The framework has its own cache of the system timezone. Calls to
43-
[NSTimeZone systemTimeZone] do not reflect changes to the system timezone
44-
and instead just return the cached value. Thus, to acquire the current
45-
system timezone, first, the cache should be cleared.
46-
47-
This solution is not without flaws, however. In particular, resetting the
48-
system timezone also resets the default timezone ([NSTimeZone default]) if
49-
it's the same as the cached system timezone:
50-
51-
NSTimeZone.defaultTimeZone = [NSTimeZone
52-
timeZoneWithName: [[NSTimeZone systemTimeZone] name]];
53-
NSLog(@"%@", NSTimeZone.defaultTimeZone.name);
54-
NSLog(@"Change the system time zone, then press Enter");
55-
getchar();
56-
[NSTimeZone resetSystemTimeZone];
57-
NSLog(@"%@", NSTimeZone.defaultTimeZone.name); // will also change
58-
59-
This is a fairly marginal problem:
60-
* It is only a problem when the developer deliberately sets the default
61-
timezone to the region that just happens to be the one that the user
62-
is in, and then the user moves to another region, and the app also
63-
uses the system timezone.
64-
* Since iOS 11, the significance of the default timezone has been
65-
de-emphasized. In particular, it is not included in the API for
66-
Swift: https://forums.swift.org/t/autoupdating-type-properties/4608/4
67-
68-
Another possible solution could involve using [NSTimeZone localTimeZone].
69-
This is documented to reflect the current, uncached system timezone on
70-
iOS 11 and later:
71-
https://developer.apple.com/documentation/foundation/nstimezone/1387209-localtimezone
72-
However:
73-
* Before iOS 11, this was the same as the default timezone and did not
74-
reflect the system timezone.
75-
* Worse, on a Mac (10.15.5), I failed to get it to work as documented.
76-
NSLog(@"%@", NSTimeZone.localTimeZone.name);
77-
NSLog(@"Change the system time zone, then press Enter");
78-
getchar();
79-
// [NSTimeZone resetSystemTimeZone]; // uncomment to make it work
80-
NSLog(@"%@", NSTimeZone.localTimeZone.name);
81-
The printed strings are the same even if I wait for good 10 minutes
82-
before pressing Enter, unless the line with "reset" is uncommented--
83-
then the timezone is updated, as it should be. So, for some reason,
84-
NSTimeZone.localTimeZone, too, is cached.
85-
With no iOS device to test this on, it doesn't seem worth the effort
86-
to avoid just resetting the system timezone due to one edge case
87-
that's hard to avoid.
88-
*/
89-
NSTimeZone.resetSystemTimeZone()
90-
val zone = NSTimeZone.systemTimeZone
91-
return RegionTimeZone(zone, zone.name)
92-
}
93-
94-
actual val availableZoneIds: Set<String>
95-
get() {
96-
val set = mutableSetOf("UTC")
97-
val zones = NSTimeZone.knownTimeZoneNames
98-
for (zone in zones) {
99-
if (zone is NSString) {
100-
set.add(zone as String)
101-
} else throw RuntimeException("$zone is expected to be NSString")
102-
}
103-
val abbrevs = NSTimeZone.abbreviationDictionary
104-
for ((key, value) in abbrevs) {
105-
if (key is NSString && value is NSString) {
106-
if (set.contains(value as String)) {
107-
set.add(key as String)
108-
}
109-
} else throw RuntimeException("$key and $value are expected to be NSString")
110-
}
111-
return set
112-
}
113-
}
114-
115-
actual override fun atStartOfDay(date: LocalDate): Instant {
116-
val ldt = LocalDateTime(date, LocalTime.MIN)
117-
val epochSeconds = ldt.toEpochSecond(UtcOffset.ZERO)
118-
// timezone
119-
val nsDate = NSDate.dateWithTimeIntervalSince1970(epochSeconds.toDouble())
120-
val newDate = systemDateByLocalDate(value, nsDate)
121-
?: throw RuntimeException("Unable to acquire the time of start of day at $nsDate for zone $this")
122-
val offset = value.secondsFromGMTForDate(newDate).toInt()
123-
/* if `epoch_sec` is not in the range supported by Darwin, assume that it
124-
is the correct local time for the midnight and just convert it to
125-
the system time. */
126-
if (nsDate.timeIntervalSinceDate(NSDate.distantPast) < 0 ||
127-
nsDate.timeIntervalSinceDate(NSDate.distantFuture) > 0)
128-
return Instant(epochSeconds - offset, 0)
129-
// The ISO-8601 calendar.
130-
val iso8601 = NSCalendar.calendarWithIdentifier(NSCalendarIdentifierISO8601)!!
131-
iso8601.timeZone = value
132-
// start of the day denoted by `newDate`
133-
val midnight = iso8601.startOfDayForDate(newDate)
134-
return Instant(midnight.timeIntervalSince1970.toLong(), 0)
135-
}
136-
137-
actual override fun atZone(dateTime: LocalDateTime, preferred: UtcOffset?): ZonedDateTime {
138-
val epochSeconds = dateTime.toEpochSecond(UtcOffset.ZERO)
139-
var offset = preferred?.totalSeconds ?: Int.MAX_VALUE
140-
val transitionDuration = run {
141-
/* a date in an unspecified timezone, defined by the number of seconds since
142-
the start of the epoch in *that* unspecified timezone */
143-
val date = dateWithTimeIntervalSince1970Saturating(epochSeconds)
144-
val newDate = systemDateByLocalDate(value, date)
145-
?: throw RuntimeException("Unable to acquire the offset at $dateTime for zone ${this@RegionTimeZone}")
146-
// we now know the offset of that timezone at this time.
147-
offset = value.secondsFromGMTForDate(newDate).toInt()
148-
/* `dateFromComponents` automatically corrects the date to avoid gaps. We
149-
need to learn which adjustments it performed. */
150-
(newDate.timeIntervalSince1970.toLong() +
151-
offset.toLong() - date.timeIntervalSince1970.toLong()).toInt()
152-
}
153-
val correctedDateTime = try {
154-
dateTime.plusSeconds(transitionDuration)
155-
} catch (e: IllegalArgumentException) {
156-
throw DateTimeArithmeticException("Overflow whet correcting the date-time to not be in the transition gap", e)
157-
} catch (e: ArithmeticException) {
158-
throw RuntimeException("Anomalously long timezone transition gap reported", e)
159-
}
160-
return ZonedDateTime(correctedDateTime, this@RegionTimeZone, UtcOffset.ofSeconds(offset))
161-
}
162-
163-
actual override fun offsetAtImpl(instant: Instant): UtcOffset {
164-
val date = dateWithTimeIntervalSince1970Saturating(instant.epochSeconds)
165-
return UtcOffset.ofSeconds(value.secondsFromGMTForDate(date).toInt())
166-
}
167-
168-
}
169-
17014
internal actual fun currentTime(): Instant = NSDate.date().toKotlinInstant()
15+
16+
internal actual val tzdbOnFilesystem = TzdbOnFilesystem(Path.fromString("/var/db/timezone/zoneinfo"))

core/linux/src/TimeZoneNative.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,4 @@ internal actual fun currentTime(): Instant = memScoped {
7575
}
7676
}
7777

78-
private val tzdbOnFilesystem = TzdbOnFilesystem(Path.fromString("/usr/share/zoneinfo"))
78+
internal actual val tzdbOnFilesystem = TzdbOnFilesystem(Path.fromString("/usr/share/zoneinfo"))

core/nix/src/TimeZoneNative.kt

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright 2019-2023 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
7+
8+
import kotlinx.cinterop.memScoped
9+
import kotlinx.datetime.internal.OffsetInfo
10+
import kotlinx.datetime.internal.TimeZoneRules
11+
import kotlinx.datetime.internal.TzdbOnFilesystem
12+
13+
internal expect val tzdbOnFilesystem: TzdbOnFilesystem
14+
15+
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
16+
internal actual class RegionTimeZone(private val tzid: TimeZoneRules, actual override val id: String) : TimeZone() {
17+
actual companion object {
18+
actual fun of(zoneId: String): RegionTimeZone = try {
19+
RegionTimeZone(tzdbOnFilesystem.rulesForId(zoneId), zoneId)
20+
} catch (e: Exception) {
21+
throw IllegalTimeZoneException("Invalid zone ID: $zoneId", e)
22+
}
23+
24+
actual fun currentSystemDefault(): RegionTimeZone {
25+
val zoneId = tzdbOnFilesystem.currentSystemDefault()?.second
26+
?: throw IllegalStateException("Failed to get the system timezone")
27+
return of(zoneId.toString())
28+
}
29+
30+
actual val availableZoneIds: Set<String>
31+
get() = tzdbOnFilesystem.availableTimeZoneIds()
32+
}
33+
34+
actual override fun atStartOfDay(date: LocalDate): Instant = memScoped {
35+
val ldt = LocalDateTime(date, LocalTime.MIN)
36+
when (val info = tzid.infoAtDatetime(ldt)) {
37+
is OffsetInfo.Regular -> ldt.toInstant(info.offset)
38+
is OffsetInfo.Gap -> info.start
39+
is OffsetInfo.Overlap -> ldt.toInstant(info.offsetBefore)
40+
}
41+
}
42+
43+
actual override fun atZone(dateTime: LocalDateTime, preferred: UtcOffset?): ZonedDateTime =
44+
when (val info = tzid.infoAtDatetime(dateTime)) {
45+
is OffsetInfo.Regular -> ZonedDateTime(dateTime, this, info.offset)
46+
is OffsetInfo.Gap -> {
47+
try {
48+
ZonedDateTime(dateTime.plusSeconds(info.transitionDurationSeconds), this, info.offsetAfter)
49+
} catch (e: IllegalArgumentException) {
50+
throw DateTimeArithmeticException(
51+
"Overflow whet correcting the date-time to not be in the transition gap",
52+
e
53+
)
54+
}
55+
}
56+
57+
is OffsetInfo.Overlap -> ZonedDateTime(dateTime, this,
58+
if (info.offsetAfter == preferred) info.offsetAfter else info.offsetBefore)
59+
}
60+
61+
actual override fun offsetAtImpl(instant: Instant): UtcOffset = tzid.infoAtInstant(instant)
62+
}

core/nix/src/internal/TzdbOnFilesystem.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55

66
package kotlinx.datetime.internal
77

8+
import kotlinx.datetime.*
9+
import kotlinx.datetime.ZonedDateTime
10+
import kotlinx.datetime.plusSeconds
11+
812
internal class TzdbOnFilesystem(defaultTzdbPath: Path) {
913

1014
internal fun rulesForId(id: String): TimeZoneRules =
@@ -41,4 +45,4 @@ private val tzdbUnneededFiles = setOf(
4145
"tzdata.zi",
4246
"leapseconds",
4347
"leap-seconds.list"
44-
)
48+
)

settings.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@ pluginManagement {
77
val dokkaVersion: String by settings
88
plugins {
99
id("org.jetbrains.dokka") version dokkaVersion
10+
1011
}
1112
}
1213

14+
plugins {
15+
id("org.gradle.toolchains.foojay-resolver-convention") version("0.4.0")
16+
}
17+
1318
rootProject.name = "Kotlin-DateTime-library"
1419

1520
include(":core")

0 commit comments

Comments
 (0)