Skip to content

Commit c0ef878

Browse files
Add WASI timezones implementation
Co-authored-by: Dmitry Khalanskiy <[email protected]>
1 parent e528f91 commit c0ef878

File tree

10 files changed

+253
-6
lines changed

10 files changed

+253
-6
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import java.io.File
2+
3+
/*
4+
* Copyright 2019-2024 JetBrains s.r.o. and contributors.
5+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
6+
*/
7+
8+
private val pkg = "package kotlinx.datetime.timezones.tzData"
9+
10+
private fun generateByteArrayProperty(tzData: TzData, header: String, propertyName: String): String = buildString {
11+
append(header)
12+
appendLine()
13+
appendLine()
14+
appendLine("/* ${tzData.fullTzNames.joinToString(", ")} */")
15+
append("internal val $propertyName get() = byteArrayOf(")
16+
for (chunk in tzData.data.toList().chunked(16)) {
17+
appendLine()
18+
append(" ")
19+
val chunkText = chunk.joinToString {
20+
it.toString().padStart(4, ' ')
21+
} + ","
22+
append(chunkText)
23+
}
24+
appendLine()
25+
append(")")
26+
}
27+
28+
private class TzData(val data: ByteArray, val fullTzNames: MutableList<String>)
29+
private fun loadTzBinaries(
30+
zoneInfo: File,
31+
currentName: String,
32+
result: MutableList<TzData>
33+
) {
34+
val zoneName = if (currentName.isEmpty()) zoneInfo.name else "$currentName/${zoneInfo.name}"
35+
if (zoneInfo.isDirectory) {
36+
zoneInfo.listFiles()?.forEach {
37+
loadTzBinaries(it, zoneName, result)
38+
}
39+
} else {
40+
val bytes = zoneInfo.readBytes()
41+
val foundTzData = result.firstOrNull { it.data.contentEquals(bytes) }
42+
val tzData: TzData
43+
if (foundTzData != null) {
44+
tzData = foundTzData
45+
} else {
46+
tzData = TzData(bytes, mutableListOf())
47+
result.add(tzData)
48+
}
49+
50+
tzData.fullTzNames.add(zoneName)
51+
}
52+
}
53+
54+
fun generateZoneInfosResources(zoneInfoDir: File, outputDir: File, version: String) {
55+
val header = buildString {
56+
appendLine()
57+
append("/* AUTOGENERATED FROM ZONE INFO DATABASE v.$version */")
58+
appendLine()
59+
appendLine()
60+
append(pkg)
61+
}
62+
63+
val loadedZones = mutableListOf<TzData>()
64+
zoneInfoDir.listFiles()?.forEach { file ->
65+
loadTzBinaries(file, "", loadedZones)
66+
}
67+
68+
val zoneDataByNameBody = StringBuilder()
69+
val getTimeZonesBody = StringBuilder()
70+
loadedZones.forEachIndexed { id, tzData ->
71+
val tzDataName = "tzData$id"
72+
val data = generateByteArrayProperty(tzData, header, tzDataName)
73+
File(outputDir, "$tzDataName.kt").writeText(data)
74+
tzData.fullTzNames.forEach { name ->
75+
zoneDataByNameBody.appendLine(" \"$name\" -> $tzDataName")
76+
getTimeZonesBody.appendLine(" \"$name\",")
77+
}
78+
}
79+
80+
val content = buildString {
81+
append(header)
82+
appendLine()
83+
appendLine()
84+
appendLine("internal fun zoneDataByName(name: String): ByteArray = when(name) {")
85+
append(zoneDataByNameBody)
86+
appendLine()
87+
append(" else -> throw kotlinx.datetime.IllegalTimeZoneException(\"Invalid timezone name\")")
88+
appendLine()
89+
append("}")
90+
appendLine()
91+
appendLine()
92+
append("internal val timeZones: Set<String> by lazy { setOf(")
93+
appendLine()
94+
append(getTimeZonesBody)
95+
appendLine()
96+
append(")")
97+
append("}")
98+
}
99+
100+
File(outputDir, "tzData.kt").writeText(content)
101+
}

core/common/src/TimeZone.kt

