Skip to content

Commit

Permalink
Test the js-joda tzdb extraction
Browse files Browse the repository at this point in the history
  • Loading branch information
dkhalanskyjb committed Nov 5, 2024
1 parent 34ab701 commit 17bfddc
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 5 deletions.
12 changes: 7 additions & 5 deletions core/commonJs/src/internal/Platform.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ private val tzdb: Result<TimeZoneDatabase?> = runCatching {
when (char) {
in '0'..'9' -> char - '0'
in 'a'..'z' -> char - 'a' + 10
in 'A'..'Z' -> char - 'A' + 36
in 'A'..'X' -> char - 'A' + 36
else -> throw IllegalArgumentException("Invalid character: $char")
}

Expand Down Expand Up @@ -76,9 +76,10 @@ private val tzdb: Result<TimeZoneDatabase?> = runCatching {
(unpackBase60(it) * SECONDS_PER_MINUTE * MILLIS_PER_ONE).roundToLong() / // minutes to milliseconds
MILLIS_PER_ONE // but we only need seconds
}
println("Zone ${components[0]}: $offsets with ${components[2]} raw data")
zones[components[0]] = TimeZoneRules(
transitionEpochSeconds = lengthsOfPeriodsWithOffsets.partialSums().take<Long>(indices.size - 1),
offsets = indices.map { UtcOffset(null, -offsets[it].roundToInt(), null) },
offsets = indices.map { UtcOffset(null, null, -(offsets[it] * 60).roundToInt()) },
recurringZoneRules = null
)
}
Expand Down Expand Up @@ -132,14 +133,15 @@ internal actual fun currentSystemDefaultZone(): Pair<String, TimeZone?> {
internal actual fun timeZoneById(zoneId: String): TimeZone {
val id = if (zoneId == "SYSTEM") {
val (name, zone) = currentSystemDefaultZone()
if (zone != null) return zone
zone?.let { return it }
name
} else zoneId
val rules = tzdb.getOrThrow()?.rulesForId(id)
if (rules != null) return RegionTimeZone(rules, id)
rulesForId(id)?.let { return RegionTimeZone(it, id) }
throw IllegalTimeZoneException("js-joda timezone database is not available")
}

internal fun rulesForId(zoneId: String): TimeZoneRules? = tzdb.getOrThrow()?.rulesForId(zoneId)

internal actual fun getAvailableZoneIds(): Set<String> =
tzdb.getOrThrow()?.availableTimeZoneIds() ?: setOf("UTC")

Expand Down
32 changes: 32 additions & 0 deletions core/commonJs/test/JSJoda.module_@js-joda_core.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2019-2024 JetBrains s.r.o. and contributors.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/
@file:kotlinx.datetime.internal.JsModule("@js-joda/core")
@file:kotlinx.datetime.internal.JsNonModule
package kotlinx.datetime.test.JSJoda

import kotlinx.datetime.internal.InteropInterface

external class ZonedDateTime : InteropInterface {
fun year(): Int
fun monthValue(): Int
fun dayOfMonth(): Int
fun hour(): Int
fun minute(): Int
fun second(): Int
fun nano(): Double
}

external class Instant : InteropInterface {
fun atZone(zone: ZoneId): ZonedDateTime
companion object {
fun ofEpochMilli(epochMilli: Double): Instant
}
}

external class ZoneId : InteropInterface {
companion object {
fun of(zoneId: String): ZoneId
}
}
66 changes: 66 additions & 0 deletions core/commonJs/test/JsJodaTimezoneTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright 2019-2024 JetBrains s.r.o. and contributors.
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
*/
package kotlinx.datetime.test

import kotlinx.datetime.*
import kotlinx.datetime.internal.rulesForId
import kotlin.math.roundToInt
import kotlin.test.*
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds

class JsJodaTimezoneTest {
@Test
fun system() {
val tz = TimeZone.currentSystemDefault()
assertNotEquals("SYSTEM", tz.id)
val systemTz = TimeZone.of("SYSTEM")
assertEquals(tz, systemTz)
assertEquals(tz.id, systemTz.id)
}

@Test
fun iterateOverAllTimezones() {
for (id in TimeZone.availableZoneIds) {
val rules = rulesForId(id) ?: throw AssertionError("No rules for $id")
val jodaZone = kotlinx.datetime.test.JSJoda.ZoneId.of(id)
assertNull(rules.recurringZoneRules)
fun checkAtInstant(instant: Instant) {
val jodaInstant = kotlinx.datetime.test.JSJoda.Instant.ofEpochMilli(instant.toEpochMilliseconds().toDouble())
val zdt = jodaInstant.atZone(jodaZone)
val offset = rules.infoAtInstant(instant)
val ourLdt = instant.toLocalDateTime(offset)
val theirLdt = LocalDateTime(
zdt.year(),
zdt.monthValue(),
zdt.dayOfMonth(),
zdt.hour(),
zdt.minute(),
zdt.second(),
zdt.nano().roundToInt()
)
if ((ourLdt.toInstant(TimeZone.UTC) - theirLdt.toInstant(TimeZone.UTC)).absoluteValue > 1.seconds) {
// It seems that sometimes, js-joda interprets its data incorrectly by at most one second,
// and we don't want to replicate that.
// Example: America/Noronha at 1914-01-01T02:09:39.998Z:
// - Computed 1913-12-31T23:59:59.998 with offset -02:09:40
// - 1914-01-01T00:00:00.998-02:09:39[America/Noronha] is js-joda's interpretation
// The raw data representing the offset is `29.E`, which is `2 * 60 + 9 + (ord 'E' - 29) / 60`,
// and `ord 'E'` is 69, so the offset is -2:09:40.
// Thus, we allow a difference of 1 second.
throw AssertionError("Failed for $id at $instant: computed $ourLdt with offset $offset, but $zdt is correct")
}
}
fun checkTransition(instant: Instant) {
checkAtInstant(instant - 2.milliseconds)
checkAtInstant(instant)
}
// check historical data
for (transition in rules.transitionEpochSeconds) {
checkTransition(Instant.fromEpochSeconds(transition))
}
}
}
}

0 comments on commit 17bfddc

Please sign in to comment.