Skip to content

Commit 17bfddc

Browse files
committed
Test the js-joda tzdb extraction
1 parent 34ab701 commit 17bfddc

File tree

3 files changed

+105
-5
lines changed

3 files changed

+105
-5
lines changed

core/commonJs/src/internal/Platform.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ private val tzdb: Result<TimeZoneDatabase?> = runCatching {
3636
when (char) {
3737
in '0'..'9' -> char - '0'
3838
in 'a'..'z' -> char - 'a' + 10
39-
in 'A'..'Z' -> char - 'A' + 36
39+
in 'A'..'X' -> char - 'A' + 36
4040
else -> throw IllegalArgumentException("Invalid character: $char")
4141
}
4242

@@ -76,9 +76,10 @@ private val tzdb: Result<TimeZoneDatabase?> = runCatching {
7676
(unpackBase60(it) * SECONDS_PER_MINUTE * MILLIS_PER_ONE).roundToLong() / // minutes to milliseconds
7777
MILLIS_PER_ONE // but we only need seconds
7878
}
79+
println("Zone ${components[0]}: $offsets with ${components[2]} raw data")
7980
zones[components[0]] = TimeZoneRules(
8081
transitionEpochSeconds = lengthsOfPeriodsWithOffsets.partialSums().take<Long>(indices.size - 1),
81-
offsets = indices.map { UtcOffset(null, -offsets[it].roundToInt(), null) },
82+
offsets = indices.map { UtcOffset(null, null, -(offsets[it] * 60).roundToInt()) },
8283
recurringZoneRules = null
8384
)
8485
}
@@ -132,14 +133,15 @@ internal actual fun currentSystemDefaultZone(): Pair<String, TimeZone?> {
132133
internal actual fun timeZoneById(zoneId: String): TimeZone {
133134
val id = if (zoneId == "SYSTEM") {
134135
val (name, zone) = currentSystemDefaultZone()
135-
if (zone != null) return zone
136+
zone?.let { return it }
136137
name
137138
} else zoneId
138-
val rules = tzdb.getOrThrow()?.rulesForId(id)
139-
if (rules != null) return RegionTimeZone(rules, id)
139+
rulesForId(id)?.let { return RegionTimeZone(it, id) }
140140
throw IllegalTimeZoneException("js-joda timezone database is not available")
141141
}
142142

143+
internal fun rulesForId(zoneId: String): TimeZoneRules? = tzdb.getOrThrow()?.rulesForId(zoneId)
144+
143145
internal actual fun getAvailableZoneIds(): Set<String> =
144146
tzdb.getOrThrow()?.availableTimeZoneIds() ?: setOf("UTC")
145147

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2019-2024 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+
@file:kotlinx.datetime.internal.JsModule("@js-joda/core")
6+
@file:kotlinx.datetime.internal.JsNonModule
7+
package kotlinx.datetime.test.JSJoda
8+
9+
import kotlinx.datetime.internal.InteropInterface
10+
11+
external class ZonedDateTime : InteropInterface {
12+
fun year(): Int
13+
fun monthValue(): Int
14+
fun dayOfMonth(): Int
15+
fun hour(): Int
16+
fun minute(): Int
17+
fun second(): Int
18+
fun nano(): Double
19+
}
20+
21+
external class Instant : InteropInterface {
22+
fun atZone(zone: ZoneId): ZonedDateTime
23+
companion object {
24+
fun ofEpochMilli(epochMilli: Double): Instant
25+
}
26+
}
27+
28+
external class ZoneId : InteropInterface {
29+
companion object {
30+
fun of(zoneId: String): ZoneId
31+
}
32+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2019-2024 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+
package kotlinx.datetime.test
6+
7+
import kotlinx.datetime.*
8+
import kotlinx.datetime.internal.rulesForId
9+
import kotlin.math.roundToInt
10+
import kotlin.test.*
11+
import kotlin.time.Duration.Companion.milliseconds
12+
import kotlin.time.Duration.Companion.seconds
13+
14+
class JsJodaTimezoneTest {
15+
@Test
16+
fun system() {
17+
val tz = TimeZone.currentSystemDefault()
18+
assertNotEquals("SYSTEM", tz.id)
19+
val systemTz = TimeZone.of("SYSTEM")
20+
assertEquals(tz, systemTz)
21+
assertEquals(tz.id, systemTz.id)
22+
}
23+
24+
@Test
25+
fun iterateOverAllTimezones() {
26+
for (id in TimeZone.availableZoneIds) {
27+
val rules = rulesForId(id) ?: throw AssertionError("No rules for $id")
28+
val jodaZone = kotlinx.datetime.test.JSJoda.ZoneId.of(id)
29+
assertNull(rules.recurringZoneRules)
30+
fun checkAtInstant(instant: Instant) {
31+
val jodaInstant = kotlinx.datetime.test.JSJoda.Instant.ofEpochMilli(instant.toEpochMilliseconds().toDouble())
32+
val zdt = jodaInstant.atZone(jodaZone)
33+
val offset = rules.infoAtInstant(instant)
34+
val ourLdt = instant.toLocalDateTime(offset)
35+
val theirLdt = LocalDateTime(
36+
zdt.year(),
37+
zdt.monthValue(),
38+
zdt.dayOfMonth(),
39+
zdt.hour(),
40+
zdt.minute(),
41+
zdt.second(),
42+
zdt.nano().roundToInt()
43+
)
44+
if ((ourLdt.toInstant(TimeZone.UTC) - theirLdt.toInstant(TimeZone.UTC)).absoluteValue > 1.seconds) {
45+
// It seems that sometimes, js-joda interprets its data incorrectly by at most one second,
46+
// and we don't want to replicate that.
47+
// Example: America/Noronha at 1914-01-01T02:09:39.998Z:
48+
// - Computed 1913-12-31T23:59:59.998 with offset -02:09:40
49+
// - 1914-01-01T00:00:00.998-02:09:39[America/Noronha] is js-joda's interpretation
50+
// The raw data representing the offset is `29.E`, which is `2 * 60 + 9 + (ord 'E' - 29) / 60`,
51+
// and `ord 'E'` is 69, so the offset is -2:09:40.
52+
// Thus, we allow a difference of 1 second.
53+
throw AssertionError("Failed for $id at $instant: computed $ourLdt with offset $offset, but $zdt is correct")
54+
}
55+
}
56+
fun checkTransition(instant: Instant) {
57+
checkAtInstant(instant - 2.milliseconds)
58+
checkAtInstant(instant)
59+
}
60+
// check historical data
61+
for (transition in rules.transitionEpochSeconds) {
62+
checkTransition(Instant.fromEpochSeconds(transition))
63+
}
64+
}
65+
}
66+
}

0 commit comments

Comments
 (0)