+6
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ public expect open class TimeZone {
6363
*
6464
* If the current system time zone changes, this function can reflect this change on the next invocation.
6565
*
66+
* Always returns the `UTC` timezone on the Wasm WASI platform due to the lack of support for retrieving system timezone information.
67+
*
6668
* @sample kotlinx.datetime.test.samples.TimeZoneSamples.currentSystemDefault
6769
*/
6870
public fun currentSystemDefault(): TimeZone
@@ -92,6 +94,10 @@ public expect open class TimeZone {
9294
*
9395
* @throws IllegalTimeZoneException if [zoneId] has an invalid format or a time-zone with the name [zoneId]
9496
* is not found.
97+
*
98+
* @throws IllegalTimeZoneException on the Wasm WASI platform for non-fixed-offset time zones,
99+
* unless a dependency on the `kotlinx-datetime-zoneinfo` artifact is added.
100+
*
95101
* @sample kotlinx.datetime.test.samples.TimeZoneSamples.constructorFunction
96102
*/
97103
public fun of(zoneId: String): TimeZone

core/wasmWasi/src/internal/Platform.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,6 @@ internal actual fun currentTime(): Instant = clockTimeGet().let { time ->
3939
}
4040

4141
internal actual fun currentSystemDefaultZone(): Pair<String, TimeZoneRules?> =
42-
throw UnsupportedOperationException("WASI platform does not support system timezone obtaining")
42+
"UTC" to null
4343

4444
internal actual val systemTzdb: TimeZoneDatabase = TzdbOnData()

core/wasmWasi/src/internal/TimeZonesInitializer.kt

+3-4
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ This is internal API which is not intended to use on user-side.
1515
*/
1616
@InternalDateTimeApi
1717
public interface TimeZonesProvider {
18-
public fun zoneDataByName(name: String): ByteArray
18+
public fun zoneDataByName(name: String): ByteArray
1919
public fun getTimeZones(): Set<String>
2020
}
2121

@@ -31,15 +31,14 @@ public fun initializeTimeZonesProvider(provider: TimeZonesProvider) {
3131
@InternalDateTimeApi
3232
private var timeZonesProvider: TimeZonesProvider? = null
3333

34+
@OptIn(InternalDateTimeApi::class)
3435
internal class TzdbOnData: TimeZoneDatabase {
3536
override fun rulesForId(id: String): TimeZoneRules {
36-
@OptIn(InternalDateTimeApi::class)
3737
val data = timeZonesProvider?.zoneDataByName(id)
3838
?: throw IllegalTimeZoneException("TimeZones are not supported")
3939
return readTzFile(data).toTimeZoneRules()
4040
}
4141

4242
override fun availableTimeZoneIds(): Set<String> =
43-
@OptIn(InternalDateTimeApi::class)
44-
timeZonesProvider?.getTimeZones() ?: emptySet()
43+
timeZonesProvider?.getTimeZones() ?: setOf("UTC")
4544
}

gradle.properties

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ group=org.jetbrains.kotlinx
55
version=0.6.0
66
versionSuffix=SNAPSHOT
77

8+
tzdbVersion=2024a
9+
810
defaultKotlinVersion=1.9.21
911
dokkaVersion=1.9.20
1012
serializationVersion=1.6.2

serialization/build.gradle.kts

+5-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,11 @@ kotlin {
109109
}
110110

111111
val wasmWasiMain by getting
112-
val wasmWasiTest by getting
112+
val wasmWasiTest by getting {
113+
dependencies {
114+
runtimeOnly(project(":kotlinx-datetime-zoneinfo"))
115+
}
116+
}
113117

114118
val nativeMain by getting
115119
val nativeTest by getting

settings.gradle.kts

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ rootProject.name = "Kotlin-DateTime-library"
1616

1717
include(":core")
1818
project(":core").name = "kotlinx-datetime"
19+
include(":timezones/full")
20+
project(":timezones/full").name = "kotlinx-datetime-zoneinfo"
1921
include(":serialization")
2022
project(":serialization").name = "kotlinx-datetime-serialization"
2123
include(":benchmarks")

timezones/full/build.gradle.kts

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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+
6+
import com.github.gradle.node.npm.task.NpmTask
7+
import com.github.gradle.node.npm.task.NpxTask
8+
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
9+
import org.jetbrains.kotlin.gradle.targets.js.npm.NpmResolverPlugin
10+
import java.util.*
11+
12+
plugins {
13+
kotlin("multiplatform")
14+
id("maven-publish")
15+
id("com.github.node-gradle.node") version "7.0.2"
16+
}
17+
18+
node {
19+
download.set(true)
20+
nodeProjectDir.set(layout.buildDirectory.dir("node"))
21+
}
22+
23+
val tzdbVersion: String by rootProject.properties
24+
version = "$tzdbVersion-spi.$version"
25+
26+
val convertedKtFilesDir = File(project.buildDir, "convertedTimesZones-full/src/internal/tzData")
27+
val tzdbDirectory = File(project.projectDir, "tzdb")
28+
29+
val timeTzdbInstall by tasks.creating(NpmTask::class) {
30+
args.addAll(
31+
"install",
32+
"@tubular/time-tzdb",
33+
)
34+
}
35+
36+
val tzdbDownloadAndCompile by tasks.creating(NpxTask::class) {
37+
doFirst {
38+
tzdbDirectory.mkdirs()
39+
}
40+
dependsOn(timeTzdbInstall)
41+
command.set("@tubular/time-tzdb")
42+
args.addAll("-b", "-o", "--large")
43+
if (tzdbVersion.isNotEmpty()) {
44+
args.addAll("-u", tzdbVersion)
45+
}
46+
args.add(tzdbDirectory.toString())
47+
}
48+
49+
val generateZoneInfo by tasks.registering {
50+
inputs.dir(tzdbDirectory)
51+
outputs.dir(convertedKtFilesDir)
52+
doLast {
53+
generateZoneInfosResources(tzdbDirectory, convertedKtFilesDir, tzdbVersion)
54+
}
55+
}
56+
57+
kotlin {
58+
@OptIn(ExperimentalWasmDsl::class)
59+
wasmWasi {
60+
nodejs()
61+
NpmResolverPlugin.apply(project) //Workaround KT-66373
62+
}
63+
64+
sourceSets.all {
65+
val suffixIndex = name.indexOfLast { it.isUpperCase() }
66+
val targetName = name.substring(0, suffixIndex)
67+
val suffix = name.substring(suffixIndex).lowercase(Locale.ROOT).takeIf { it != "main" }
68+
kotlin.srcDir("$targetName/${suffix ?: "src"}")
69+
resources.srcDir("$targetName/${suffix?.let { it + "Resources" } ?: "resources"}")
70+
}
71+
72+
sourceSets {
73+
commonMain {
74+
dependencies {
75+
compileOnly(project(":kotlinx-datetime"))
76+
kotlin.srcDir(generateZoneInfo)
77+
}
78+
}
79+
80+
val commonTest by getting {
81+
dependencies {
82+
runtimeOnly(project(":kotlinx-datetime"))
83+
implementation(kotlin("test"))
84+
}
85+
}
86+
87+
val wasmWasiMain by getting {
88+
languageSettings.optIn("kotlinx.datetime.internal.InternalDateTimeApi")
89+
}
90+
}
91+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
6+
package kotlinx.datetime.timezones
7+
8+
@Suppress("DEPRECATION")
9+
@OptIn(ExperimentalStdlibApi::class)
10+
@EagerInitialization
11+
private val initializeTimeZones = run {
12+
kotlinx.datetime.internal.initializeTimeZonesProvider(
13+
object : kotlinx.datetime.internal.TimeZonesProvider {
14+
override fun zoneDataByName(name: String): ByteArray =
15+
kotlinx.datetime.timezones.tzData.zoneDataByName(name)
16+
override fun getTimeZones(): Set<String> =
17+
kotlinx.datetime.timezones.tzData.timeZones
18+
}
19+
)
20+
}
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package kotlinx.datetime.timezones
2+
3+
import kotlinx.datetime.timezones.tzData.*
4+
import kotlin.test.Test
5+
import kotlin.test.assertContains
6+
7+
class SimpleChecks {
8+
@Test
9+
fun getTimeZonesTest() {
10+
val timezones = timeZones
11+
assertContains(timezones, "UTC")
12+
assertContains(timezones, "GMT")
13+
assertContains(timezones, "Europe/Amsterdam")
14+
}
15+
16+
@Test
17+
fun checkZonesData() {
18+
zoneDataByName("UTC")
19+
zoneDataByName("GMT")
20+
zoneDataByName("Europe/Amsterdam")
21+
}
22+
}

0 commit comments

Comments
 (0)