Skip to content

Commit b5ad3ef

Browse files
committed
Reimplement the behavior for when the js-joda tzdb is not available
1 parent 49dd802 commit b5ad3ef

File tree

12 files changed

+110
-54
lines changed

12 files changed

+110
-54
lines changed

core/androidNative/src/internal/TimeZoneNative.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,18 @@
77
package kotlinx.datetime.internal
88

99
import kotlinx.cinterop.*
10+
import kotlinx.datetime.TimeZone
1011
import platform.posix.*
1112

12-
internal actual val systemTzdb: TimeZoneDatabase get() = tzdb.getOrThrow()
13+
internal actual fun timeZoneById(zoneId: String): TimeZone =
14+
RegionTimeZone(tzdb.getOrThrow().rulesForId(zoneId), zoneId)
15+
16+
internal actual fun getAvailableZoneIds(): Set<String> =
17+
tzdb.getOrThrow().availableTimeZoneIds()
1318

1419
private val tzdb = runCatching { TzdbBionic() }
1520

16-
internal actual fun currentSystemDefaultZone(): Pair<String, TimeZoneRules?> = memScoped {
21+
internal actual fun currentSystemDefaultZone(): Pair<String, TimeZone?> = memScoped {
1722
val name = readSystemProperty("persist.sys.timezone")
1823
?: throw IllegalStateException("The system property 'persist.sys.timezone' should contain the system timezone")
1924
return name to null

core/commonJs/src/internal/Platform.kt

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,7 @@ import kotlinx.datetime.internal.JSJoda.ZoneId
1111
import kotlin.math.roundToInt
1212
import kotlin.math.roundToLong
1313

14-
private val tzdb: Result<TimeZoneDatabase> = runCatching { parseTzdb() }
15-
16-
internal actual val systemTzdb: TimeZoneDatabase get() = tzdb.getOrThrow()
17-
18-
private fun parseTzdb(): TimeZoneDatabase {
14+
private val tzdb: Result<TimeZoneDatabase?> = runCatching {
1915
/**
2016
* References:
2117
* - https://github.com/js-joda/js-joda/blob/8c1a7448db92ca014417346049fb64b55f7b1ac1/packages/timezone/src/MomentZoneRulesProvider.js#L78-L94
@@ -71,7 +67,7 @@ private fun parseTzdb(): TimeZoneDatabase {
7167
fun List<Long>.partialSums(): List<Long> = scanWithoutInitial(0, Long::plus)
7268

7369
val zones = mutableMapOf<String, TimeZoneRules>()
74-
val (zonesPacked, linksPacked) = readTzdb() ?: return EmptyTimeZoneDatabase
70+
val (zonesPacked, linksPacked) = readTzdb() ?: return@runCatching null
7571
for (zone in zonesPacked) {
7672
val components = zone.split('|')
7773
val offsets = components[2].split(' ').map { unpackBase60(it) }
@@ -92,32 +88,65 @@ private fun parseTzdb(): TimeZoneDatabase {
9288
zones[components[1]] = rules
9389
}
9490
}
95-
return object : TimeZoneDatabase {
91+
object : TimeZoneDatabase {
9692
override fun rulesForId(id: String): TimeZoneRules =
9793
zones[id] ?: throw IllegalTimeZoneException("Unknown time zone: $id")
9894

9995
override fun availableTimeZoneIds(): Set<String> = zones.keys
10096
}
10197
}
10298

103-
private object EmptyTimeZoneDatabase : TimeZoneDatabase {
104-
override fun rulesForId(id: String): TimeZoneRules = when (id) {
105-
"SYSTEM" -> TimeZoneRules(
106-
transitionEpochSeconds = emptyList(),
107-
offsets = listOf(UtcOffset.ZERO),
108-
recurringZoneRules = null
109-
) // TODO: that's not correct, we need to use `Date()`'s offset
110-
else -> throw IllegalTimeZoneException("JSJoda timezone database is not available")
99+
private object SystemTimeZone: TimeZone() {
100+
override val id: String get() = "SYSTEM"
101+
102+
/* https://github.com/js-joda/js-joda/blob/8c1a7448db92ca014417346049fb64b55f7b1ac1/packages/core/src/LocalDate.js#L1404-L1416 +
103+
* https://github.com/js-joda/js-joda/blob/8c1a7448db92ca014417346049fb64b55f7b1ac1/packages/core/src/zone/SystemDefaultZoneRules.js#L69-L71 */
104+
override fun atStartOfDay(date: LocalDate): Instant = atZone(date.atTime(LocalTime.MIN)).toInstant()
105+
106+
/* https://github.com/js-joda/js-joda/blob/8c1a7448db92ca014417346049fb64b55f7b1ac1/packages/core/src/zone/SystemDefaultZoneRules.js#L21-L24 */
107+
override fun offsetAtImpl(instant: Instant): UtcOffset =
108+
UtcOffset(minutes = -Date(instant.toEpochMilliseconds().toDouble()).getTimezoneOffset().toInt())
109+
110+
/* https://github.com/js-joda/js-joda/blob/8c1a7448db92ca014417346049fb64b55f7b1ac1/packages/core/src/zone/SystemDefaultZoneRules.js#L49-L55 */
111+
override fun atZone(dateTime: LocalDateTime, preferred: UtcOffset?): ZonedDateTime {
112+
val epochMilli = dateTime.toInstant(UTC).toEpochMilliseconds()
113+
val offsetInMinutesBeforePossibleTransition = Date(epochMilli.toDouble()).getTimezoneOffset().toInt()
114+
val epochMilliSystemZone = epochMilli +
115+
offsetInMinutesBeforePossibleTransition * SECONDS_PER_MINUTE * MILLIS_PER_ONE
116+
val offsetInMinutesAfterPossibleTransition = Date(epochMilliSystemZone.toDouble()).getTimezoneOffset().toInt()
117+
val offset = UtcOffset(minutes = -offsetInMinutesAfterPossibleTransition)
118+
return ZonedDateTime(dateTime, this, offset)
111119
}
112120

113-
override fun availableTimeZoneIds(): Set<String> = emptySet()
121+
override fun equals(other: Any?): Boolean = other === this
122+
123+
override fun hashCode(): Int = id.hashCode()
124+
}
125+
126+
internal actual fun currentSystemDefaultZone(): Pair<String, TimeZone?> {
127+
val id = ZoneId.systemDefault().id()
128+
return if (id == "SYSTEM") id to SystemTimeZone
129+
else id to null
130+
}
131+
132+
internal actual fun timeZoneById(zoneId: String): TimeZone {
133+
val id = if (zoneId == "SYSTEM") {
134+
val (name, zone) = currentSystemDefaultZone()
135+
if (zone != null) return zone
136+
name
137+
} else zoneId
138+
val rules = tzdb.getOrThrow()?.rulesForId(id)
139+
if (rules != null) return RegionTimeZone(rules, id)
140+
throw IllegalTimeZoneException("js-joda timezone database is not available")
114141
}
115142

116-
internal actual fun currentSystemDefaultZone(): Pair<String, TimeZoneRules?> =
117-
ZoneId.systemDefault().id() to null
143+
internal actual fun getAvailableZoneIds(): Set<String> =
144+
tzdb.getOrThrow()?.availableTimeZoneIds() ?: setOf("UTC")
118145

119146
internal actual fun currentTime(): Instant = Instant.fromEpochMilliseconds(Date().getTime().toLong())
120147

121148
internal external class Date() {
149+
constructor(milliseconds: Double)
122150
fun getTime(): Double
151+
fun getTimezoneOffset(): Double
123152
}

core/commonKotlin/src/TimeZone.kt

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ public actual open class TimeZone internal constructor() {
2020

2121
public actual fun currentSystemDefault(): TimeZone {
2222
// TODO: probably check if currentSystemDefault name is parseable as FixedOffsetTimeZone?
23-
val (name, rules) = currentSystemDefaultZone()
24-
return if (rules == null) {
23+
val (name, zone) = currentSystemDefaultZone()
24+
return if (zone == null) {
2525
of(name)
2626
} else {
27-
RegionTimeZone(rules, name)
27+
zone
2828
}
2929
}
3030

@@ -36,6 +36,9 @@ public actual open class TimeZone internal constructor() {
3636
if (zoneId == "Z") {
3737
return UTC
3838
}
39+
if (zoneId == "SYSTEM") {
40+
return currentSystemDefault()
41+
}
3942
if (zoneId.length == 1) {
4043
throw IllegalTimeZoneException("Invalid zone ID: $zoneId")
4144
}
@@ -67,14 +70,14 @@ public actual open class TimeZone internal constructor() {
6770
throw IllegalTimeZoneException(e)
6871
}
6972
return try {
70-
RegionTimeZone(systemTzdb.rulesForId(zoneId), zoneId)
73+
timeZoneById(zoneId)
7174
} catch (e: Exception) {
7275
throw IllegalTimeZoneException("Invalid zone ID: $zoneId", e)
7376
}
7477
}
7578

7679
public actual val availableZoneIds: Set<String>
77-
get() = systemTzdb.availableTimeZoneIds()
80+
get() = getAvailableZoneIds()
7881
}
7982

8083
public actual open val id: String

core/commonKotlin/src/internal/Platform.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@
66
package kotlinx.datetime.internal
77

88
import kotlinx.datetime.Instant
9+
import kotlinx.datetime.TimeZone
910

10-
internal expect val systemTzdb: TimeZoneDatabase
11+
// RegionTimeZone(systemTzdb.rulesForId(zoneId), zoneId)
12+
internal expect fun timeZoneById(zoneId: String): TimeZone
1113

12-
internal expect fun currentSystemDefaultZone(): Pair<String, TimeZoneRules?>
14+
internal expect fun getAvailableZoneIds(): Set<String>
1315

14-
internal expect fun currentTime(): Instant
16+
internal expect fun currentSystemDefaultZone(): Pair<String, TimeZone?>
17+
18+
internal expect fun currentTime(): Instant

core/darwin/src/internal/TimeZoneNative.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,21 @@
77
package kotlinx.datetime.internal
88

99
import kotlinx.cinterop.*
10+
import kotlinx.datetime.TimeZone
1011
import kotlinx.datetime.internal.*
1112
import platform.Foundation.*
1213

13-
internal actual val systemTzdb: TimeZoneDatabase get() = tzdb.getOrThrow()
14+
internal actual fun timeZoneById(zoneId: String): TimeZone =
15+
RegionTimeZone(tzdb.getOrThrow().rulesForId(zoneId), zoneId)
16+
17+
internal actual fun getAvailableZoneIds(): Set<String> =
18+
tzdb.getOrThrow().availableTimeZoneIds()
1419

1520
private val tzdb = runCatching { TzdbOnFilesystem(Path.fromString(defaultTzdbPath())) }
1621

1722
internal expect fun defaultTzdbPath(): String
1823

19-
internal actual fun currentSystemDefaultZone(): Pair<String, TimeZoneRules?> {
24+
internal actual fun currentSystemDefaultZone(): Pair<String, TimeZone?> {
2025
/* The framework has its own cache of the system timezone. Calls to
2126
[NSTimeZone systemTimeZone] do not reflect changes to the system timezone
2227
and instead just return the cached value. Thus, to acquire the current

core/linux/src/internal/TimeZoneNative.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,17 @@
66
package kotlinx.datetime.internal
77

88
import kotlinx.datetime.IllegalTimeZoneException
9+
import kotlinx.datetime.TimeZone
910

10-
internal actual val systemTzdb: TimeZoneDatabase get() = tzdb.getOrThrow()
11+
internal actual fun timeZoneById(zoneId: String): TimeZone =
12+
RegionTimeZone(tzdb.getOrThrow().rulesForId(zoneId), zoneId)
13+
14+
internal actual fun getAvailableZoneIds(): Set<String> =
15+
tzdb.getOrThrow().availableTimeZoneIds()
1116

1217
private val tzdb = runCatching { TzdbOnFilesystem() }
1318

14-
internal actual fun currentSystemDefaultZone(): Pair<String, TimeZoneRules?> {
19+
internal actual fun currentSystemDefaultZone(): Pair<String, TimeZone?> {
1520
// according to https://www.man7.org/linux/man-pages/man5/localtime.5.html, when there is no symlink, UTC is used
1621
val zonePath = currentSystemTimeZonePath ?: return "Z" to null
1722
val zoneId = zonePath.splitTimeZonePath()?.second?.toString()

core/wasmJs/src/JSJodaExceptions.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
44
*/
55

6-
package kotlinx.datetime
6+
package kotlinx.datetime.internal
77

88
private fun withCaughtJsException(body: () -> Unit): JsAny? = js("""{
99
try {

core/wasmJs/src/PlatformSpecifics.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ internal actual fun readTzdb(): Pair<List<String>, List<String>>? = try {
1515
jsTry {
1616
val zones = getZones(ZoneRulesProvider as JsAny)
1717
val links = getLinks(ZoneRulesProvider as JsAny)
18-
return zones.unsafeCast<JsArray<JsString>>().toList() to links.unsafeCast<JsArray<JsString>>().toList()
18+
zones.unsafeCast<JsArray<JsString>>().toList() to links.unsafeCast<JsArray<JsString>>().toList()
1919
}
2020
} catch (_: Throwable) {
21-
return null
21+
null
2222
}
2323

2424
private fun JsArray<JsString>.toList(): List<String> = buildList {

core/wasmWasi/src/internal/Platform.kt

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,3 @@ internal actual fun currentTime(): Instant = clockTimeGet().let { time ->
3737
// Instant.MAX and Instant.MIN are never going to be exceeded using just the Long number of nanoseconds
3838
Instant(time.floorDiv(NANOS_PER_ONE.toLong()), time.mod(NANOS_PER_ONE.toLong()).toInt())
3939
}
40-
41-
internal actual fun currentSystemDefaultZone(): Pair<String, TimeZoneRules?> =
42-
"UTC" to null
43-
44-
internal actual val systemTzdb: TimeZoneDatabase = TzdbOnData()

core/wasmWasi/src/internal/TimeZonesInitializer.kt

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package kotlinx.datetime.internal
77

88
import kotlinx.datetime.IllegalTimeZoneException
9+
import kotlinx.datetime.TimeZone
910

1011
@RequiresOptIn
1112
internal annotation class InternalDateTimeApi
@@ -32,13 +33,15 @@ public fun initializeTimeZonesProvider(provider: TimeZonesProvider) {
3233
private var timeZonesProvider: TimeZonesProvider? = null
3334

3435
@OptIn(InternalDateTimeApi::class)
35-
internal class TzdbOnData: TimeZoneDatabase {
36-
override fun rulesForId(id: String): TimeZoneRules {
37-
val data = timeZonesProvider?.zoneDataByName(id)
38-
?: throw IllegalTimeZoneException("TimeZones are not supported")
39-
return readTzFile(data).toTimeZoneRules()
40-
}
41-
42-
override fun availableTimeZoneIds(): Set<String> =
43-
timeZonesProvider?.getTimeZones() ?: setOf("UTC")
44-
}
36+
internal actual fun timeZoneById(zoneId: String): TimeZone {
37+
val data = timeZonesProvider?.zoneDataByName(zoneId)
38+
?: throw IllegalTimeZoneException("TimeZones are not supported")
39+
val rules = readTzFile(data).toTimeZoneRules()
40+
return RegionTimeZone(rules, zoneId)
41+
}
42+
43+
@OptIn(InternalDateTimeApi::class)
44+
internal actual fun getAvailableZoneIds(): Set<String> =
45+
timeZonesProvider?.getTimeZones() ?: setOf("UTC")
46+
47+
internal actual fun currentSystemDefaultZone(): Pair<String, TimeZone?> = "UTC" to null

0 commit comments

Comments
 (0)