Skip to content

Commit dbcb1b5

Browse files
committed
Use the filesystem tzdb for Darwin
1 parent dc494fe commit dbcb1b5

File tree

10 files changed

+106
-23
lines changed

10 files changed

+106
-23
lines changed

core/build.gradle.kts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,12 @@ kotlin {
156156
// do nothing special
157157
}
158158
konanTarget.family.isAppleFamily -> {
159-
// do nothing special
159+
compilations["main"].cinterops {
160+
create("declarations") {
161+
defFile("$projectDir/darwin/cinterop/definitions.def")
162+
headers("$projectDir/darwin/cinterop/definitions.h")
163+
}
164+
}
160165
}
161166
else -> {
162167
throw IllegalArgumentException("Unknown native target ${this@withType}")

core/common/test/TimeZoneTest.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,24 @@ class TimeZoneTest {
4646

4747
@Test
4848
fun availableZonesAreAvailable() {
49+
val availableZones = mutableListOf<String>()
50+
val nonAvailableZones = mutableListOf<Pair<String, Exception>>()
4951
for (zoneName in TimeZone.availableZoneIds) {
5052
val timezone = try {
5153
TimeZone.of(zoneName)
5254
} catch (e: Exception) {
53-
throw Exception("Zone $zoneName is not available", e)
55+
nonAvailableZones.add(zoneName to e)
56+
continue
5457
}
58+
availableZones.add(zoneName)
5559
Instant.DISTANT_FUTURE.toLocalDateTime(timezone).toInstant(timezone)
5660
Instant.DISTANT_PAST.toLocalDateTime(timezone).toInstant(timezone)
5761
}
62+
if (nonAvailableZones.isNotEmpty()) {
63+
println("Available zones: $availableZones")
64+
println("Non-available zones: $nonAvailableZones")
65+
throw nonAvailableZones[0].second
66+
}
5867
}
5968

6069
@Test

core/darwin/cinterop/definitions.def

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package = kotlinx.datetime.internal

core/darwin/cinterop/definitions.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#include <stdbool.h>
2+
3+
const bool kotlinxDatetimeRunningInSimulator =
4+
#if TARGET_OS_SIMULATOR
5+
true
6+
#else
7+
false
8+
#endif
9+
;

core/darwin/src/Converters.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public fun NSDate.toKotlinInstant(): Instant {
4242
*
4343
* If the time zone is represented as a fixed number of seconds from UTC+0 (for example, if it is the result of a call
4444
* to [TimeZone.offset]) and the offset is not given in even minutes but also includes seconds, this method throws
45-
* [DateTimeException] to denote that lossy conversion would happen, as Darwin internally rounds the offsets to the
45+
* [IllegalArgumentException] to denote that lossy conversion would happen, as Darwin internally rounds the offsets to the
4646
* nearest minute.
4747
*/
4848
public fun TimeZone.toNSTimeZone(): NSTimeZone = if (this is FixedOffsetTimeZone) {
@@ -51,7 +51,8 @@ public fun TimeZone.toNSTimeZone(): NSTimeZone = if (this is FixedOffsetTimeZone
5151
}
5252
NSTimeZone.timeZoneForSecondsFromGMT(offset.totalSeconds.convert())
5353
} else {
54-
NSTimeZone.timeZoneWithName(id) ?: NSTimeZone.timeZoneWithAbbreviation(id)!!
54+
NSTimeZone.timeZoneWithName(id) ?: NSTimeZone.timeZoneWithAbbreviation(id)
55+
?: throw IllegalArgumentException("The Foundation framework does not support the timezone '$id'")
5556
}
5657

5758
/**

core/darwin/src/TimeZoneNative.kt

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,65 @@
77

88
package kotlinx.datetime
99

10-
import kotlinx.datetime.internal.Path
11-
import kotlinx.datetime.internal.TzdbOnFilesystem
10+
import kotlinx.datetime.internal.*
1211
import platform.Foundation.*
1312

1413
internal actual fun currentTime(): Instant = NSDate.date().toKotlinInstant()
1514

16-
internal actual val tzdbOnFilesystem = TzdbOnFilesystem(Path.fromString("/var/db/timezone/zoneinfo"))
15+
// iOS simulator needs a different path, hence the check. See https://github.com/HowardHinnant/date/pull/577
16+
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
17+
internal actual val tzdbOnFilesystem = TzdbOnFilesystem(Path.fromString(
18+
if (kotlinxDatetimeRunningInSimulator) "/usr/share/zoneinfo" else "/var/db/timezone/zoneinfo"))
19+
20+
internal actual fun currentSystemDefaultId(): String? {
21+
/* The framework has its own cache of the system timezone. Calls to
22+
[NSTimeZone systemTimeZone] do not reflect changes to the system timezone
23+
and instead just return the cached value. Thus, to acquire the current
24+
system timezone, first, the cache should be cleared.
25+
26+
This solution is not without flaws, however. In particular, resetting the
27+
system timezone also resets the default timezone ([NSTimeZone default]) if
28+
it's the same as the cached system timezone:
29+
30+
NSTimeZone.defaultTimeZone = [NSTimeZone
31+
timeZoneWithName: [[NSTimeZone systemTimeZone] name]];
32+
NSLog(@"%@", NSTimeZone.defaultTimeZone.name);
33+
NSLog(@"Change the system time zone, then press Enter");
34+
getchar();
35+
[NSTimeZone resetSystemTimeZone];
36+
NSLog(@"%@", NSTimeZone.defaultTimeZone.name); // will also change
37+
38+
This is a fairly marginal problem:
39+
* It is only a problem when the developer deliberately sets the default
40+
timezone to the region that just happens to be the one that the user
41+
is in, and then the user moves to another region, and the app also
42+
uses the system timezone.
43+
* Since iOS 11, the significance of the default timezone has been
44+
de-emphasized. In particular, it is not included in the API for
45+
Swift: https://forums.swift.org/t/autoupdating-type-properties/4608/4
46+
47+
Another possible solution could involve using [NSTimeZone localTimeZone].
48+
This is documented to reflect the current, uncached system timezone on
49+
iOS 11 and later:
50+
https://developer.apple.com/documentation/foundation/nstimezone/1387209-localtimezone
51+
However:
52+
* Before iOS 11, this was the same as the default timezone and did not
53+
reflect the system timezone.
54+
* Worse, on a Mac (10.15.5), I failed to get it to work as documented.
55+
NSLog(@"%@", NSTimeZone.localTimeZone.name);
56+
NSLog(@"Change the system time zone, then press Enter");
57+
getchar();
58+
// [NSTimeZone resetSystemTimeZone]; // uncomment to make it work
59+
NSLog(@"%@", NSTimeZone.localTimeZone.name);
60+
The printed strings are the same even if I wait for good 10 minutes
61+
before pressing Enter, unless the line with "reset" is uncommented--
62+
then the timezone is updated, as it should be. So, for some reason,
63+
NSTimeZone.localTimeZone, too, is cached.
64+
With no iOS device to test this on, it doesn't seem worth the effort
65+
to avoid just resetting the system timezone due to one edge case
66+
that's hard to avoid.
67+
*/
68+
NSTimeZone.resetSystemTimeZone()
69+
val zone = NSTimeZone.systemTimeZone
70+
return zone.name
71+
}

core/linux/src/TimeZoneNative.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,7 @@ internal actual fun currentTime(): Instant = memScoped {
7575
}
7676
}
7777

78+
internal actual fun currentSystemDefaultId(): String? =
79+
pathToSystemDefault()?.second?.toString()
80+
7881
internal actual val tzdbOnFilesystem = TzdbOnFilesystem(Path.fromString("/usr/share/zoneinfo"))

core/nix/src/TimeZoneNative.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@
77
package kotlinx.datetime
88

99
import kotlinx.cinterop.memScoped
10-
import kotlinx.datetime.internal.OffsetInfo
11-
import kotlinx.datetime.internal.TimeZoneRules
12-
import kotlinx.datetime.internal.TzdbOnFilesystem
10+
import kotlinx.datetime.internal.*
1311

1412
internal expect val tzdbOnFilesystem: TzdbOnFilesystem
1513

@@ -23,9 +21,9 @@ internal actual class RegionTimeZone(private val tzid: TimeZoneRules, actual ove
2321
}
2422

2523
actual fun currentSystemDefault(): RegionTimeZone {
26-
val zoneId = tzdbOnFilesystem.currentSystemDefault()?.second
24+
val zoneId = currentSystemDefaultId()
2725
?: throw IllegalStateException("Failed to get the system timezone")
28-
return of(zoneId.toString())
26+
return of(zoneId)
2927
}
3028

3129
actual val availableZoneIds: Set<String>
@@ -61,3 +59,5 @@ internal actual class RegionTimeZone(private val tzid: TimeZoneRules, actual ove
6159

6260
actual override fun offsetAtImpl(instant: Instant): UtcOffset = tzid.infoAtInstant(instant)
6361
}
62+
63+
internal expect fun currentSystemDefaultId(): String?

core/nix/src/internal/TzdbOnFilesystem.kt

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,21 @@ internal class TzdbOnFilesystem(defaultTzdbPath: Path) {
1414
tzdbPath.traverseDirectory(exclude = tzdbUnneededFiles) { add(it.toString()) }
1515
}
1616

17-
internal fun currentSystemDefault(): Pair<Path, Path>? {
18-
val info = Path(true, listOf("etc", "localtime")).readLink() ?: return null
19-
val i = info.components.indexOf("zoneinfo")
20-
if (!info.isAbsolute || i == -1 || i == info.components.size - 1) return null
21-
return Pair(
22-
Path(true, info.components.subList(0, i + 1)),
23-
Path(false, info.components.subList(i + 1, info.components.size))
24-
)
25-
}
26-
2717
private val tzdbPath = defaultTzdbPath.check()?.let { defaultTzdbPath }
28-
?: currentSystemDefault()?.first ?: throw IllegalStateException("Could not find the path to the timezone database")
18+
?: pathToSystemDefault()?.first ?: throw IllegalStateException("Could not find the path to the timezone database")
2919

3020
}
3121

22+
internal fun pathToSystemDefault(): Pair<Path, Path>? {
23+
val info = Path(true, listOf("etc", "localtime")).readLink() ?: return null
24+
val i = info.components.indexOf("zoneinfo")
25+
if (!info.isAbsolute || i == -1 || i == info.components.size - 1) return null
26+
return Pair(
27+
Path(true, info.components.subList(0, i + 1)),
28+
Path(false, info.components.subList(i + 1, info.components.size))
29+
)
30+
}
31+
3232
private val tzdbUnneededFiles = setOf(
3333
"posix",
3434
"posixrules",

0 commit comments

Comments
 (0)