Skip to content

Commit

Permalink
Reimplement the behavior for when the js-joda tzdb is not available
Browse files Browse the repository at this point in the history
  • Loading branch information
dkhalanskyjb committed Nov 4, 2024
1 parent ad2ed3c commit 74cbbe3
Show file tree
Hide file tree
Showing 12 changed files with 110 additions and 54 deletions.
9 changes: 7 additions & 2 deletions core/androidNative/src/internal/TimeZoneNative.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@
package kotlinx.datetime.internal

import kotlinx.cinterop.*
import kotlinx.datetime.TimeZone
import platform.posix.*

internal actual val systemTzdb: TimeZoneDatabase get() = tzdb.getOrThrow()
internal actual fun timeZoneById(zoneId: String): TimeZone =
RegionTimeZone(tzdb.getOrThrow().rulesForId(zoneId), zoneId)

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

private val tzdb = runCatching { TzdbBionic() }

internal actual fun currentSystemDefaultZone(): Pair<String, TimeZoneRules?> = memScoped {
internal actual fun currentSystemDefaultZone(): Pair<String, TimeZone?> = memScoped {
val name = readSystemProperty("persist.sys.timezone")
?: throw IllegalStateException("The system property 'persist.sys.timezone' should contain the system timezone")
return name to null
Expand Down
65 changes: 47 additions & 18 deletions core/commonJs/src/internal/Platform.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@ import kotlinx.datetime.internal.JSJoda.ZoneId
import kotlin.math.roundToInt
import kotlin.math.roundToLong

private val tzdb: Result<TimeZoneDatabase> = runCatching { parseTzdb() }

internal actual val systemTzdb: TimeZoneDatabase get() = tzdb.getOrThrow()

private fun parseTzdb(): TimeZoneDatabase {
private val tzdb: Result<TimeZoneDatabase?> = runCatching {
/**
* References:
* - https://github.com/js-joda/js-joda/blob/8c1a7448db92ca014417346049fb64b55f7b1ac1/packages/timezone/src/MomentZoneRulesProvider.js#L78-L94
Expand Down Expand Up @@ -71,7 +67,7 @@ private fun parseTzdb(): TimeZoneDatabase {
fun List<Long>.partialSums(): List<Long> = scanWithoutInitial(0, Long::plus)

val zones = mutableMapOf<String, TimeZoneRules>()
val (zonesPacked, linksPacked) = readTzdb() ?: return EmptyTimeZoneDatabase
val (zonesPacked, linksPacked) = readTzdb() ?: return@runCatching null
for (zone in zonesPacked) {
val components = zone.split('|')
val offsets = components[2].split(' ').map { unpackBase60(it) }
Expand All @@ -92,32 +88,65 @@ private fun parseTzdb(): TimeZoneDatabase {
zones[components[1]] = rules
}
}
return object : TimeZoneDatabase {
object : TimeZoneDatabase {
override fun rulesForId(id: String): TimeZoneRules =
zones[id] ?: throw IllegalTimeZoneException("Unknown time zone: $id")

override fun availableTimeZoneIds(): Set<String> = zones.keys
}
}

private object EmptyTimeZoneDatabase : TimeZoneDatabase {
override fun rulesForId(id: String): TimeZoneRules = when (id) {
"SYSTEM" -> TimeZoneRules(
transitionEpochSeconds = emptyList(),
offsets = listOf(UtcOffset.ZERO),
recurringZoneRules = null
) // TODO: that's not correct, we need to use `Date()`'s offset
else -> throw IllegalTimeZoneException("JSJoda timezone database is not available")
private object SystemTimeZone: TimeZone() {
override val id: String get() = "SYSTEM"

/* https://github.com/js-joda/js-joda/blob/8c1a7448db92ca014417346049fb64b55f7b1ac1/packages/core/src/LocalDate.js#L1404-L1416 +
* https://github.com/js-joda/js-joda/blob/8c1a7448db92ca014417346049fb64b55f7b1ac1/packages/core/src/zone/SystemDefaultZoneRules.js#L69-L71 */
override fun atStartOfDay(date: LocalDate): Instant = atZone(date.atTime(LocalTime.MIN)).toInstant()

/* https://github.com/js-joda/js-joda/blob/8c1a7448db92ca014417346049fb64b55f7b1ac1/packages/core/src/zone/SystemDefaultZoneRules.js#L21-L24 */
override fun offsetAtImpl(instant: Instant): UtcOffset =
UtcOffset(minutes = -Date(instant.toEpochMilliseconds().toDouble()).getTimezoneOffset().toInt())

/* https://github.com/js-joda/js-joda/blob/8c1a7448db92ca014417346049fb64b55f7b1ac1/packages/core/src/zone/SystemDefaultZoneRules.js#L49-L55 */
override fun atZone(dateTime: LocalDateTime, preferred: UtcOffset?): ZonedDateTime {
val epochMilli = dateTime.toInstant(UTC).toEpochMilliseconds()
val offsetInMinutesBeforePossibleTransition = Date(epochMilli.toDouble()).getTimezoneOffset().toInt()
val epochMilliSystemZone = epochMilli +
offsetInMinutesBeforePossibleTransition * SECONDS_PER_MINUTE * MILLIS_PER_ONE
val offsetInMinutesAfterPossibleTransition = Date(epochMilliSystemZone.toDouble()).getTimezoneOffset().toInt()
val offset = UtcOffset(minutes = -offsetInMinutesAfterPossibleTransition)
return ZonedDateTime(dateTime, this, offset)
}

override fun availableTimeZoneIds(): Set<String> = emptySet()
override fun equals(other: Any?): Boolean = other === this

override fun hashCode(): Int = id.hashCode()
}

internal actual fun currentSystemDefaultZone(): Pair<String, TimeZone?> {
val id = ZoneId.systemDefault().id()
return if (id == "SYSTEM") id to SystemTimeZone
else id to null
}

internal actual fun timeZoneById(zoneId: String): TimeZone {
val id = if (zoneId == "SYSTEM") {
val (name, zone) = currentSystemDefaultZone()
if (zone != null) return zone
name
} else zoneId
val rules = tzdb.getOrThrow()?.rulesForId(id)
if (rules != null) return RegionTimeZone(rules, id)
throw IllegalTimeZoneException("js-joda timezone database is not available")
}

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

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

internal external class Date() {
constructor(milliseconds: Double)
fun getTime(): Double
fun getTimezoneOffset(): Double
}
13 changes: 8 additions & 5 deletions core/commonKotlin/src/TimeZone.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ public actual open class TimeZone internal constructor() {

public actual fun currentSystemDefault(): TimeZone {
// TODO: probably check if currentSystemDefault name is parseable as FixedOffsetTimeZone?
val (name, rules) = currentSystemDefaultZone()
return if (rules == null) {
val (name, zone) = currentSystemDefaultZone()
return if (zone == null) {
of(name)
} else {
RegionTimeZone(rules, name)
zone
}
}

Expand All @@ -36,6 +36,9 @@ public actual open class TimeZone internal constructor() {
if (zoneId == "Z") {
return UTC
}
if (zoneId == "SYSTEM") {
return currentSystemDefault()
}
if (zoneId.length == 1) {
throw IllegalTimeZoneException("Invalid zone ID: $zoneId")
}
Expand Down Expand Up @@ -67,14 +70,14 @@ public actual open class TimeZone internal constructor() {
throw IllegalTimeZoneException(e)
}
return try {
RegionTimeZone(systemTzdb.rulesForId(zoneId), zoneId)
timeZoneById(zoneId)
} catch (e: Exception) {
throw IllegalTimeZoneException("Invalid zone ID: $zoneId", e)
}
}

public actual val availableZoneIds: Set<String>
get() = systemTzdb.availableTimeZoneIds()
get() = getAvailableZoneIds()
}

public actual open val id: String
Expand Down
10 changes: 7 additions & 3 deletions core/commonKotlin/src/internal/Platform.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@
package kotlinx.datetime.internal

import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone

internal expect val systemTzdb: TimeZoneDatabase
// RegionTimeZone(systemTzdb.rulesForId(zoneId), zoneId)
internal expect fun timeZoneById(zoneId: String): TimeZone

internal expect fun currentSystemDefaultZone(): Pair<String, TimeZoneRules?>
internal expect fun getAvailableZoneIds(): Set<String>

internal expect fun currentTime(): Instant
internal expect fun currentSystemDefaultZone(): Pair<String, TimeZone?>

internal expect fun currentTime(): Instant
9 changes: 7 additions & 2 deletions core/darwin/src/internal/TimeZoneNative.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,21 @@
package kotlinx.datetime.internal

import kotlinx.cinterop.*
import kotlinx.datetime.TimeZone
import kotlinx.datetime.internal.*
import platform.Foundation.*

internal actual val systemTzdb: TimeZoneDatabase get() = tzdb.getOrThrow()
internal actual fun timeZoneById(zoneId: String): TimeZone =
RegionTimeZone(tzdb.getOrThrow().rulesForId(zoneId), zoneId)

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

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

internal expect fun defaultTzdbPath(): String

internal actual fun currentSystemDefaultZone(): Pair<String, TimeZoneRules?> {
internal actual fun currentSystemDefaultZone(): Pair<String, TimeZone?> {
/* The framework has its own cache of the system timezone. Calls to
[NSTimeZone systemTimeZone] do not reflect changes to the system timezone
and instead just return the cached value. Thus, to acquire the current
Expand Down
9 changes: 7 additions & 2 deletions core/linux/src/internal/TimeZoneNative.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@
package kotlinx.datetime.internal

import kotlinx.datetime.IllegalTimeZoneException
import kotlinx.datetime.TimeZone

internal actual val systemTzdb: TimeZoneDatabase get() = tzdb.getOrThrow()
internal actual fun timeZoneById(zoneId: String): TimeZone =
RegionTimeZone(tzdb.getOrThrow().rulesForId(zoneId), zoneId)

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

private val tzdb = runCatching { TzdbOnFilesystem() }

internal actual fun currentSystemDefaultZone(): Pair<String, TimeZoneRules?> {
internal actual fun currentSystemDefaultZone(): Pair<String, TimeZone?> {
// according to https://www.man7.org/linux/man-pages/man5/localtime.5.html, when there is no symlink, UTC is used
val zonePath = currentSystemTimeZonePath ?: return "Z" to null
val zoneId = zonePath.splitTimeZonePath()?.second?.toString()
Expand Down
2 changes: 1 addition & 1 deletion core/wasmJs/src/JSJodaExceptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* 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
package kotlinx.datetime.internal

private fun withCaughtJsException(body: () -> Unit): JsAny? = js("""{
try {
Expand Down
4 changes: 2 additions & 2 deletions core/wasmJs/src/PlatformSpecifics.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ internal actual fun readTzdb(): Pair<List<String>, List<String>>? = try {
jsTry {
val zones = getZones(ZoneRulesProvider as JsAny)
val links = getLinks(ZoneRulesProvider as JsAny)
return zones.unsafeCast<JsArray<JsString>>().toList() to links.unsafeCast<JsArray<JsString>>().toList()
zones.unsafeCast<JsArray<JsString>>().toList() to links.unsafeCast<JsArray<JsString>>().toList()
}
} catch (_: Throwable) {
return null
null
}

private fun JsArray<JsString>.toList(): List<String> = buildList {
Expand Down
5 changes: 0 additions & 5 deletions core/wasmWasi/src/internal/Platform.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,3 @@ internal actual fun currentTime(): Instant = clockTimeGet().let { time ->
// Instant.MAX and Instant.MIN are never going to be exceeded using just the Long number of nanoseconds
Instant(time.floorDiv(NANOS_PER_ONE.toLong()), time.mod(NANOS_PER_ONE.toLong()).toInt())
}

internal actual fun currentSystemDefaultZone(): Pair<String, TimeZoneRules?> =
"UTC" to null

internal actual val systemTzdb: TimeZoneDatabase = TzdbOnData()
23 changes: 13 additions & 10 deletions core/wasmWasi/src/internal/TimeZonesInitializer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package kotlinx.datetime.internal

import kotlinx.datetime.IllegalTimeZoneException
import kotlinx.datetime.TimeZone

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

@OptIn(InternalDateTimeApi::class)
internal class TzdbOnData: TimeZoneDatabase {
override fun rulesForId(id: String): TimeZoneRules {
val data = timeZonesProvider?.zoneDataByName(id)
?: throw IllegalTimeZoneException("TimeZones are not supported")
return readTzFile(data).toTimeZoneRules()
}

override fun availableTimeZoneIds(): Set<String> =
timeZonesProvider?.getTimeZones() ?: setOf("UTC")
}
internal actual fun timeZoneById(zoneId: String): TimeZone {
val data = timeZonesProvider?.zoneDataByName(zoneId)
?: throw IllegalTimeZoneException("TimeZones are not supported")
val rules = readTzFile(data).toTimeZoneRules()
return RegionTimeZone(rules, zoneId)
}

@OptIn(InternalDateTimeApi::class)
internal actual fun getAvailableZoneIds(): Set<String> =
timeZonesProvider?.getTimeZones() ?: setOf("UTC")

internal actual fun currentSystemDefaultZone(): Pair<String, TimeZone?> = "UTC" to null
10 changes: 8 additions & 2 deletions core/windows/src/internal/TimeZoneNative.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@

package kotlinx.datetime.internal

internal actual val systemTzdb: TimeZoneDatabase get() = tzdbInRegistry.getOrThrow()
import kotlinx.datetime.TimeZone

internal actual fun currentSystemDefaultZone(): Pair<String, TimeZoneRules?> =
internal actual fun timeZoneById(zoneId: String): TimeZone =
RegionTimeZone(tzdbInRegistry.getOrThrow().rulesForId(zoneId), zoneId)

internal actual fun getAvailableZoneIds(): Set<String> =
tzdbInRegistry.getOrThrow().availableTimeZoneIds()

internal actual fun currentSystemDefaultZone(): Pair<String, TimeZone?> =
tzdbInRegistry.getOrThrow().currentSystemDefault()

private val tzdbInRegistry = runCatching { TzdbInRegistry() }
5 changes: 3 additions & 2 deletions core/windows/src/internal/TzdbInRegistry.kt
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ internal class TzdbInRegistry: TimeZoneDatabase {
windowsToRules.containsKey(it.value)
}.keys

internal fun currentSystemDefault(): Pair<String, TimeZoneRules> = memScoped {
internal fun currentSystemDefault(): Pair<String, TimeZone> = memScoped {
val dtzi = alloc<DYNAMIC_TIME_ZONE_INFORMATION>()
val result = GetDynamicTimeZoneInformation(dtzi.ptr)
check(result != TIME_ZONE_ID_INVALID) { "The current system time zone is invalid: ${getLastWindowsError()}" }
Expand All @@ -95,12 +95,13 @@ internal class TzdbInRegistry: TimeZoneDatabase {
?: throw IllegalStateException("Unknown time zone name '$windowsName'")
val tz = windowsToRules[windowsName]
check(tz != null) { "The system time zone is set to a value rules for which are not known: '$windowsName'" }
ianaTzName to if (dtzi.DynamicDaylightTimeDisabled == 0.convert<BOOLEAN>()) {
val rules = if (dtzi.DynamicDaylightTimeDisabled == 0.convert<BOOLEAN>()) {
tz
} else {
// the user explicitly disabled DST transitions, so
TimeZoneRules(UtcOffset(minutes = -(dtzi.Bias + dtzi.StandardBias)), RecurringZoneRules(emptyList()))
}
return ianaTzName to RegionTimeZone(rules, ianaTzName)
}
}

Expand Down

0 comments on commit 74cbbe3

Please sign in to comment.