From c53c746cd0f0b617fd2e647eb99c8f56fbdb40af Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Fri, 23 Jun 2023 12:02:48 +0200 Subject: [PATCH 1/2] Implement the timezone database for Linux in Kotlin --- core/build.gradle.kts | 125 +++---- core/common/src/internal/BinaryDataReader.kt | 51 +++ core/common/test/TimeZoneTest.kt | 110 +++++- core/linux/src/TimeZoneNative.kt | 78 ++++ core/linux/test/TimeZoneRulesCompleteTest.kt | 148 ++++++++ core/native/cinterop/cpp/cdate.cpp | 210 ----------- .../native/cinterop_actuals/TimeZoneNative.kt | 3 +- core/native/src/LocalDate.kt | 6 + core/native/src/ZonedDateTime.kt | 1 - core/native/src/internal/MonthDayTime.kt | 174 +++++++++ core/native/src/internal/OffsetInfo.kt | 46 +++ core/native/src/internal/TimeZoneRules.kt | 183 +++++++++ core/native/test/ThreeTenBpTimeZoneTest.kt | 1 - core/nix/src/internal/TzdbOnFilesystem.kt | 44 +++ core/nix/src/internal/Tzfile.kt | 353 ++++++++++++++++++ core/nix/src/internal/filesystem.kt | 104 ++++++ core/nix/test/TimeZoneRulesTest.kt | 44 +++ core/nix/test/Util.kt | 158 ++++++++ 18 files changed, 1556 insertions(+), 283 deletions(-) create mode 100644 core/common/src/internal/BinaryDataReader.kt create mode 100644 core/linux/src/TimeZoneNative.kt create mode 100644 core/linux/test/TimeZoneRulesCompleteTest.kt delete mode 100644 core/native/cinterop/cpp/cdate.cpp create mode 100644 core/native/src/internal/MonthDayTime.kt create mode 100644 core/native/src/internal/OffsetInfo.kt create mode 100644 core/native/src/internal/TimeZoneRules.kt create mode 100644 core/nix/src/internal/TzdbOnFilesystem.kt create mode 100644 core/nix/src/internal/Tzfile.kt create mode 100644 core/nix/src/internal/filesystem.kt create mode 100644 core/nix/test/TimeZoneRulesTest.kt create mode 100644 core/nix/test/Util.kt diff --git a/core/build.gradle.kts b/core/build.gradle.kts index edb567b7e..5b9417801 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -37,42 +37,44 @@ kotlin { explicitApi() infra { - // Tiers are in accordance with - // Tier 1 - target("linuxX64") - // Tier 2 - target("linuxArm64") + common("nix") { + // Tiers are in accordance with + common("linux") { + // Tier 1 + target("linuxX64") + // Tier 2 + target("linuxArm64") + // Tier 4 (deprecated, but still in demand) + target("linuxArm32Hfp") + } + // the following targets are not supported, as we don't have timezone database implementations for them: + /* + target("androidNativeArm32") + target("androidNativeArm64") + target("androidNativeX86") + target("androidNativeX64") + */ + common("darwin") { + // Tier 1 + target("macosX64") + target("macosArm64") + target("iosSimulatorArm64") + target("iosX64") + // Tier 2 + target("watchosSimulatorArm64") + target("watchosX64") + target("watchosArm32") + target("watchosArm64") + target("tvosSimulatorArm64") + target("tvosX64") + target("tvosArm64") + target("iosArm64") + // Tier 3 + target("watchosDeviceArm64") + } + } // Tier 3 target("mingwX64") - // the following targets are not supported by kotlinx.serialization: - /* - target("androidNativeArm32") - target("androidNativeArm64") - target("androidNativeX86") - target("androidNativeX64") - */ - // Tier 4 (deprecated, but still in demand) - target("linuxArm32Hfp") - - // Darwin targets are listed separately - common("darwin") { - // Tier 1 - target("macosX64") - target("macosArm64") - target("iosSimulatorArm64") - target("iosX64") - // Tier 2 - target("watchosSimulatorArm64") - target("watchosX64") - target("watchosArm32") - target("watchosArm64") - target("tvosSimulatorArm64") - target("tvosX64") - target("tvosArm64") - target("iosArm64") - // Tier 3 - target("watchosDeviceArm64") - } } jvm { @@ -138,31 +140,16 @@ kotlin { compilations["test"].kotlinOptions { freeCompilerArgs += listOf("-trw") } - if (konanTarget.family.isAppleFamily) { - return@withType - } - compilations["main"].cinterops { - create("date") { - val cinteropDir = "$projectDir/native/cinterop" - val dateLibDir = "${project(":").projectDir}/thirdparty/date" - headers("$cinteropDir/public/cdate.h") - defFile("native/cinterop/date.def") - extraOpts("-Xsource-compiler-option", "-I$cinteropDir/public") - extraOpts("-Xsource-compiler-option", "-DONLY_C_LOCALE=1") - when { - konanTarget.family == org.jetbrains.kotlin.konan.target.Family.LINUX -> { - // needed for the date library so that it does not try to download the timezone database - extraOpts("-Xsource-compiler-option", "-DUSE_OS_TZDB=1") - /* using a more modern C++ version causes the date library to use features that are not - * present in the currently outdated GCC root shipped with Kotlin/Native for Linux. */ - extraOpts("-Xsource-compiler-option", "-std=c++11") - // the date library and its headers - extraOpts("-Xcompile-source", "$dateLibDir/src/tz.cpp") - extraOpts("-Xsource-compiler-option", "-I$dateLibDir/include") - // the main source for the platform bindings. - extraOpts("-Xcompile-source", "$cinteropDir/cpp/cdate.cpp") - } - konanTarget.family == org.jetbrains.kotlin.konan.target.Family.MINGW -> { + when { + konanTarget.family == org.jetbrains.kotlin.konan.target.Family.MINGW -> { + compilations["main"].cinterops { + create("date") { + val cinteropDir = "$projectDir/native/cinterop" + val dateLibDir = "${project(":").projectDir}/thirdparty/date" + headers("$cinteropDir/public/cdate.h") + defFile("native/cinterop/date.def") + extraOpts("-Xsource-compiler-option", "-I$cinteropDir/public") + extraOpts("-Xsource-compiler-option", "-DONLY_C_LOCALE=1") // needed to be able to use std::shared_mutex to implement caching. extraOpts("-Xsource-compiler-option", "-std=c++17") // the date library headers, needed for some pure calculations. @@ -170,14 +157,20 @@ kotlin { // the main source for the platform bindings. extraOpts("-Xcompile-source", "$cinteropDir/cpp/windows.cpp") } - else -> { - throw IllegalArgumentException("Unknown native target ${this@withType}") - } + } + compilations["main"].defaultSourceSet { + kotlin.srcDir("native/cinterop_actuals") } } - } - compilations["main"].defaultSourceSet { - kotlin.srcDir("native/cinterop_actuals") + konanTarget.family == org.jetbrains.kotlin.konan.target.Family.LINUX -> { + // do nothing special + } + konanTarget.family.isAppleFamily -> { + // do nothing special + } + else -> { + throw IllegalArgumentException("Unknown native target ${this@withType}") + } } } @@ -422,4 +415,4 @@ tasks.configureEach { with(org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootPlugin.apply(rootProject)) { nodeVersion = "21.0.0-v8-canary202309167e82ab1fa2" nodeDownloadBaseUrl = "https://nodejs.org/download/v8-canary" -} \ No newline at end of file +} diff --git a/core/common/src/internal/BinaryDataReader.kt b/core/common/src/internal/BinaryDataReader.kt new file mode 100644 index 000000000..9703d33b7 --- /dev/null +++ b/core/common/src/internal/BinaryDataReader.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2019-2023 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.internal + +/** + * A helper for reading binary data. + */ +internal class BinaryDataReader(private val bytes: ByteArray, private var position: Int = 0) { + /** + * Reads a byte. + */ + fun readByte(): Byte = bytes[position++] + + /** + * Reads an unsigned byte. + */ + fun readUnsignedByte(): UByte = + readByte().toUByte() + + /** + * Reads a big-endian (network byte order) 32-bit integer. + */ + fun readInt(): Int = + (bytes[position].toInt() and 0xFF shl 24) or + (bytes[position + 1].toInt() and 0xFF shl 16) or + (bytes[position + 2].toInt() and 0xFF shl 8) or + (bytes[position + 3].toInt() and 0xFF).also { position += 4 } + + /** + * Reads a big-endian (network byte order) 64-bit integer. + */ + fun readLong(): Long = + (bytes[position].toLong() and 0xFF shl 56) or + (bytes[position + 1].toLong() and 0xFF shl 48) or + (bytes[position + 2].toLong() and 0xFF shl 40) or + (bytes[position + 3].toLong() and 0xFF shl 32) or + (bytes[position + 4].toLong() and 0xFF shl 24) or + (bytes[position + 5].toLong() and 0xFF shl 16) or + (bytes[position + 6].toLong() and 0xFF shl 8) or + (bytes[position + 7].toLong() and 0xFF).also { position += 8 } + + fun readUtf8String(length: Int) = + bytes.decodeToString(position, position + length).also { position += length } + + fun readAsciiChar(): Char = readByte().toInt().toChar() + + fun skip(length: Int) { position += length } +} diff --git a/core/common/test/TimeZoneTest.kt b/core/common/test/TimeZoneTest.kt index fb9a00ad3..4116aaca8 100644 --- a/core/common/test/TimeZoneTest.kt +++ b/core/common/test/TimeZoneTest.kt @@ -35,8 +35,9 @@ class TimeZoneTest { @Test fun available() { val allTzIds = TimeZone.availableZoneIds - println("Available TZs:") - allTzIds.forEach(::println) + assertContains(allTzIds, "Europe/Berlin") + assertContains(allTzIds, "Europe/Moscow") + assertContains(allTzIds, "America/New_York") assertNotEquals(0, allTzIds.size) assertTrue(TimeZone.currentSystemDefault().id in allTzIds) @@ -46,7 +47,13 @@ class TimeZoneTest { @Test fun availableZonesAreAvailable() { for (zoneName in TimeZone.availableZoneIds) { - TimeZone.of(zoneName) + val timezone = try { + TimeZone.of(zoneName) + } catch (e: Exception) { + throw Exception("Zone $zoneName is not available", e) + } + Instant.DISTANT_FUTURE.toLocalDateTime(timezone).toInstant(timezone) + Instant.DISTANT_PAST.toLocalDateTime(timezone).toInstant(timezone) } } @@ -198,6 +205,103 @@ class TimeZoneTest { check("-5", LocalDateTime(2008, 11, 2, 2, 0, 0, 0)) } + @Test + fun checkKnownTimezoneDatabaseRecords() { + with(TimeZone.of("America/New_York")) { + checkRegular(this, LocalDateTime(2019, 3, 8, 23, 0), UtcOffset(hours = -5)) + checkGap(this, LocalDateTime(2019, 3, 10, 2, 0)) + checkRegular(this, LocalDateTime(2019, 6, 2, 23, 0), UtcOffset(hours = -4)) + checkOverlap(this, LocalDateTime(2019, 11, 3, 2, 0)) + checkRegular(this, LocalDateTime(2019, 12, 5, 23, 0), UtcOffset(hours = -5)) + } + with(TimeZone.of("Europe/Berlin")) { + checkRegular(this, LocalDateTime(2019, 1, 31, 1, 0), UtcOffset(hours = 1)) + checkGap(this, LocalDateTime(2019, 3, 31, 2, 0)) + checkRegular(this, LocalDateTime(2019, 6, 27, 1, 0), UtcOffset(hours = 2)) + checkOverlap(this, LocalDateTime(2019, 10, 27, 3, 0)) + checkRegular(this, LocalDateTime(2019, 12, 5, 23, 0), UtcOffset(hours = 1)) + } + with(TimeZone.of("Europe/Moscow")) { + checkRegular(this, LocalDateTime(2019, 1, 31, 1, 0), UtcOffset(hours = 3)) + checkRegular(this, LocalDateTime(2011, 1, 31, 1, 0), UtcOffset(hours = 3)) + checkGap(this, LocalDateTime(2011, 3, 27, 2, 0)) + checkRegular(this, LocalDateTime(2011, 5, 3, 1, 0), UtcOffset(hours = 4)) + } + with(TimeZone.of("Australia/Sydney")) { + checkRegular(this, LocalDateTime(2019, 1, 31, 1, 0), UtcOffset(hours = 11)) + checkOverlap(this, LocalDateTime(2019, 4, 7, 3, 0)) + checkRegular(this, LocalDateTime(2019, 10, 6, 1, 0), UtcOffset(hours = 10)) + checkGap(this, LocalDateTime(2019, 10, 6, 2, 0)) + checkRegular(this, LocalDateTime(2019, 12, 5, 23, 0), UtcOffset(hours = 11)) + } + } + private fun LocalDateTime(year: Int, monthNumber: Int, dayOfMonth: Int) = LocalDateTime(year, monthNumber, dayOfMonth, 0, 0) } + +/** + * [gapStart] is the first non-existent moment. + */ +private fun checkGap(timeZone: TimeZone, gapStart: LocalDateTime) { + val instant = gapStart.toInstant(timeZone) + /** the first [LocalDateTime] after the gap */ + val adjusted = instant.toLocalDateTime(timeZone) + try { + // there is at least a one-second gap + assertNotEquals(gapStart, adjusted) + // the offsets before the gap are equal + assertEquals( + instant.offsetIn(timeZone), + instant.plus(1, DateTimeUnit.SECOND).offsetIn(timeZone)) + // the offsets after the gap are equal + assertEquals( + instant.minus(1, DateTimeUnit.SECOND).offsetIn(timeZone), + instant.minus(2, DateTimeUnit.SECOND).offsetIn(timeZone) + ) + } catch (e: Throwable) { + throw Exception("Didn't find a gap at $gapStart for $timeZone", e) + } +} + +/** + * [overlapStart] is the first non-ambiguous date-time. + */ +private fun checkOverlap(timeZone: TimeZone, overlapStart: LocalDateTime) { + // the earlier occurrence of the overlap + val instantStart = overlapStart.plusNominalSeconds(-1).toInstant(timeZone).plus(1, DateTimeUnit.SECOND) + // the later occurrence of the overlap + val instantEnd = overlapStart.plusNominalSeconds(1).toInstant(timeZone).minus(1, DateTimeUnit.SECOND) + try { + // there is at least a one-second overlap + assertNotEquals(instantStart, instantEnd) + // the offsets before the overlap are equal + assertEquals( + instantStart.minus(1, DateTimeUnit.SECOND).offsetIn(timeZone), + instantStart.minus(2, DateTimeUnit.SECOND).offsetIn(timeZone) + ) + // the offsets after the overlap are equal + assertEquals( + instantStart.offsetIn(timeZone), + instantEnd.offsetIn(timeZone) + ) + } catch (e: Throwable) { + throw Exception("Didn't find an overlap at $overlapStart for $timeZone", e) + } +} + +private fun checkRegular(timeZone: TimeZone, dateTime: LocalDateTime, offset: UtcOffset) { + val instant = dateTime.toInstant(timeZone) + assertEquals(offset, instant.offsetIn(timeZone)) + try { + // not a gap: + assertEquals(dateTime, instant.toLocalDateTime(timeZone)) + // not an overlap, or an overlap longer than one hour: + assertTrue(dateTime.plusNominalSeconds(3600) <= instant.plus(1, DateTimeUnit.HOUR).toLocalDateTime(timeZone)) + } catch (e: Throwable) { + throw Exception("The date-time at $dateTime for $timeZone was in a gap or overlap", e) + } +} + +private fun LocalDateTime.plusNominalSeconds(seconds: Int): LocalDateTime = + toInstant(UtcOffset.ZERO).plus(seconds, DateTimeUnit.SECOND).toLocalDateTime(UtcOffset.ZERO) diff --git a/core/linux/src/TimeZoneNative.kt b/core/linux/src/TimeZoneNative.kt new file mode 100644 index 000000000..7fe68ac36 --- /dev/null +++ b/core/linux/src/TimeZoneNative.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2019-2023 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +@file:OptIn(ExperimentalForeignApi::class) +package kotlinx.datetime + +import kotlinx.cinterop.* +import kotlinx.datetime.internal.* +import platform.posix.* + +internal actual class RegionTimeZone(private val tzid: TimeZoneRules, actual override val id: String) : TimeZone() { + actual companion object { + actual fun of(zoneId: String): RegionTimeZone = try { + RegionTimeZone(tzdbOnFilesystem.rulesForId(zoneId), zoneId) + } catch (e: Exception) { + throw IllegalTimeZoneException("Invalid zone ID: $zoneId", e) + } + + actual fun currentSystemDefault(): RegionTimeZone { + val zoneId = tzdbOnFilesystem.currentSystemDefault()?.second + ?: throw IllegalStateException("Failed to get the system timezone") + return of(zoneId.toString()) + } + + actual val availableZoneIds: Set + get() = tzdbOnFilesystem.availableTimeZoneIds() + } + + actual override fun atStartOfDay(date: LocalDate): Instant = memScoped { + val ldt = LocalDateTime(date, LocalTime.MIN) + when (val info = tzid.infoAtDatetime(ldt)) { + is OffsetInfo.Regular -> ldt.toInstant(info.offset) + is OffsetInfo.Gap -> info.start + is OffsetInfo.Overlap -> ldt.toInstant(info.offsetBefore) + } + } + + actual override fun atZone(dateTime: LocalDateTime, preferred: UtcOffset?): ZonedDateTime = + when (val info = tzid.infoAtDatetime(dateTime)) { + is OffsetInfo.Regular -> ZonedDateTime(dateTime, this, info.offset) + is OffsetInfo.Gap -> { + try { + ZonedDateTime(dateTime.plusSeconds(info.transitionDurationSeconds), this, info.offsetAfter) + } catch (e: IllegalArgumentException) { + throw DateTimeArithmeticException( + "Overflow whet correcting the date-time to not be in the transition gap", + e + ) + } + } + + is OffsetInfo.Overlap -> ZonedDateTime(dateTime, this, + if (info.offsetAfter == preferred) info.offsetAfter else info.offsetBefore) + } + + actual override fun offsetAtImpl(instant: Instant): UtcOffset = tzid.infoAtInstant(instant) +} + +internal actual fun currentTime(): Instant = memScoped { + val tm = alloc() + val error = clock_gettime(CLOCK_REALTIME, tm.ptr) + if (error != 0) { + val errorStr: String = strerror(errno)?.toKString() ?: "Unknown error" + throw IllegalStateException("Could not obtain the current clock readings from the system: $errorStr") + } + val seconds: Long = tm.tv_sec.convert() + val nanoseconds: Int = tm.tv_nsec.convert() + try { + require(nanoseconds in 0 until NANOS_PER_ONE) + return Instant(seconds, nanoseconds) + } catch (e: IllegalArgumentException) { + throw IllegalStateException("The readings from the system clock are not representable as an Instant") + } +} + +private val tzdbOnFilesystem = TzdbOnFilesystem(Path.fromString("/usr/share/zoneinfo")) diff --git a/core/linux/test/TimeZoneRulesCompleteTest.kt b/core/linux/test/TimeZoneRulesCompleteTest.kt new file mode 100644 index 000000000..fb509da13 --- /dev/null +++ b/core/linux/test/TimeZoneRulesCompleteTest.kt @@ -0,0 +1,148 @@ +/* + * Copyright 2019-2023 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.cinterop.* +import kotlinx.datetime.* +import kotlinx.datetime.internal.* +import platform.posix.* +import kotlin.io.encoding.* +import kotlin.test.* + +class TimeZoneRulesCompleteTest { + @OptIn(ExperimentalEncodingApi::class) + @Test + fun iterateOverAllTimezones() { + val root = Path.fromString("/usr/share/zoneinfo") + val tzdb = TzdbOnFilesystem(root) + for (id in tzdb.availableTimeZoneIds()) { + val file = root.resolve(Path.fromString(id)) + val rules = tzdb.rulesForId(id) + runUnixCommand("env LOCALE=C zdump -V $file").windowed(size = 2, step = 2).forEach { (line1, line2) -> + val beforeTransition = parseZdumpLine(line1) + val afterTransition = parseZdumpLine(line2) + try { + val infoAfter = rules.infoAtInstant(afterTransition.instant) + val infoBefore = rules.infoAtInstant(beforeTransition.instant) + assertEquals(beforeTransition.offset, infoBefore) + assertEquals(afterTransition.offset, infoAfter) + if (beforeTransition.localDateTime.plusSeconds(1) == afterTransition.localDateTime) { + // Regular + val infoAt1 = rules.infoAtDatetime(beforeTransition.localDateTime) + val infoAt2 = rules.infoAtDatetime(afterTransition.localDateTime) + assertEquals(infoAt1, infoAt2) + assertIs(infoAt1) + } else if (afterTransition.localDateTime < beforeTransition.localDateTime) { + // Overlap + val infoAt1 = rules.infoAtDatetime(beforeTransition.localDateTime.plusSeconds(-1)) + val infoAt2 = rules.infoAtDatetime(afterTransition.localDateTime.plusSeconds(1)) + assertEquals(infoAt1, infoAt2) + assertIs(infoAt1) + } else if (afterTransition.localDateTime > beforeTransition.localDateTime) { + // Gap + val infoAt1 = rules.infoAtDatetime(afterTransition.localDateTime.plusSeconds(-1)) + val infoAt2 = rules.infoAtDatetime(beforeTransition.localDateTime.plusSeconds(1)) + assertIs(infoAt1) + assertEquals(infoAt1, infoAt2) + } + } catch (e: Throwable) { + println(beforeTransition) + println(afterTransition) + println(Base64.encode(file.readBytes())) + throw e + } + } + } + } +} + +@OptIn(ExperimentalForeignApi::class) +private inline fun runUnixCommand(command: String): Sequence = sequence { + val pipe = popen(command, "r") ?: error("Failed to run command: $command") + try { + memScoped { + // read line by line + while (true) { + val linePtr = alloc>() + val nPtr = alloc() + try { + val result = getline(linePtr.ptr, nPtr.ptr, pipe) + if (result != (-1).convert()) { + yield(linePtr.value!!.toKString()) + } else { + break + } + } finally { + free(linePtr.value) + } + } + } + } finally { + pclose(pipe) + } +} + +private fun parseZdumpLine(line: String): ZdumpLine { + val parts = line.indexOf(" ") + val path = Path.fromString(line.substring(0, parts)) + val equalSign = line.indexOf(" = ") + val firstDate = line.substring(parts + 2, equalSign) + val isDstStart = line.indexOf(" isdst=") + val secondDate = line.substring(equalSign + 3, isDstStart) + val isDstEnd = line.indexOf(" gmtoff=") + val isDst = line.substring(isDstStart + 7, isDstEnd).toInt() != 0 + val offset = line.substring(isDstEnd + 8, line.length - 1).toInt() + val (firstLdt, firstAbbreviation) = parseRfc2822(firstDate) + check(firstAbbreviation == "UT") + val (secondLdt, secondAbbreviation) = parseRfc2822(secondDate) + return ZdumpLine( + path, + firstLdt.toInstant(UtcOffset.ZERO), + secondLdt, + secondAbbreviation, + isDst, + UtcOffset(seconds = offset), + ) +} + +// TODO: use the datetime formatting capabilities when they are available +fun parseRfc2822(input: String): Pair { + val abbreviation = input.substringAfterLast(" ") + val dateTime = input.substringBeforeLast(" ") + val components = dateTime.split(Regex(" +")) + val dayOfWeek = components[0] + val month = components[1] + val dayOfMonth = components[2].toInt() + val time = LocalTime.parse(components[3]) + val year = components[4].toInt() + val monthNumber = when (month) { + "Jan" -> 1 + "Feb" -> 2 + "Mar" -> 3 + "Apr" -> 4 + "May" -> 5 + "Jun" -> 6 + "Jul" -> 7 + "Aug" -> 8 + "Sep" -> 9 + "Oct" -> 10 + "Nov" -> 11 + "Dec" -> 12 + else -> error("Unknown month: $month") + } + return LocalDateTime(LocalDate(year, monthNumber, dayOfMonth), time) to abbreviation +} + +private class ZdumpLine( + val path: Path, + val instant: Instant, + val localDateTime: LocalDateTime, + val abbreviation: String, + val isDst: Boolean, + val offset: UtcOffset, +) { + override fun toString(): String = "$path $instant = $localDateTime $abbreviation isDst=$isDst offset=$offset" +} diff --git a/core/native/cinterop/cpp/cdate.cpp b/core/native/cinterop/cpp/cdate.cpp deleted file mode 100644 index 2e1e70014..000000000 --- a/core/native/cinterop/cpp/cdate.cpp +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. - * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. - */ -/* This file implements the functions specified in `cdate.h` using the C++20 - chrono API. Since this is not yet widely supported, it relies on the `date` - library. This implementation is used for MacOS and Linux, but once - is available for all the target platforms, the dependency on `date` can be - removed along with the neighboring implementations. */ -#include "date/date.h" -#include "date/tz.h" -#include "helper_macros.hpp" -#include -using namespace date; -using namespace std::chrono; - -static int64_t first_instant_of_year(const year& yr) { - return sys_seconds{sys_days{yr/January/1}}.time_since_epoch().count(); -} -/* This constant represents the earliest moment that our system recognizes; -everything earlier than that is considered the same moment. -This doesn't make us lose any precision in computations, as timezone -information doesn't make sense to use at that time, as there were no -timezones, and even the calendars were all different. -The reason for this explicit check is that the years that are considered -valid by the "date" library are [-32767; 32767], and library crashes if -it sees a date in year -32768 or earlier. */ -static const int64_t min_available_instant = - first_instant_of_year(++year::min()); -// Lack of this check didn't cause any problems yet, but why not add it too? -static const int64_t max_available_instant = - first_instant_of_year(--year::max()); - -static seconds saturating(int64_t epoch_sec) -{ - if (epoch_sec < min_available_instant) - epoch_sec = min_available_instant; - else if (epoch_sec > max_available_instant) - epoch_sec = max_available_instant; - return seconds(epoch_sec); -} - -extern "C" { -#include "cdate.h" -} - -template -static char * timezone_name(const T& zone) -{ - return strdup(zone.name().c_str()); -} - -static const time_zone *zone_by_id(TZID id) -{ - /* The `date` library provides a linked list of `tzdb` objects. `get_tzdb()` - always returns the head of that list. For now, the list never changes: - a call to `reload_tzdb()` would be required to load the updated version - of the timezone database. We never do this because for now (with use of - `date`) this operation is not even present for the configuration that - uses the system timezone database. If we move to C++20 support for this, - it may be feasible to call `reload_tzdb()` and construct a more elaborate - ID scheme. */ - auto& tzdb = get_tzdb(); - try { - return &tzdb.zones.at(id); - } catch (std::out_of_range e) { - throw std::runtime_error("Invalid timezone id"); - } -} - -static TZID id_by_zone(const tzdb& db, const time_zone* tz) -{ - size_t id = tz - &db.zones[0]; - if (id >= db.zones.size()) { - throw std::runtime_error("The time zone is not part of the tzdb"); - } - return id; -} - -extern "C" { - -bool current_time(int64_t *sec, int32_t *nano) -{ - timespec tm; - int error = clock_gettime(CLOCK_REALTIME, &tm); - if (error) { - return false; - } - *sec = tm.tv_sec; - *nano = tm.tv_nsec; - return true; -} - -char * get_system_timezone(TZID * id) -{ - try { - auto& tzdb = get_tzdb(); - auto zone = tzdb.current_zone(); - *id = id_by_zone(tzdb, zone); - return timezone_name(*zone); - } catch (std::runtime_error e) { - *id = TZID_INVALID; - return nullptr; - } -} - -char ** available_zone_ids() -{ - try { - auto& tzdb = get_tzdb(); - auto& zones = tzdb.zones; - char ** zones_copy = check_allocation( - (char **)malloc(sizeof(char *) * (zones.size() + 1))); - zones_copy[zones.size()] = nullptr; - for (unsigned long i = 0; i < zones.size(); ++i) { - zones_copy[i] = timezone_name(zones[i]); - } - return zones_copy; - } catch (std::runtime_error e) { - return nullptr; - } -} - -int offset_at_instant(TZID zone_id, int64_t epoch_sec) -{ - try { - /* `sys_time` is usually Unix time (UTC, not counting leap seconds). - Starting from C++20, it is specified in the standard. */ - auto stime = sys_time(saturating(epoch_sec)); - auto zone = zone_by_id(zone_id); - auto info = zone->get_info(stime); - return info.offset.count(); - } catch (std::runtime_error e) { - return INT_MAX; - } -} - -TZID timezone_by_name(const char *zone_name) -{ - try { - auto& tzdb = get_tzdb(); - return id_by_zone(tzdb, tzdb.locate_zone(zone_name)); - } catch (std::runtime_error e) { - return TZID_INVALID; - } -} - -static int offset_at_datetime_impl(TZID zone_id, seconds sec, int *offset, -GAP_HANDLING gap_handling) -{ - try { - auto zone = zone_by_id(zone_id); - local_seconds seconds(sec); - auto info = zone->get_info(seconds); - switch (info.result) { - case local_info::unique: - *offset = info.first.offset.count(); - return 0; - case local_info::nonexistent: { - *offset = info.second.offset.count(); - switch (gap_handling) { - case GAP_HANDLING_MOVE_FORWARD: - return info.second.offset.count() - - info.first.offset.count(); - case GAP_HANDLING_NEXT_CORRECT: { - return duration_cast( - info.second.begin.time_since_epoch()).count() - - sec.count() + info.second.offset.count(); - } - default: - // impossible - *offset = INT_MAX; - return 0; - } - } - case local_info::ambiguous: - if (info.second.offset.count() != *offset) - *offset = info.first.offset.count(); - return 0; - default: - // the pattern matching above is supposedly exhaustive - *offset = INT_MAX; - return 0; - } - } catch (std::runtime_error e) { - *offset = INT_MAX; - return 0; - } -} - -int offset_at_datetime(TZID zone_id, int64_t epoch_sec, int *offset) -{ - return offset_at_datetime_impl(zone_id, saturating(epoch_sec), offset, - GAP_HANDLING_MOVE_FORWARD); -} - -int64_t at_start_of_day(TZID zone_id, int64_t epoch_sec) -{ - int offset = 0; - int trans = offset_at_datetime_impl(zone_id, saturating(epoch_sec), &offset, - GAP_HANDLING_NEXT_CORRECT); - if (offset == INT_MAX) - return LONG_MAX; - if (epoch_sec > max_available_instant || epoch_sec < min_available_instant) { - trans = 0; - } - return epoch_sec - offset + trans; -} - -} diff --git a/core/native/cinterop_actuals/TimeZoneNative.kt b/core/native/cinterop_actuals/TimeZoneNative.kt index 60f9d0741..0d87660ef 100644 --- a/core/native/cinterop_actuals/TimeZoneNative.kt +++ b/core/native/cinterop_actuals/TimeZoneNative.kt @@ -9,7 +9,6 @@ package kotlinx.datetime import kotlinx.datetime.internal.* import kotlinx.cinterop.* -import kotlinx.datetime.internal.* import platform.posix.free internal actual class RegionTimeZone(private val tzid: TZID, actual override val id: String): TimeZone() { @@ -98,4 +97,4 @@ internal actual fun currentTime(): Instant = memScoped { } catch (e: IllegalArgumentException) { throw IllegalStateException("The readings from the system clock are not representable as an Instant") } -} \ No newline at end of file +} diff --git a/core/native/src/LocalDate.kt b/core/native/src/LocalDate.kt index a63279fec..e08fa6297 100644 --- a/core/native/src/LocalDate.kt +++ b/core/native/src/LocalDate.kt @@ -295,3 +295,9 @@ public actual fun LocalDate.periodUntil(other: LocalDate): DatePeriod { val days = plusMonths(months).daysUntil(other) return DatePeriod(totalMonths = months, days) } + +internal fun LocalDate.previousOrSame(dayOfWeek: DayOfWeek) = + minus((this.dayOfWeek.isoDayNumber - dayOfWeek.isoDayNumber).mod(7), DateTimeUnit.DAY) + +internal fun LocalDate.nextOrSame(dayOfWeek: DayOfWeek) = + plus((dayOfWeek.isoDayNumber - this.dayOfWeek.isoDayNumber).mod(7), DateTimeUnit.DAY) diff --git a/core/native/src/ZonedDateTime.kt b/core/native/src/ZonedDateTime.kt index f19ac49ad..655da4408 100644 --- a/core/native/src/ZonedDateTime.kt +++ b/core/native/src/ZonedDateTime.kt @@ -31,7 +31,6 @@ internal class ZonedDateTime(val dateTime: LocalDateTime, private val zone: Time this === other || other is ZonedDateTime && dateTime == other.dateTime && offset == other.offset && zone == other.zone - @OptIn(ExperimentalStdlibApi::class) override fun hashCode(): Int { return dateTime.hashCode() xor offset.hashCode() xor zone.hashCode().rotateLeft(3) } diff --git a/core/native/src/internal/MonthDayTime.kt b/core/native/src/internal/MonthDayTime.kt new file mode 100644 index 000000000..e1596e8aa --- /dev/null +++ b/core/native/src/internal/MonthDayTime.kt @@ -0,0 +1,174 @@ +/* + * Copyright 2019-2023 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.internal + +import kotlinx.datetime.* + +/** + * A rule expressing how to create a date in a given year. + * + * Some examples of expressible dates: + * * the 16th March + * * the Sunday on or after the 16th March + * * the Sunday on or before the 16th March + * * the last Sunday in February + * * the 300th day of the year + * * the last day of February + */ +internal interface DateOfYear { + /** + * Converts this date-time to an [Instant] in the given [year], + * using the knowledge of the offset that's in effect at the resulting date-time. + */ + fun toLocalDate(year: Int): LocalDate +} + +/** + * The day of year, in the 0..365 range. During leap years, 29th February is counted as the 60th day of the year. + * The number 366 is not supported, as outside the leap years, there are only 365 days in a year. + */ +internal class JulianDayOfYear(val zeroBasedDayOfYear: Int) : DateOfYear { + init { + require(zeroBasedDayOfYear in 0..365) { + "Expected a value in 1..365 for the Julian day-of-year, but got $zeroBasedDayOfYear" + } + } + override fun toLocalDate(year: Int): LocalDate = + LocalDate(year, 1, 1).plusDays(zeroBasedDayOfYear) + + override fun toString(): String = "JulianDayOfYear($zeroBasedDayOfYear)" +} + +/** + * The day of year, in the 1..365 range. During leap years, 29th February is skipped. + */ +internal fun JulianDayOfYearSkippingLeapDate(dayOfYear: Int) : DateOfYear { + require(dayOfYear in 1..365) { + "Expected a value in 1..365 for the Julian day-of-year (skipping the leap date), but got $dayOfYear" + } + // In this form, the `dayOfYear` corresponds exactly to a specific month and day. + // For example, `dayOfYear = 60` is always 1st March, even in leap years. + // We take a non-leap year, as in that case, this is the same as JulianDayOfYear, so regular addition works. + val date = LocalDate(2011, 1, 1).plusDays(dayOfYear - 1) + return MonthDayOfYear(date.month, MonthDayOfYear.TransitionDay.ExactlyDayOfMonth(date.dayOfMonth)) +} + +internal class MonthDayOfYear(val month: Month, val day: TransitionDay) : DateOfYear { + override fun toLocalDate(year: Int): LocalDate = day.resolve(year, month) + + /** + * The day of month when the transition occurs. + */ + sealed interface TransitionDay { + /** + * The first given [dayOfWeek] of the month that is not earlier than [atLeastDayOfMonth]. + */ + class First(val dayOfWeek: DayOfWeek, val atLeastDayOfMonth: Int = 1) : TransitionDay { + override fun resolve(year: Int, month: Month): LocalDate = + LocalDate(year, month, atLeastDayOfMonth).nextOrSame(dayOfWeek) + + override fun toString(): String = "the first $dayOfWeek" + + (if (atLeastDayOfMonth > 1) " on or after $atLeastDayOfMonth" else "") + } + + companion object { + /** + * The [n]th given [dayOfWeek] in the month. + */ + fun Nth(dayOfWeek: DayOfWeek, n: Int): TransitionDay = + First(dayOfWeek, (n-1) * 7 + 1) + } + + /** + * The last given [dayOfWeek] of the month that is not later than [atMostDayOfMonth]. + */ + class Last(val dayOfWeek: DayOfWeek, val atMostDayOfMonth: Int?) : TransitionDay { + override fun resolve(year: Int, month: Month): LocalDate { + val dayOfMonth = atMostDayOfMonth ?: month.number.monthLength(isLeapYear(year)) + return LocalDate(year, month, dayOfMonth).previousOrSame(dayOfWeek) + } + + override fun toString(): String = "the last $dayOfWeek" + + (atMostDayOfMonth?.let { " on or before $it" } ?: "") + } + + /** + * Exactly the given [dayOfMonth]. + */ + class ExactlyDayOfMonth(val dayOfMonth: Int) : TransitionDay { + override fun resolve(year: Int, month: Month): LocalDate = LocalDate(year, month, dayOfMonth) + + override fun toString(): String = "$dayOfMonth" + } + + fun resolve(year: Int, month: Month): LocalDate + } + + override fun toString(): String = "$month, $day" +} + +internal class MonthDayTime( + /** + * The date. + */ + val date: DateOfYear, + /** + * The procedure to calculate the local time. + */ + val time: TransitionLocaltime, + /** + * The definition of how the offset in which the local date-time is expressed. + */ + val offset: OffsetResolver, +) { + + /** + * Converts this [MonthDayTime] to an [Instant] in the given [year], + * using the knowledge of the offset that's in effect at the resulting date-time. + */ + fun toInstant(year: Int, effectiveOffset: UtcOffset): Instant { + val localDateTime = time.resolve(date.toLocalDate(year)) + return when (this.offset) { + is OffsetResolver.WallClockOffset -> localDateTime.toInstant(effectiveOffset) + is OffsetResolver.FixedOffset -> localDateTime.toInstant(this.offset.offset) + } + } + + /** + * Describes how the offset in which the local date-time is expressed is defined. + */ + sealed interface OffsetResolver { + /** + * The offset is the one currently used by the wall clock. + */ + object WallClockOffset : OffsetResolver { + override fun toString(): String = "wall clock offset" + } + + /** + * The offset is fixed to a specific value. + */ + class FixedOffset(val offset: UtcOffset) : OffsetResolver { + override fun toString(): String = offset.toString() + } + } + + /** + * The local time of day at which the transition occurs. + */ + class TransitionLocaltime(val seconds: Int) { + constructor(time: LocalTime) : this(time.toSecondOfDay()) + + constructor(hour: Int, minute: Int, second: Int) : this(hour * 3600 + minute * 60 + second) + + fun resolve(date: LocalDate): LocalDateTime = date.atTime(LocalTime(0, 0)).plusSeconds(seconds) + + override fun toString(): String = if (seconds < 86400) + LocalTime.ofSecondOfDay(seconds, 0).toString() else "$seconds seconds since the day start" + } + + override fun toString(): String = "$date, $time, $offset" +} diff --git a/core/native/src/internal/OffsetInfo.kt b/core/native/src/internal/OffsetInfo.kt new file mode 100644 index 000000000..a89946da8 --- /dev/null +++ b/core/native/src/internal/OffsetInfo.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019-2023 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.internal + +import kotlinx.datetime.* + +internal sealed interface OffsetInfo { + data class Gap( + val start: Instant, + val offsetBefore: UtcOffset, + val offsetAfter: UtcOffset + ): OffsetInfo { + init { + check(offsetBefore.totalSeconds < offsetAfter.totalSeconds) + } + + val transitionDurationSeconds: Int get() = offsetAfter.totalSeconds - offsetBefore.totalSeconds + } + + data class Overlap( + val start: Instant, + val offsetBefore: UtcOffset, + val offsetAfter: UtcOffset + ): OffsetInfo { + init { + check(offsetBefore.totalSeconds > offsetAfter.totalSeconds) + } + } + + data class Regular( + val offset: UtcOffset + ) : OffsetInfo +} + +internal fun OffsetInfo(transitionInstant: Instant, offsetBefore: UtcOffset, offsetAfter: UtcOffset): OffsetInfo = + if (offsetBefore == offsetAfter) { + OffsetInfo.Regular(offsetBefore) + } else if (offsetBefore.totalSeconds < offsetAfter.totalSeconds) { + OffsetInfo.Gap(transitionInstant, offsetBefore, offsetAfter) + } else { + OffsetInfo.Overlap(transitionInstant, offsetBefore, offsetAfter) + } + diff --git a/core/native/src/internal/TimeZoneRules.kt b/core/native/src/internal/TimeZoneRules.kt new file mode 100644 index 000000000..4eae909ce --- /dev/null +++ b/core/native/src/internal/TimeZoneRules.kt @@ -0,0 +1,183 @@ +/* + * Copyright 2019-2023 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.internal + +import kotlinx.datetime.* +import kotlin.math.* + +internal class TimeZoneRules( + /** + * The list of [Instant.epochSeconds] parts of the instants when recorded transitions occur, in ascending order. + */ + val transitionEpochSeconds: List, + /** + * The list of effective offsets. + * The length is one more than the length of [transitionEpochSeconds]. + * The first element is the offset before the initial transition, and all the rest are the offsets after the + * corresponding transitions. + */ + val offsets: List, + /** + * The transition rules for recurring transitions. + * + * If not null, then the last element of [offsets] must be the offset that is in effect at the start of each year + * after the last transition recorded in [transitionEpochSeconds], before the first transition recorded in + * [recurringZoneRules]. + */ + val recurringZoneRules: RecurringZoneRules?, +) { + init { + require(offsets.size == transitionEpochSeconds.size + 1) { + "offsets.size must be one more than transitionEpochSeconds.size" + } + } + + /** + * Constructs a [TimeZoneRules] without any historic data. + */ + constructor(initialOffset: UtcOffset, rules: RecurringZoneRules) : this( + transitionEpochSeconds = emptyList(), + offsets = listOf(initialOffset), + recurringZoneRules = rules, + ) + + /** + * The list of [LocalDateTime] values related to the transitions. + * The length is twice the length of [transitionEpochSeconds]. + * For each transition, there are two [LocalDateTime] elements in a row, one before the transition, and one after, + * but reordered so that they are in ascending order to allow efficient binary search. + */ + private val transitionLocalDateTimes: List = buildList { + for (i in transitionEpochSeconds.indices) { + val instant = Instant.fromEpochSeconds(transitionEpochSeconds[i]) + val ldtBefore = instant.toLocalDateTime(offsets[i]) + val ldtAfter = instant.toLocalDateTime(offsets[i + 1]) + if (ldtBefore < ldtAfter) { + add(ldtBefore) + add(ldtAfter) + } else { + add(ldtAfter) + add(ldtBefore) + } + } + } + + fun infoAtInstant(instant: Instant): UtcOffset { + val epochSeconds = instant.epochSeconds + // good: no transitions, or instant is after the last transition + if (recurringZoneRules != null && transitionEpochSeconds.lastOrNull()?.let { epochSeconds >= it } != false) { + return recurringZoneRules.infoAtInstant(instant, offsets.last()) + } + // an index in the [offsets] list of the offset that is in effect at the given instant, + // which is the index of the first element that is greater than the given instant, plus one. + val index = transitionEpochSeconds.binarySearch(epochSeconds).let { + // if the exact value is not found, the returned value is (-insertionPoint - 1), but in that case, we want + // the index of the element that is smaller than the searched value, so we look at (insertionPoint - 1). + (it + 1).absoluteValue + } + return offsets[index] + } + + fun infoAtDatetime(localDateTime: LocalDateTime): OffsetInfo { + if (recurringZoneRules != null && transitionLocalDateTimes.lastOrNull()?.let { localDateTime > it } != false) { + return recurringZoneRules.infoAtLocalDateTime(localDateTime, offsets.last()) + } + val lastIndexNotBiggerThanLdt = transitionLocalDateTimes.binarySearch(localDateTime).let { + // if the exact value is not found, the returned value is (-insertionPoint - 1), but in that case, we want + // the index of the element that is smaller than the searched value, so we look at (insertionPoint - 1). + if (it < 0) -it - 2 else it + } + if (lastIndexNotBiggerThanLdt == -1) { + // before the first transition + return OffsetInfo.Regular(offsets.first()) + } + return if (lastIndexNotBiggerThanLdt % 2 == 0) { + // inside a transition: after the smaller LDT but before the bigger one + offsetInfoForTransitionIndex(lastIndexNotBiggerThanLdt / 2) + } else if (lastIndexNotBiggerThanLdt != transitionLocalDateTimes.size - 1 && + transitionLocalDateTimes[lastIndexNotBiggerThanLdt] == transitionLocalDateTimes[lastIndexNotBiggerThanLdt + 1] + ) { + // seemingly outside a transition, but actually a transition happens right after the last one. + // TODO: 310bp does this, but can this ever actually happen? + offsetInfoForTransitionIndex(lastIndexNotBiggerThanLdt / 2 + 1) + } else { + // outside a transition + OffsetInfo.Regular(offsets[lastIndexNotBiggerThanLdt / 2 + 1]) + } + } + + private fun offsetInfoForTransitionIndex(transitionIndex: Int): OffsetInfo { + val transitionInstant = Instant.fromEpochSeconds(transitionEpochSeconds[transitionIndex]) + val offsetBefore = offsets[transitionIndex] + val offsetAfter = offsets[transitionIndex + 1] + return OffsetInfo(transitionInstant, offsetBefore, offsetAfter) + } +} + +internal class RecurringZoneRules( + /** + * The list of transitions that occur every year, in the order of occurrence. + */ + val rules: List> +) { + class Rule( + val transitionDateTime: T, + val offsetBefore: UtcOffset, + val offsetAfter: UtcOffset, + ) { + override fun toString(): String = "transitioning to $offsetAfter on $transitionDateTime" + } + + // see `tzparse` in https://data.iana.org/time-zones/tzdb/localtime.c: looks like there's no guarantees about + // a way to pre-sort the transitions, so we have to do it for each query separately. + private fun rulesForYear(year: Int): List> { + return rules.map { rule -> + val transitionInstant = rule.transitionDateTime.toInstant(year, rule.offsetBefore) + Rule(transitionInstant, rule.offsetBefore, rule.offsetAfter) + }.sortedBy { it.transitionDateTime } + } + + fun infoAtInstant(instant: Instant, offsetAtYearStart: UtcOffset): UtcOffset { + val approximateYear = instant.toLocalDateTime(offsetAtYearStart).year + var offset = offsetAtYearStart + for (rule in rulesForYear(approximateYear)) { + if (instant < rule.transitionDateTime) { + return rule.offsetBefore + } + offset = rule.offsetAfter + } + return if (instant.toLocalDateTime(offset).year == approximateYear) { + // [instant] is still in the same year, just after the last transition + offset + } else { + // [instant] is in the next year, so we need to find the offset at the start of that year. + // This will converge in the next iteration, because then, the year will be correct. + infoAtInstant(instant, offset) + } + } + + fun infoAtLocalDateTime(localDateTime: LocalDateTime, offsetAtYearStart: UtcOffset): OffsetInfo { + val year = localDateTime.year + var offset = offsetAtYearStart + for (rule in rulesForYear(year)) { + val ldtBefore = rule.transitionDateTime.toLocalDateTime(rule.offsetBefore) + val ldtAfter = rule.transitionDateTime.toLocalDateTime(rule.offsetAfter) + return if (localDateTime < ldtBefore && localDateTime < ldtAfter) { + OffsetInfo.Regular(rule.offsetBefore) + } else if (localDateTime > ldtBefore && localDateTime >= ldtAfter) { + offset = rule.offsetAfter + continue + } else if (ldtAfter < ldtBefore) { + OffsetInfo.Overlap(rule.transitionDateTime, rule.offsetBefore, rule.offsetAfter) + } else { + OffsetInfo.Gap(rule.transitionDateTime, rule.offsetBefore, rule.offsetAfter) + } + } + return OffsetInfo.Regular(offset) + } + + override fun toString(): String = rules.joinToString(", ") +} diff --git a/core/native/test/ThreeTenBpTimeZoneTest.kt b/core/native/test/ThreeTenBpTimeZoneTest.kt index ae2eb10e7..66bc6a2ff 100644 --- a/core/native/test/ThreeTenBpTimeZoneTest.kt +++ b/core/native/test/ThreeTenBpTimeZoneTest.kt @@ -9,7 +9,6 @@ package kotlinx.datetime.test import kotlinx.datetime.* -import kotlin.math.* import kotlin.test.* diff --git a/core/nix/src/internal/TzdbOnFilesystem.kt b/core/nix/src/internal/TzdbOnFilesystem.kt new file mode 100644 index 000000000..dcf1680fd --- /dev/null +++ b/core/nix/src/internal/TzdbOnFilesystem.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2019-2023 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.internal + +internal class TzdbOnFilesystem(defaultTzdbPath: Path) { + + internal fun rulesForId(id: String): TimeZoneRules = + readTzFile(tzdbPath.resolve(Path.fromString(id)).readBytes()).toTimeZoneRules() + + internal fun availableTimeZoneIds(): Set = buildSet { + tzdbPath.traverseDirectory(exclude = tzdbUnneededFiles) { add(it.toString()) } + } + + internal fun currentSystemDefault(): Pair? { + val info = Path(true, listOf("etc", "localtime")).readLink() ?: return null + val i = info.components.indexOf("zoneinfo") + if (!info.isAbsolute || i == -1 || i == info.components.size - 1) return null + return Pair( + Path(true, info.components.subList(0, i + 1)), + Path(false, info.components.subList(i + 1, info.components.size)) + ) + } + + private val tzdbPath = defaultTzdbPath.check()?.let { defaultTzdbPath } + ?: currentSystemDefault()?.first ?: throw IllegalStateException("Could not find the path to the timezone database") + +} + +private val tzdbUnneededFiles = setOf( + "posix", + "posixrules", + "Factory", + "iso3166.tab", + "right", + "+VERSION", + "zone.tab", + "zone1970.tab", + "tzdata.zi", + "leapseconds", + "leap-seconds.list" +) diff --git a/core/nix/src/internal/Tzfile.kt b/core/nix/src/internal/Tzfile.kt new file mode 100644 index 000000000..d0a5362ae --- /dev/null +++ b/core/nix/src/internal/Tzfile.kt @@ -0,0 +1,353 @@ +/* + * Copyright 2019-2023 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.internal + +import kotlinx.datetime.* + +internal class TzFileData( + val leapSecondRules: List, + val transitions: List, + val states: List, +) { + /** + * The list of rules for inserting leap seconds. + */ + class LeapSecondRule( + /** + * The time at which a new leap second is to be inserted. + */ + val time: Long, + /** + * The total number of leap seconds to be applied after [time] and before the next rule. + */ + val total: Int + ) + + class ClockState( + val offset: TzFileOffset, + val isDst: Boolean, + val abbreviation: String, + ) + + class Transition( + val time: Long, + val stateIndex: Int + ) +} + +internal class TzFile(val data: TzFileData, val rules: PosixTzString?) { + fun toTimeZoneRules(): TimeZoneRules { + val tzOffsets = buildList { + add(data.states[0].offset) + data.transitions.forEach { add(data.states[it.stateIndex].offset) } + } + val offsets = tzOffsets.map { it.toUtcOffset() } + return TimeZoneRules(data.transitions.map { it.time }, offsets, rules?.toRecurringZoneRules()) + } +} + +/** + * An extension of [UtcOffset] to support the full range of offsets used in `tzfile`, which is: + * + * ``` + * The tt_utoff value is never equal to -2**31, to let 32-bit clients negate it without overflow. + * Also, in realistic applications tt_utoff is in the range [-89999, 93599] (i.e., more than -25 hours and less than 26 + * hours); this allows easy support by implementations that already support the POSIX-required range + * [-24:59:59, 25:59:59]. + * ``` + */ +internal class TzFileOffset(val totalSeconds: Int) { + /** + * Converts this offset to a [UtcOffset]. + * + * @throws IllegalArgumentException if the offset is not in the range [-18 hours, +18 hours]. + */ + fun toUtcOffset(): UtcOffset = UtcOffset(seconds = totalSeconds) +} + +// https://datatracker.ietf.org/doc/html/rfc8536 +internal fun readTzFile(data: ByteArray): TzFile { + class Header( + val version: Int?, + val ttisutcnt: Int, + val ttisstdcnt: Int, + val leapcnt: Int, + val timecnt: Int, + val typecnt: Int, + val charcnt: Int + ) { + override fun toString(): String = "Header(version=$version, ttisutcnt=$ttisutcnt, ttisstdcnt=$ttisstdcnt, " + + "leapcnt=$leapcnt, timecnt=$timecnt, typecnt=$typecnt, charcnt=$charcnt)" + } + + class Ttinfo(val utoff: Int, val isdst: Boolean, val abbrind: UByte) { + override fun toString(): String = "Ttinfo(utoff=$utoff, isdst=$isdst, abbrind=$abbrind)" + } + + inline fun BinaryDataReader.readData(header: Header, readTime: () -> Long): TzFileData { + val transitionTimes = List(header.timecnt) { readTime() } + val transitionTypes = List(header.timecnt) { readByte() } + val ttinfos = List(header.typecnt) { + Ttinfo( + readInt(), + readByte() != 0.toByte(), + readUnsignedByte() + ) + } + val abbreviations = List(header.charcnt) { readByte() } + fun abbreviationForIndex(startIndex: UByte): String = abbreviations.drop(startIndex.toInt()) + .takeWhile { byte -> byte != 0.toByte() }.toByteArray().decodeToString() + val leapSecondRules = List(header.leapcnt) { + TzFileData.LeapSecondRule( + readTime(), + readInt() + ) + } + // The following fields are not used in practice. See https://datatracker.ietf.org/doc/html/rfc8536#section-3.2, + // near the end of the section, `A given pair of standard/wall and UT/local indicators...` + repeat(header.ttisstdcnt) { readByte() } + repeat(header.ttisutcnt) { readByte() } + return TzFileData( + leapSecondRules, + transitionTimes.zip(transitionTypes) { time, type -> TzFileData.Transition(time, type.toInt()) }, + ttinfos.map { TzFileData.ClockState(TzFileOffset(it.utoff), it.isdst, abbreviationForIndex(it.abbrind)) } + ) + } + + fun BinaryDataReader.read32BitData(header: Header): TzFileData = readData(header) { readInt().toLong() } + + fun BinaryDataReader.read64BitData(header: Header): TzFileData = readData(header) { readLong() } + + inline fun BinaryDataReader.readFooter() = check(readByte() == '\n'.code.toByte()).let { + PosixTzString.readIfPresent(this) + } + + val reader = BinaryDataReader(data) + + fun readHeader(): Header { + val magic = reader.readUtf8String(4) + check(magic == "TZif") { "Invalid tzfile magic: '$magic', expected 'TZif'" } + val version = when (reader.readByte()) { + 0.toByte() -> 1 + 0x32.toByte() -> 2 + 0x33.toByte() -> 3 + else -> null + } + reader.skip(15) + return Header( + version, + reader.readInt(), + reader.readInt(), + reader.readInt(), + reader.readInt(), + reader.readInt(), + reader.readInt() + ).also { + check(it.ttisutcnt == 0 || it.ttisutcnt == it.typecnt) + check(it.ttisstdcnt == 0 || it.ttisstdcnt == it.typecnt) + } + } + + val header = readHeader() + return when (header.version) { + 1 -> { + TzFile(reader.read32BitData(header), null) + } + else -> { + reader.read32BitData(header) // skipped + val newHeader = readHeader() + val parsedData = reader.read64BitData(newHeader) + val footer = reader.readFooter() + TzFile(parsedData, footer) + } + } +} + +internal class PosixTzString( + private val standardTime: Pair, + private val daylightTime: Pair?, + private val rules: Pair?, +) { + companion object { + /** + * Reads a POSIX TZ string from the [reader] if it is present, or returns `null` if it is not. + * + * The string format is described in https://pubs.opengroup.org/onlinepubs/9699919799/, section 8.3, + * with additional extensions in https://datatracker.ietf.org/doc/html/rfc8536#section-3.3.1 + * + * @throws IllegalArgumentException if the string is invalid + * @throws IllegalStateException if the string is invalid + */ + fun readIfPresent(reader: BinaryDataReader): PosixTzString? = reader.readPosixTzString() + } + + fun toRecurringZoneRules(): RecurringZoneRules? { + /** + * In theory, it's possible to have a DST transition but no start/end date. + * In this case, the behavior is not specified, and on Linux, the rules for America/New_York are used + * to determine the start/end dates (see `tzset(3)`, search for `posixrules`). + * The library takes the lack of start/end dates + * to mean that the standard offset is always in effect, which seems to be a much more reasonable interpretation. + */ + if (daylightTime == null || rules == null) return null + val (start, end) = rules + val rule1 = RecurringZoneRules.Rule(start, standardTime.second, daylightTime.second) + val rule2 = RecurringZoneRules.Rule(end, daylightTime.second, standardTime.second) + return RecurringZoneRules(listOf(rule1, rule2)) + } +} +private fun BinaryDataReader.readPosixTzString(): PosixTzString? { + var c = readAsciiChar() + fun readName(): String? { + if (c == '\n') return null + val name = StringBuilder() + if (c == '<') { + c = readAsciiChar() + while (c != '>') { + check(c.isLetterOrDigit() || c == '-' || c == '+') { "Invalid char '$c' in the std name in POSIX TZ string" } + name.append(c) + c = readAsciiChar() + } + c = readAsciiChar() + } else { + while (c.isLetter()) { + name.append(c) + c = readAsciiChar() + } + } + check(name.isNotEmpty()) { "Empty std name in POSIX TZ string" } + return name.toString() + } + + fun readOffset(): UtcOffset? { + if (c == '\n') return null + val offsetIsNegative: Boolean + when (c) { + '-' -> { + offsetIsNegative = true + c = readAsciiChar() + } + + '+' -> { + offsetIsNegative = false + c = readAsciiChar() + } + + else -> { + if (!c.isDigit()) return null + offsetIsNegative = false + } + } + val sign = if (offsetIsNegative) 1 else -1 // not a typo: the sign is inverted in the rules + var hours = c.digitToInt() + c = readAsciiChar() + if (c.isDigit()) { + hours = hours * 10 + c.digitToInt() + c = readAsciiChar() + } + if (c != ':') return UtcOffset(sign * hours) + val minutes = readAsciiChar().digitToInt() * 10 + readAsciiChar().digitToInt() + c = readAsciiChar() + if (c != ':') return UtcOffset(sign * hours, sign * minutes) + val seconds = readAsciiChar().digitToInt() * 10 + readAsciiChar().digitToInt() + c = readAsciiChar() + return UtcOffset(sign * hours, sign * minutes, sign * seconds) + } + + fun readDate(): DateOfYear? { + if (c == '\n') return null + check(c == ',') { "Invalid char '$c' in POSIX TZ string after the DST offset" } + c = readAsciiChar() + return when (c) { + 'J' -> { + c = readAsciiChar() + var result = 0 + while (c.isDigit()) { + result = result * 10 + c.digitToInt() + c = readAsciiChar() + } + JulianDayOfYearSkippingLeapDate(result) + } + 'M' -> { + c = readAsciiChar() + var month = c.digitToInt() + c = readAsciiChar() + if (c.isDigit()) { + month = month * 10 + c.digitToInt() + c = readAsciiChar() + } + check(c == '.') { "Invalid char '$c' in POSIX TZ string after M$month" } + c = readAsciiChar() + val week = c.digitToInt() + check(week in 1..5) { "Invalid week number '$week' in POSIX TZ string after M$month" } + c = readAsciiChar() + check(c == '.') { "Invalid char '$c' in POSIX TZ string after M$month.$week" } + c = readAsciiChar() + val dayOfWeek = when (val n = c.digitToInt()) { + 0 -> DayOfWeek.SUNDAY + else -> DayOfWeek(n) + } + val dayOfMonth: MonthDayOfYear.TransitionDay = when (week) { + 5 -> MonthDayOfYear.TransitionDay.Last(dayOfWeek, null) + else -> MonthDayOfYear.TransitionDay.Nth(dayOfWeek, week) + } + c = readAsciiChar() + MonthDayOfYear(Month(month), dayOfMonth) + } + else -> { + check(c.isDigit()) { "Invalid char '$c' in POSIX TZ string after the DST offset" } + var result = 0 + while (c.isDigit()) { + result = result * 10 + c.digitToInt() + c = readAsciiChar() + } + JulianDayOfYear(result) + } + } + } + + fun readTime(): MonthDayTime.TransitionLocaltime? { + if (c != '/') return null + c = readAsciiChar() + val hourIsNegative: Boolean + when (c) { + '-' -> { + hourIsNegative = true + c = readAsciiChar() + } + else -> { + if (!c.isDigit()) return null + hourIsNegative = false + } + } + var hour = c.digitToInt() + c = readAsciiChar() + while (c.isDigit()) { + hour = hour * 10 + c.digitToInt() + c = readAsciiChar() + } + hour *= if (hourIsNegative) -1 else 1 + if (c != ':') return MonthDayTime.TransitionLocaltime(hour, 0, 0) + val minutes = readAsciiChar().digitToInt() * 10 + readAsciiChar().digitToInt() + c = readAsciiChar() + if (c != ':') return MonthDayTime.TransitionLocaltime(hour, minutes, 0) + val seconds = readAsciiChar().digitToInt() * 10 + readAsciiChar().digitToInt() + c = readAsciiChar() + return MonthDayTime.TransitionLocaltime(hour, minutes, seconds) + } + + val std = readName() ?: return null + val stdOffset = readOffset() ?: throw IllegalArgumentException("Could not parse the std offset in POSIX TZ string") + val dst = readName() ?: return PosixTzString(std to stdOffset, null, null) + val dstOffset = readOffset() ?: UtcOffset(seconds = stdOffset.totalSeconds + 3600) + val startDate = readDate() ?: return PosixTzString(std to stdOffset, dst to dstOffset, null) + val startTime = readTime() ?: MonthDayTime.TransitionLocaltime(2, 0, 0) + val endDate = readDate() ?: throw IllegalArgumentException("Could not parse the end date in POSIX TZ string") + val endTime = readTime() ?: MonthDayTime.TransitionLocaltime(2, 0, 0) + val start = MonthDayTime(startDate, startTime, MonthDayTime.OffsetResolver.WallClockOffset) + val end = MonthDayTime(endDate, endTime, MonthDayTime.OffsetResolver.WallClockOffset) + return PosixTzString(std to stdOffset, dst to dstOffset, start to end) +} diff --git a/core/nix/src/internal/filesystem.kt b/core/nix/src/internal/filesystem.kt new file mode 100644 index 000000000..7f4574b85 --- /dev/null +++ b/core/nix/src/internal/filesystem.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2019-2023 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:OptIn(ExperimentalForeignApi::class) +package kotlinx.datetime.internal + +import kotlinx.cinterop.* +import platform.posix.* + +internal class Path(val isAbsolute: Boolean, val components: List) { + fun check(): PathInfo? = memScoped { + val stat = alloc() + val err = stat(this@Path.toString(), stat.ptr) + if (err != 0) return null + object : PathInfo { + override val isDirectory: Boolean = stat.st_mode.toInt() and S_IFMT == S_IFDIR // `inode(7)`, S_ISDIR + override val isSymlink: Boolean = stat.st_mode.toInt() and S_IFMT == S_IFLNK // `inode(7)`, S_ISLNK + } + } + + fun readLink(): Path? = memScoped { + val buffer = allocArray(PATH_MAX) + val err = readlink(this@Path.toString(), buffer, PATH_MAX.convert()) + if (err == (-1).convert()) return null + buffer[err] = 0 + fromString(buffer.toKString()) + } + + fun resolve(other: Path): Path = when { + other.isAbsolute -> other + else -> Path(isAbsolute, components + other.components) + } + + override fun toString(): String = buildString { + if (isAbsolute) append("/") + if (components.isNotEmpty()) { + for (i in 0 until components.size - 1) { + append(components[i]) + append("/") + } + append(components.last()) + } + } + + companion object { + fun fromString(path: String): Path { + val absolutePath = path.startsWith("/") + val components = path.split("/").filter { it.isNotEmpty() } + return Path(absolutePath, components) + } + } +} + +// `stat(2)` lists the other available fields +internal interface PathInfo { + val isDirectory: Boolean + val isSymlink: Boolean +} + +internal fun Path.traverseDirectory(exclude: Set = emptySet(), stripLeadingComponents: Int = this.components.size, actionOnFile: (Path) -> Unit) { + val handler = opendir(this.toString()) ?: return + try { + while (true) { + val entry = readdir(handler) ?: break + val name = entry.pointed.d_name.toKString() + if (name == "." || name == "..") continue + if (name in exclude) continue + val path = Path(isAbsolute, components + name) + val info = path.check() ?: continue // skip broken symlinks + if (info.isDirectory) { + if (!info.isSymlink) { + path.traverseDirectory(exclude, stripLeadingComponents, actionOnFile) + } + } else { + actionOnFile(Path(false, path.components.drop(stripLeadingComponents))) + } + } + } finally { + closedir(handler) + } +} + +internal fun Path.readBytes(): ByteArray { + val handler = fopen(this.toString(), "rb") ?: throw RuntimeException("Cannot open file $this") + try { + var err = fseek(handler, 0, SEEK_END) + if (err == -1) throw RuntimeException("Cannot jump to the end of $this: $errnoString") + val size = ftell(handler).convert() + if (size == -1L) throw RuntimeException("Cannot get file size for $this: $errnoString") + err = fseek(handler, 0, SEEK_SET) + if (err == -1) throw RuntimeException("Cannot jump to the start of $this: $errnoString") + val buffer = ByteArray(size.toInt()) + val readAmount = fread(buffer.refTo(0), size.convert(), 1u, handler) + check(readAmount.convert() == 1uL) { "Cannot read file $this: $errnoString" } + return buffer + } finally { + fclose(handler) + } +} + +private val errnoString + get() = strerror(errno)?.toKString() ?: "Unknown error" diff --git a/core/nix/test/TimeZoneRulesTest.kt b/core/nix/test/TimeZoneRulesTest.kt new file mode 100644 index 000000000..36850bf16 --- /dev/null +++ b/core/nix/test/TimeZoneRulesTest.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2019-2023 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.* +import kotlin.test.* + +class TimeZoneRulesTest { + @Test + fun ruleStrings() { + val rules = readTzFile(EuropeBerlinTzFile2023c).toTimeZoneRules() + // first, check that for the future, there are no explicitly defined transitions + assertTrue(rules.transitionEpochSeconds.all { + Instant.fromEpochSeconds(it) < LocalDateTime(2038, 1, 1, 0, 0).toInstant(UtcOffset.ZERO) + }) + // Next, check that even after that, the rules behave correctly. + // They are that the DST starts at 02:00 on the last Sunday in March and ends at 03:00 + // on the last Sunday in October. + val dstStartTime = LocalDateTime(2040, 3, 25, 2, 0) + val infoAtDstStart = rules.infoAtDatetime(dstStartTime) + assertTrue(infoAtDstStart is OffsetInfo.Gap, "Expected Gap, got $infoAtDstStart") + val dstEndTime = LocalDateTime(2040, 10, 28, 3, 0) + val infoAtDstEnd = rules.infoAtDatetime(dstEndTime) + assertTrue(infoAtDstEnd is OffsetInfo.Overlap, "Expected Overlap, got $infoAtDstEnd") + } + + @Test + fun ruleStringWithNonLastDayOfWeek() { + val ruleString = "AST4ADT,M3.2.0,M11.1.0\n" // Atlantic/Bermuda + val recurringRules = + PosixTzString.readIfPresent(BinaryDataReader(ruleString.encodeToByteArray()))!!.toRecurringZoneRules()!! + val rules = TimeZoneRules(UtcOffset(hours = -4), recurringRules) + val dstStartTime = LocalDateTime(2020, 3, 8, 2, 1) + val infoAtDstStart = rules.infoAtDatetime(dstStartTime) + assertTrue(infoAtDstStart is OffsetInfo.Gap, "Expected Gap, got $infoAtDstStart") + val dstEndTime = LocalDateTime(2020, 11, 1, 1, 1) + val infoAtDstEnd = rules.infoAtDatetime(dstEndTime) + assertTrue(infoAtDstEnd is OffsetInfo.Overlap, "Expected Overlap, got $infoAtDstEnd") + } +} diff --git a/core/nix/test/Util.kt b/core/nix/test/Util.kt new file mode 100644 index 000000000..069994efd --- /dev/null +++ b/core/nix/test/Util.kt @@ -0,0 +1,158 @@ +/* + * Copyright 2019-2023 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 + +// od --format=x1 --output-duplicates --address-radix=n --width=16 /usr/share/zoneinfo/Europe/Berlin | +// sed -e 's/\b\(\w\)/0x\1/g' -e 's/\(\w\)\b/\1,/g' +// Do not remove the type annotation, otherwise the compiler slows down to a crawl for this file even more. +// This constant is in a separate file to avoid recompiling it on every change to the test file, which is slow to the +// point of freezing the IDE. +internal val EuropeBerlinTzFile2023c = listOf( + 0x54, 0x5a, 0x69, 0x66, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x8f, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x12, 0x80, 0x00, 0x00, 0x00, + 0x9b, 0x0c, 0x17, 0x60, 0x9b, 0xd5, 0xda, 0xf0, 0x9c, 0xd9, 0xae, 0x90, 0x9d, 0xa4, 0xb5, 0x90, + 0x9e, 0xb9, 0x90, 0x90, 0x9f, 0x84, 0x97, 0x90, 0xc8, 0x09, 0x71, 0x90, 0xcc, 0xe7, 0x4b, 0x10, + 0xcd, 0xa9, 0x17, 0x90, 0xce, 0xa2, 0x43, 0x10, 0xcf, 0x92, 0x34, 0x10, 0xd0, 0x82, 0x25, 0x10, + 0xd1, 0x72, 0x16, 0x10, 0xd1, 0xb6, 0x96, 0x00, 0xd2, 0x58, 0xbe, 0x80, 0xd2, 0xa1, 0x4f, 0x10, + 0xd3, 0x63, 0x1b, 0x90, 0xd4, 0x4b, 0x23, 0x90, 0xd5, 0x39, 0xd1, 0x20, 0xd5, 0x67, 0xe7, 0x90, + 0xd5, 0xa8, 0x73, 0x00, 0xd6, 0x29, 0xb4, 0x10, 0xd7, 0x2c, 0x1a, 0x10, 0xd8, 0x09, 0x96, 0x10, + 0xd9, 0x02, 0xc1, 0x90, 0xd9, 0xe9, 0x78, 0x10, 0x13, 0x4d, 0x44, 0x10, 0x14, 0x33, 0xfa, 0x90, + 0x15, 0x23, 0xeb, 0x90, 0x16, 0x13, 0xdc, 0x90, 0x17, 0x03, 0xcd, 0x90, 0x17, 0xf3, 0xbe, 0x90, + 0x18, 0xe3, 0xaf, 0x90, 0x19, 0xd3, 0xa0, 0x90, 0x1a, 0xc3, 0x91, 0x90, 0x1b, 0xbc, 0xbd, 0x10, + 0x1c, 0xac, 0xae, 0x10, 0x1d, 0x9c, 0x9f, 0x10, 0x1e, 0x8c, 0x90, 0x10, 0x1f, 0x7c, 0x81, 0x10, + 0x20, 0x6c, 0x72, 0x10, 0x21, 0x5c, 0x63, 0x10, 0x22, 0x4c, 0x54, 0x10, 0x23, 0x3c, 0x45, 0x10, + 0x24, 0x2c, 0x36, 0x10, 0x25, 0x1c, 0x27, 0x10, 0x26, 0x0c, 0x18, 0x10, 0x27, 0x05, 0x43, 0x90, + 0x27, 0xf5, 0x34, 0x90, 0x28, 0xe5, 0x25, 0x90, 0x29, 0xd5, 0x16, 0x90, 0x2a, 0xc5, 0x07, 0x90, + 0x2b, 0xb4, 0xf8, 0x90, 0x2c, 0xa4, 0xe9, 0x90, 0x2d, 0x94, 0xda, 0x90, 0x2e, 0x84, 0xcb, 0x90, + 0x2f, 0x74, 0xbc, 0x90, 0x30, 0x64, 0xad, 0x90, 0x31, 0x5d, 0xd9, 0x10, 0x32, 0x72, 0xb4, 0x10, + 0x33, 0x3d, 0xbb, 0x10, 0x34, 0x52, 0x96, 0x10, 0x35, 0x1d, 0x9d, 0x10, 0x36, 0x32, 0x78, 0x10, + 0x36, 0xfd, 0x7f, 0x10, 0x38, 0x1b, 0x94, 0x90, 0x38, 0xdd, 0x61, 0x10, 0x39, 0xfb, 0x76, 0x90, + 0x3a, 0xbd, 0x43, 0x10, 0x3b, 0xdb, 0x58, 0x90, 0x3c, 0xa6, 0x5f, 0x90, 0x3d, 0xbb, 0x3a, 0x90, + 0x3e, 0x86, 0x41, 0x90, 0x3f, 0x9b, 0x1c, 0x90, 0x40, 0x66, 0x23, 0x90, 0x41, 0x84, 0x39, 0x10, + 0x42, 0x46, 0x05, 0x90, 0x43, 0x64, 0x1b, 0x10, 0x44, 0x25, 0xe7, 0x90, 0x45, 0x43, 0xfd, 0x10, + 0x46, 0x05, 0xc9, 0x90, 0x47, 0x23, 0xdf, 0x10, 0x47, 0xee, 0xe6, 0x10, 0x49, 0x03, 0xc1, 0x10, + 0x49, 0xce, 0xc8, 0x10, 0x4a, 0xe3, 0xa3, 0x10, 0x4b, 0xae, 0xaa, 0x10, 0x4c, 0xcc, 0xbf, 0x90, + 0x4d, 0x8e, 0x8c, 0x10, 0x4e, 0xac, 0xa1, 0x90, 0x4f, 0x6e, 0x6e, 0x10, 0x50, 0x8c, 0x83, 0x90, + 0x51, 0x57, 0x8a, 0x90, 0x52, 0x6c, 0x65, 0x90, 0x53, 0x37, 0x6c, 0x90, 0x54, 0x4c, 0x47, 0x90, + 0x55, 0x17, 0x4e, 0x90, 0x56, 0x2c, 0x29, 0x90, 0x56, 0xf7, 0x30, 0x90, 0x58, 0x15, 0x46, 0x10, + 0x58, 0xd7, 0x12, 0x90, 0x59, 0xf5, 0x28, 0x10, 0x5a, 0xb6, 0xf4, 0x90, 0x5b, 0xd5, 0x0a, 0x10, + 0x5c, 0xa0, 0x11, 0x10, 0x5d, 0xb4, 0xec, 0x10, 0x5e, 0x7f, 0xf3, 0x10, 0x5f, 0x94, 0xce, 0x10, + 0x60, 0x5f, 0xd5, 0x10, 0x61, 0x7d, 0xea, 0x90, 0x62, 0x3f, 0xb7, 0x10, 0x63, 0x5d, 0xcc, 0x90, + 0x64, 0x1f, 0x99, 0x10, 0x65, 0x3d, 0xae, 0x90, 0x66, 0x08, 0xb5, 0x90, 0x67, 0x1d, 0x90, 0x90, + 0x67, 0xe8, 0x97, 0x90, 0x68, 0xfd, 0x72, 0x90, 0x69, 0xc8, 0x79, 0x90, 0x6a, 0xdd, 0x54, 0x90, + 0x6b, 0xa8, 0x5b, 0x90, 0x6c, 0xc6, 0x71, 0x10, 0x6d, 0x88, 0x3d, 0x90, 0x6e, 0xa6, 0x53, 0x10, + 0x6f, 0x68, 0x1f, 0x90, 0x70, 0x86, 0x35, 0x10, 0x71, 0x51, 0x3c, 0x10, 0x72, 0x66, 0x17, 0x10, + 0x73, 0x31, 0x1e, 0x10, 0x74, 0x45, 0xf9, 0x10, 0x75, 0x11, 0x00, 0x10, 0x76, 0x2f, 0x15, 0x90, + 0x76, 0xf0, 0xe2, 0x10, 0x78, 0x0e, 0xf7, 0x90, 0x78, 0xd0, 0xc4, 0x10, 0x79, 0xee, 0xd9, 0x90, + 0x7a, 0xb0, 0xa6, 0x10, 0x7b, 0xce, 0xbb, 0x90, 0x7c, 0x99, 0xc2, 0x90, 0x7d, 0xae, 0x9d, 0x90, + 0x7e, 0x79, 0xa4, 0x90, 0x7f, 0x8e, 0x7f, 0x90, 0x02, 0x01, 0x02, 0x03, 0x04, 0x03, 0x04, 0x03, + 0x04, 0x03, 0x04, 0x03, 0x04, 0x03, 0x05, 0x01, 0x04, 0x03, 0x04, 0x03, 0x06, 0x01, 0x04, 0x03, + 0x04, 0x03, 0x04, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, + 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, + 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, + 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, + 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, + 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, + 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, + 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x00, 0x00, 0x0c, 0x88, 0x00, 0x00, 0x00, 0x00, 0x1c, + 0x20, 0x01, 0x04, 0x00, 0x00, 0x0e, 0x10, 0x00, 0x09, 0x00, 0x00, 0x1c, 0x20, 0x01, 0x04, 0x00, + 0x00, 0x0e, 0x10, 0x00, 0x09, 0x00, 0x00, 0x2a, 0x30, 0x01, 0x0d, 0x00, 0x00, 0x2a, 0x30, 0x01, + 0x0d, 0x00, 0x00, 0x1c, 0x20, 0x01, 0x04, 0x00, 0x00, 0x0e, 0x10, 0x00, 0x09, 0x4c, 0x4d, 0x54, + 0x00, 0x43, 0x45, 0x53, 0x54, 0x00, 0x43, 0x45, 0x54, 0x00, 0x43, 0x45, 0x4d, 0x54, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x01, 0x54, 0x5a, 0x69, 0x66, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x8f, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x12, 0xff, 0xff, 0xff, + 0xff, 0x6f, 0xa2, 0x61, 0xf8, 0xff, 0xff, 0xff, 0xff, 0x9b, 0x0c, 0x17, 0x60, 0xff, 0xff, 0xff, + 0xff, 0x9b, 0xd5, 0xda, 0xf0, 0xff, 0xff, 0xff, 0xff, 0x9c, 0xd9, 0xae, 0x90, 0xff, 0xff, 0xff, + 0xff, 0x9d, 0xa4, 0xb5, 0x90, 0xff, 0xff, 0xff, 0xff, 0x9e, 0xb9, 0x90, 0x90, 0xff, 0xff, 0xff, + 0xff, 0x9f, 0x84, 0x97, 0x90, 0xff, 0xff, 0xff, 0xff, 0xc8, 0x09, 0x71, 0x90, 0xff, 0xff, 0xff, + 0xff, 0xcc, 0xe7, 0x4b, 0x10, 0xff, 0xff, 0xff, 0xff, 0xcd, 0xa9, 0x17, 0x90, 0xff, 0xff, 0xff, + 0xff, 0xce, 0xa2, 0x43, 0x10, 0xff, 0xff, 0xff, 0xff, 0xcf, 0x92, 0x34, 0x10, 0xff, 0xff, 0xff, + 0xff, 0xd0, 0x82, 0x25, 0x10, 0xff, 0xff, 0xff, 0xff, 0xd1, 0x72, 0x16, 0x10, 0xff, 0xff, 0xff, + 0xff, 0xd1, 0xb6, 0x96, 0x00, 0xff, 0xff, 0xff, 0xff, 0xd2, 0x58, 0xbe, 0x80, 0xff, 0xff, 0xff, + 0xff, 0xd2, 0xa1, 0x4f, 0x10, 0xff, 0xff, 0xff, 0xff, 0xd3, 0x63, 0x1b, 0x90, 0xff, 0xff, 0xff, + 0xff, 0xd4, 0x4b, 0x23, 0x90, 0xff, 0xff, 0xff, 0xff, 0xd5, 0x39, 0xd1, 0x20, 0xff, 0xff, 0xff, + 0xff, 0xd5, 0x67, 0xe7, 0x90, 0xff, 0xff, 0xff, 0xff, 0xd5, 0xa8, 0x73, 0x00, 0xff, 0xff, 0xff, + 0xff, 0xd6, 0x29, 0xb4, 0x10, 0xff, 0xff, 0xff, 0xff, 0xd7, 0x2c, 0x1a, 0x10, 0xff, 0xff, 0xff, + 0xff, 0xd8, 0x09, 0x96, 0x10, 0xff, 0xff, 0xff, 0xff, 0xd9, 0x02, 0xc1, 0x90, 0xff, 0xff, 0xff, + 0xff, 0xd9, 0xe9, 0x78, 0x10, 0x00, 0x00, 0x00, 0x00, 0x13, 0x4d, 0x44, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x14, 0x33, 0xfa, 0x90, 0x00, 0x00, 0x00, 0x00, 0x15, 0x23, 0xeb, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x16, 0x13, 0xdc, 0x90, 0x00, 0x00, 0x00, 0x00, 0x17, 0x03, 0xcd, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x17, 0xf3, 0xbe, 0x90, 0x00, 0x00, 0x00, 0x00, 0x18, 0xe3, 0xaf, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x19, 0xd3, 0xa0, 0x90, 0x00, 0x00, 0x00, 0x00, 0x1a, 0xc3, 0x91, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x1b, 0xbc, 0xbd, 0x10, 0x00, 0x00, 0x00, 0x00, 0x1c, 0xac, 0xae, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x1d, 0x9c, 0x9f, 0x10, 0x00, 0x00, 0x00, 0x00, 0x1e, 0x8c, 0x90, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x1f, 0x7c, 0x81, 0x10, 0x00, 0x00, 0x00, 0x00, 0x20, 0x6c, 0x72, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x21, 0x5c, 0x63, 0x10, 0x00, 0x00, 0x00, 0x00, 0x22, 0x4c, 0x54, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x23, 0x3c, 0x45, 0x10, 0x00, 0x00, 0x00, 0x00, 0x24, 0x2c, 0x36, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x25, 0x1c, 0x27, 0x10, 0x00, 0x00, 0x00, 0x00, 0x26, 0x0c, 0x18, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x27, 0x05, 0x43, 0x90, 0x00, 0x00, 0x00, 0x00, 0x27, 0xf5, 0x34, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x28, 0xe5, 0x25, 0x90, 0x00, 0x00, 0x00, 0x00, 0x29, 0xd5, 0x16, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x2a, 0xc5, 0x07, 0x90, 0x00, 0x00, 0x00, 0x00, 0x2b, 0xb4, 0xf8, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x2c, 0xa4, 0xe9, 0x90, 0x00, 0x00, 0x00, 0x00, 0x2d, 0x94, 0xda, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x2e, 0x84, 0xcb, 0x90, 0x00, 0x00, 0x00, 0x00, 0x2f, 0x74, 0xbc, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x30, 0x64, 0xad, 0x90, 0x00, 0x00, 0x00, 0x00, 0x31, 0x5d, 0xd9, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x32, 0x72, 0xb4, 0x10, 0x00, 0x00, 0x00, 0x00, 0x33, 0x3d, 0xbb, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x34, 0x52, 0x96, 0x10, 0x00, 0x00, 0x00, 0x00, 0x35, 0x1d, 0x9d, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x36, 0x32, 0x78, 0x10, 0x00, 0x00, 0x00, 0x00, 0x36, 0xfd, 0x7f, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x38, 0x1b, 0x94, 0x90, 0x00, 0x00, 0x00, 0x00, 0x38, 0xdd, 0x61, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x39, 0xfb, 0x76, 0x90, 0x00, 0x00, 0x00, 0x00, 0x3a, 0xbd, 0x43, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x3b, 0xdb, 0x58, 0x90, 0x00, 0x00, 0x00, 0x00, 0x3c, 0xa6, 0x5f, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x3d, 0xbb, 0x3a, 0x90, 0x00, 0x00, 0x00, 0x00, 0x3e, 0x86, 0x41, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x3f, 0x9b, 0x1c, 0x90, 0x00, 0x00, 0x00, 0x00, 0x40, 0x66, 0x23, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x41, 0x84, 0x39, 0x10, 0x00, 0x00, 0x00, 0x00, 0x42, 0x46, 0x05, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x43, 0x64, 0x1b, 0x10, 0x00, 0x00, 0x00, 0x00, 0x44, 0x25, 0xe7, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x45, 0x43, 0xfd, 0x10, 0x00, 0x00, 0x00, 0x00, 0x46, 0x05, 0xc9, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x47, 0x23, 0xdf, 0x10, 0x00, 0x00, 0x00, 0x00, 0x47, 0xee, 0xe6, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x49, 0x03, 0xc1, 0x10, 0x00, 0x00, 0x00, 0x00, 0x49, 0xce, 0xc8, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x4a, 0xe3, 0xa3, 0x10, 0x00, 0x00, 0x00, 0x00, 0x4b, 0xae, 0xaa, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x4c, 0xcc, 0xbf, 0x90, 0x00, 0x00, 0x00, 0x00, 0x4d, 0x8e, 0x8c, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x4e, 0xac, 0xa1, 0x90, 0x00, 0x00, 0x00, 0x00, 0x4f, 0x6e, 0x6e, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x50, 0x8c, 0x83, 0x90, 0x00, 0x00, 0x00, 0x00, 0x51, 0x57, 0x8a, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x52, 0x6c, 0x65, 0x90, 0x00, 0x00, 0x00, 0x00, 0x53, 0x37, 0x6c, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x54, 0x4c, 0x47, 0x90, 0x00, 0x00, 0x00, 0x00, 0x55, 0x17, 0x4e, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x56, 0x2c, 0x29, 0x90, 0x00, 0x00, 0x00, 0x00, 0x56, 0xf7, 0x30, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x58, 0x15, 0x46, 0x10, 0x00, 0x00, 0x00, 0x00, 0x58, 0xd7, 0x12, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x59, 0xf5, 0x28, 0x10, 0x00, 0x00, 0x00, 0x00, 0x5a, 0xb6, 0xf4, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x5b, 0xd5, 0x0a, 0x10, 0x00, 0x00, 0x00, 0x00, 0x5c, 0xa0, 0x11, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x5d, 0xb4, 0xec, 0x10, 0x00, 0x00, 0x00, 0x00, 0x5e, 0x7f, 0xf3, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x5f, 0x94, 0xce, 0x10, 0x00, 0x00, 0x00, 0x00, 0x60, 0x5f, 0xd5, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x61, 0x7d, 0xea, 0x90, 0x00, 0x00, 0x00, 0x00, 0x62, 0x3f, 0xb7, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x63, 0x5d, 0xcc, 0x90, 0x00, 0x00, 0x00, 0x00, 0x64, 0x1f, 0x99, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x65, 0x3d, 0xae, 0x90, 0x00, 0x00, 0x00, 0x00, 0x66, 0x08, 0xb5, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x67, 0x1d, 0x90, 0x90, 0x00, 0x00, 0x00, 0x00, 0x67, 0xe8, 0x97, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x68, 0xfd, 0x72, 0x90, 0x00, 0x00, 0x00, 0x00, 0x69, 0xc8, 0x79, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x6a, 0xdd, 0x54, 0x90, 0x00, 0x00, 0x00, 0x00, 0x6b, 0xa8, 0x5b, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x6c, 0xc6, 0x71, 0x10, 0x00, 0x00, 0x00, 0x00, 0x6d, 0x88, 0x3d, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x6e, 0xa6, 0x53, 0x10, 0x00, 0x00, 0x00, 0x00, 0x6f, 0x68, 0x1f, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x70, 0x86, 0x35, 0x10, 0x00, 0x00, 0x00, 0x00, 0x71, 0x51, 0x3c, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x72, 0x66, 0x17, 0x10, 0x00, 0x00, 0x00, 0x00, 0x73, 0x31, 0x1e, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x74, 0x45, 0xf9, 0x10, 0x00, 0x00, 0x00, 0x00, 0x75, 0x11, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x76, 0x2f, 0x15, 0x90, 0x00, 0x00, 0x00, 0x00, 0x76, 0xf0, 0xe2, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x78, 0x0e, 0xf7, 0x90, 0x00, 0x00, 0x00, 0x00, 0x78, 0xd0, 0xc4, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x79, 0xee, 0xd9, 0x90, 0x00, 0x00, 0x00, 0x00, 0x7a, 0xb0, 0xa6, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x7b, 0xce, 0xbb, 0x90, 0x00, 0x00, 0x00, 0x00, 0x7c, 0x99, 0xc2, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x7d, 0xae, 0x9d, 0x90, 0x00, 0x00, 0x00, 0x00, 0x7e, 0x79, 0xa4, 0x90, 0x00, 0x00, 0x00, + 0x00, 0x7f, 0x8e, 0x7f, 0x90, 0x02, 0x01, 0x02, 0x03, 0x04, 0x03, 0x04, 0x03, 0x04, 0x03, 0x04, + 0x03, 0x04, 0x03, 0x05, 0x01, 0x04, 0x03, 0x04, 0x03, 0x06, 0x01, 0x04, 0x03, 0x04, 0x03, 0x04, + 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, + 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, + 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, + 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, + 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, + 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, + 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, 0x07, 0x08, + 0x07, 0x08, 0x07, 0x08, 0x00, 0x00, 0x0c, 0x88, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x20, 0x01, 0x04, + 0x00, 0x00, 0x0e, 0x10, 0x00, 0x09, 0x00, 0x00, 0x1c, 0x20, 0x01, 0x04, 0x00, 0x00, 0x0e, 0x10, + 0x00, 0x09, 0x00, 0x00, 0x2a, 0x30, 0x01, 0x0d, 0x00, 0x00, 0x2a, 0x30, 0x01, 0x0d, 0x00, 0x00, + 0x1c, 0x20, 0x01, 0x04, 0x00, 0x00, 0x0e, 0x10, 0x00, 0x09, 0x4c, 0x4d, 0x54, 0x00, 0x43, 0x45, + 0x53, 0x54, 0x00, 0x43, 0x45, 0x54, 0x00, 0x43, 0x45, 0x4d, 0x54, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x01, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x0a, 0x43, + 0x45, 0x54, 0x2d, 0x31, 0x43, 0x45, 0x53, 0x54, 0x2c, 0x4d, 0x33, 0x2e, 0x35, 0x2e, 0x30, 0x2c, + 0x4d, 0x31, 0x30, 0x2e, 0x35, 0x2e, 0x30, 0x2f, 0x33, 0x0a, +).map { it.toByte() }.toByteArray() From d86065c0edb3fdf636bd3dd46a597c7df6579ac3 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Thu, 30 Mar 2023 16:05:30 +0200 Subject: [PATCH 2/2] Rewrite the Windows code in pure Kotlin --- .gitmodules | 3 - README.md | 9 - core/build.gradle.kts | 53 +- core/common/test/TimeZoneTest.kt | 12 +- core/linux/test/TimeZoneRulesCompleteTest.kt | 1 - core/native/cinterop/cpp/windows.cpp | 437 ------- core/native/cinterop/date.def | 5 - core/native/cinterop/public/cdate.h | 44 - core/native/cinterop/public/helper_macros.hpp | 18 - core/native/cinterop/public/windows_zones.hpp | 1066 ----------------- .../native/cinterop_actuals/TimeZoneNative.kt | 100 -- core/windows/cinterop/definitions.def | 1 + core/windows/cinterop/definitions.h | 11 + core/windows/src/TimeZoneNative.kt | 72 ++ core/windows/src/TzdbInRegistry.kt | 215 ++++ core/windows/src/WindowsZoneNames.kt | 606 ++++++++++ thirdparty/date | 1 - 17 files changed, 928 insertions(+), 1726 deletions(-) delete mode 100644 core/native/cinterop/cpp/windows.cpp delete mode 100644 core/native/cinterop/date.def delete mode 100644 core/native/cinterop/public/cdate.h delete mode 100644 core/native/cinterop/public/helper_macros.hpp delete mode 100644 core/native/cinterop/public/windows_zones.hpp delete mode 100644 core/native/cinterop_actuals/TimeZoneNative.kt create mode 100644 core/windows/cinterop/definitions.def create mode 100644 core/windows/cinterop/definitions.h create mode 100644 core/windows/src/TimeZoneNative.kt create mode 100644 core/windows/src/TzdbInRegistry.kt create mode 100644 core/windows/src/WindowsZoneNames.kt delete mode 160000 thirdparty/date diff --git a/.gitmodules b/.gitmodules index 08e9272ef..e69de29bb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "date-cpp-library/date"] - path = thirdparty/date - url = https://github.com/HowardHinnant/date diff --git a/README.md b/README.md index 021d6a38a..428213291 100644 --- a/README.md +++ b/README.md @@ -381,15 +381,6 @@ Add a dependency to the `` element. Note that you need to use the ## Building -Before building, ensure that you have [thirdparty/date](thirdparty/date) submodule initialized and updated. -IDEA does that automatically when cloning the repository, and if you cloned it in the command line, you may need -to run additionally: - -```kotlin -git submodule init -git submodule update -``` - The project requires JDK 8 to build classes and to run tests. Gradle will try to find it among the installed JDKs or [provision](https://docs.gradle.org/current/userguide/toolchains.html#sec:provisioning) it automatically if it couldn't be found. The path to JDK 8 can be additionally specified with the environment variable `JDK_8`. diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 5b9417801..97b2cc090 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -74,7 +74,9 @@ kotlin { } } // Tier 3 - target("mingwX64") + common("windows") { + target("mingwX64") + } } jvm { @@ -143,24 +145,11 @@ kotlin { when { konanTarget.family == org.jetbrains.kotlin.konan.target.Family.MINGW -> { compilations["main"].cinterops { - create("date") { - val cinteropDir = "$projectDir/native/cinterop" - val dateLibDir = "${project(":").projectDir}/thirdparty/date" - headers("$cinteropDir/public/cdate.h") - defFile("native/cinterop/date.def") - extraOpts("-Xsource-compiler-option", "-I$cinteropDir/public") - extraOpts("-Xsource-compiler-option", "-DONLY_C_LOCALE=1") - // needed to be able to use std::shared_mutex to implement caching. - extraOpts("-Xsource-compiler-option", "-std=c++17") - // the date library headers, needed for some pure calculations. - extraOpts("-Xsource-compiler-option", "-I$dateLibDir/include") - // the main source for the platform bindings. - extraOpts("-Xcompile-source", "$cinteropDir/cpp/windows.cpp") + create("declarations") { + defFile("$projectDir/windows/cinterop/definitions.def") + headers("$projectDir/windows/cinterop/definitions.h") } } - compilations["main"].defaultSourceSet { - kotlin.srcDir("native/cinterop_actuals") - } } konanTarget.family == org.jetbrains.kotlin.konan.target.Family.LINUX -> { // do nothing special @@ -173,8 +162,6 @@ kotlin { } } } - - sourceSets { commonMain { dependencies { @@ -321,10 +308,10 @@ tasks { val downloadWindowsZonesMapping by tasks.registering { description = "Updates the mapping between Windows-specific and usual names for timezones" - val output = "$projectDir/native/cinterop/public/windows_zones.hpp" - val initialFileContents = File(output).readBytes() + val output = "$projectDir/windows/src/WindowsZoneNames.kt" outputs.file(output) doLast { + val initialFileContents = try { File(output).readBytes() } catch(e: Throwable) { ByteArray(0) } val documentBuilderFactory = DocumentBuilderFactory.newInstance() // otherwise, parsing fails since it can't find the dtd documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false) @@ -336,11 +323,13 @@ val downloadWindowsZonesMapping by tasks.registering { xmlDoc.documentElement.normalize() val mapZones = xmlDoc.getElementsByTagName("mapZone") val mapping = linkedMapOf() + mapping["UTC"] = "UTC" for (i in 0 until mapZones.length) { val mapZone = mapZones.item(i) val windowsName = mapZone.attributes.getNamedItem("other").nodeValue val usualNames = mapZone.attributes.getNamedItem("type").nodeValue for (usualName in usualNames.split(' ')) { + if (usualName == "") continue val oldWindowsName = mapping[usualName] // don't do it in `put` to preserve the order in the map if (oldWindowsName == null) { mapping[usualName] = windowsName @@ -353,14 +342,13 @@ val downloadWindowsZonesMapping by tasks.registering { val bos = ByteArrayOutputStream() PrintWriter(bos).use { out -> out.println("""// generated with gradle task `$name`""") - out.println("""#include """) - out.println("""#include """) - out.println("""static const std::unordered_map standard_to_windows = {""") + out.println("""package kotlinx.datetime""") + out.println("""internal val standardToWindows: Map = mutableMapOf(""") for ((usualName, windowsName) in sortedMapping) { - out.println("\t{ \"$usualName\", \"$windowsName\" },") + out.println(" \"$usualName\" to \"$windowsName\",") } - out.println("};") - out.println("""static const std::unordered_map windows_to_standard = {""") + out.println(")") + out.println("""internal val windowsToStandard: Map = mutableMapOf(""") val reverseMap = sortedMapOf() for ((usualName, windowsName) in mapping) { if (reverseMap[windowsName] == null) { @@ -368,16 +356,9 @@ val downloadWindowsZonesMapping by tasks.registering { } } for ((windowsName, usualName) in reverseMap) { - out.println("\t{ \"$windowsName\", \"$usualName\" },") - } - out.println("};") - out.println("""static const std::unordered_map zone_ids = {""") - var i = 0 - for ((usualName, windowsName) in sortedMapping) { - out.println("\t{ \"$usualName\", $i },") - ++i + out.println(" \"$windowsName\" to \"$usualName\",") } - out.println("};") + out.println(")") } val newFileContents = bos.toByteArray() if (!(initialFileContents contentEquals newFileContents)) { diff --git a/core/common/test/TimeZoneTest.kt b/core/common/test/TimeZoneTest.kt index 4116aaca8..21f4618b0 100644 --- a/core/common/test/TimeZoneTest.kt +++ b/core/common/test/TimeZoneTest.kt @@ -35,13 +35,13 @@ class TimeZoneTest { @Test fun available() { val allTzIds = TimeZone.availableZoneIds - assertContains(allTzIds, "Europe/Berlin") - assertContains(allTzIds, "Europe/Moscow") - assertContains(allTzIds, "America/New_York") + assertContains(allTzIds, "Europe/Berlin", "Europe/Berlin not in $allTzIds") + assertContains(allTzIds, "Europe/Moscow", "Europe/Moscow not in $allTzIds") + assertContains(allTzIds, "America/New_York", "America/New_York not in $allTzIds") - assertNotEquals(0, allTzIds.size) - assertTrue(TimeZone.currentSystemDefault().id in allTzIds) - assertTrue("UTC" in allTzIds) + assertTrue(TimeZone.currentSystemDefault().id in allTzIds, + "The current system timezone ${TimeZone.currentSystemDefault().id} is not in $allTzIds") + assertTrue("UTC" in allTzIds, "The UTC timezone not in $allTzIds") } @Test diff --git a/core/linux/test/TimeZoneRulesCompleteTest.kt b/core/linux/test/TimeZoneRulesCompleteTest.kt index fb509da13..e7b6a5c84 100644 --- a/core/linux/test/TimeZoneRulesCompleteTest.kt +++ b/core/linux/test/TimeZoneRulesCompleteTest.kt @@ -113,7 +113,6 @@ fun parseRfc2822(input: String): Pair { val abbreviation = input.substringAfterLast(" ") val dateTime = input.substringBeforeLast(" ") val components = dateTime.split(Regex(" +")) - val dayOfWeek = components[0] val month = components[1] val dayOfMonth = components[2].toInt() val time = LocalTime.parse(components[3]) diff --git a/core/native/cinterop/cpp/windows.cpp b/core/native/cinterop/cpp/windows.cpp deleted file mode 100644 index baabb29ac..000000000 --- a/core/native/cinterop/cpp/windows.cpp +++ /dev/null @@ -1,437 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. - * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. - */ -/* This file implements the functions specified in `cdate.h` for Windows. */ -/* only Windows 8 and later is supported. This is needed for - `EnumDynamicTimeZoneInformation` to be available. */ -#define _WIN32_WINNT _WIN32_WINNT_WIN8 -// avoid bloat from including `Windows.h`. -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif - -#include -#include -#include -#include -#include -#include -#ifdef DEBUG -#include -#endif -#include -#include -#include -#include "date/date.h" -#include "helper_macros.hpp" -#include "windows_zones.hpp" -extern "C" { -#include "cdate.h" -} - -/* The maximum length of the registry key name for timezones. Taken from - https://docs.microsoft.com/en-us/windows/win32/api/timezoneapi/ns-timezoneapi-dynamic_time_zone_information - */ -#define MAX_KEY_LENGTH 128 - -// The amount of time the cache is considered up-to-date. -#define CACHE_INVALIDATION_TIMEOUT std::chrono::minutes(5) - -/* Taken from the `date` library, function `getTimeZoneKeyName()`. - Gets the `std::string` representation of a time zone registry key name. - Throws if the registry key is malformed and has a key longer than - `MAX_KEY_LENGTH` */ -static std::string key_to_string(const DYNAMIC_TIME_ZONE_INFORMATION& dtzi) { - auto wlen = wcslen(dtzi.TimeZoneKeyName); - char buf[MAX_KEY_LENGTH] = {}; - if (sizeof(buf) < wlen+1) { - // Can only happen if something is terribly broken. - throw std::runtime_error("Anomalously long timezone registry key"); - } - wcstombs(buf, dtzi.TimeZoneKeyName, wlen); - if (strcmp(buf, "Coordinated Universal Time") == 0) - return "UTC"; - // Allocates std::string. - return buf; -} - -/* Finds the unique number assigned to each standard name. */ -static TZID id_by_name(const std::string& name) -{ - try { - return zone_ids.at(name); - } catch (std::out_of_range e) { - return TZID_INVALID; - } -} - -/* Returns a standard timezone name given a Windows registry key name. - The returned C string is guaranteed to have static lifetime. */ -static const char *native_name_to_standard_name(const std::string& native) { - // inspired by `native_to_standard_timezone_name` from the `date` library - if (native == "UTC") { - // string literals have static lifetime. - return "Etc/UTC"; - } - try { - /* `windows_to_standard` is immutable, so its contents can't - become invalidated. */ - return windows_to_standard.at(native).c_str(); - } catch (std::out_of_range e) { - return nullptr; - } -} - -// The next time the timezone cache should be flushed. -static auto next_flush = std::chrono::time_point::min(); -// The timezone cache. Access to it should be guarded with `cache_rwlock`. -static std::unordered_map< - TZID, DYNAMIC_TIME_ZONE_INFORMATION> cache; -// The read-write lock guarding access to the cache. -static std::shared_mutex cache_rwlock; - -// Updates the timezone cache if it's time to do so. -static void repopulate_timezone_cache( - std::chrono::time_point current_time) -{ - const std::lock_guard lock(cache_rwlock); - if (current_time < next_flush) { - return; - } - cache.clear(); - std::unordered_map - native_to_zones; - DYNAMIC_TIME_ZONE_INFORMATION dtzi{}; - next_flush = current_time + CACHE_INVALIDATION_TIMEOUT; - for (DWORD dwResult = 0, i = 0; dwResult != ERROR_NO_MORE_ITEMS; ++i) { - dwResult = EnumDynamicTimeZoneInformation(i, &dtzi); - if (dwResult == ERROR_SUCCESS) { - native_to_zones[key_to_string(dtzi)] = dtzi; - } - } - for (auto it = standard_to_windows.begin(); - it != standard_to_windows.end(); ++it) - { - try { - auto& dtzi = native_to_zones.at(it->second); - auto id = id_by_name(it->first); - cache[id] = dtzi; - } catch (std::out_of_range e) { - } - } -} - -/* Populates `dtzi` with the time zone information for standard timezone name - `name`. Returns `false` if the name is invalid. */ -static bool time_zone_by_id( - TZID id, DYNAMIC_TIME_ZONE_INFORMATION& dtzi) -{ - try { - const auto current_time = std::chrono::steady_clock::now(); - if (current_time > next_flush) { - repopulate_timezone_cache(current_time); - } - const std::shared_lock lock(cache_rwlock); - dtzi = cache.at(id); - return true; - } catch (std::out_of_range e) { - return false; - } -} - -/* this code is explained at -https://docs.microsoft.com/en-us/windows/win32/api/timezoneapi/ns-timezoneapi-time_zone_information -in the section about `StandardDate`. -In short, the `StandardDate` structure uses `SYSTEMTIME` in a... -non-conventional way. This function translates that representation to one -representing a proper date at a given year. -*/ -static void get_transition_date(int year, const SYSTEMTIME& src, SYSTEMTIME& dst) -{ - dst = src; - // if the year is not 0, this is the absolute time. - if (src.wYear != 0) { - return; - } - /* otherwise, the transition happens yearly at the specified month, hour, - and minute at the specified day of the week. */ - dst.wYear = year; - // The number of the occurrence of the specified day of week in the month, - // or the special value "5" to denote the last such occurrence. - unsigned int dowOccurrenceNumber = src.wDay; - // lastly, we find the real date that corresponds to the nth occurrence. - date::sys_days days = dowOccurrenceNumber == 5 ? - date::sys_days( - date::year_month_weekday_last(date::year(year), date::month(src.wMonth), - date::weekday_last{date::weekday{src.wDayOfWeek}})) : - date::sys_days( - date::year_month_weekday(date::year(year), date::month(src.wMonth), - date::weekday_indexed{ - date::weekday{src.wDayOfWeek}, dowOccurrenceNumber})); - auto date = date::year_month_day(days); - dst.wDay = (unsigned int)date.day(); -} - -#ifdef DEBUG -static void printSystime(const SYSTEMTIME& time) -{ - std::cout << time.wYear << "/" << time.wMonth << "/" << - time.wDay << " (" << time.wDayOfWeek << ") " << - time.wHour << ":" << time.wMinute << ":" << time.wSecond; -} -#endif - -#define SECS_BETWEEN_1601_1970 11644473600LL -#define WINDOWS_TICKS_PER_SEC 10000000 - -static void unix_time_to_systemtime(int64_t epoch_sec, SYSTEMTIME& systime) -{ - int64_t windows_ticks = (epoch_sec + SECS_BETWEEN_1601_1970) - * WINDOWS_TICKS_PER_SEC; - ULARGE_INTEGER li; - li.QuadPart = windows_ticks; - FILETIME ft { li.LowPart, li.HighPart }; - FileTimeToSystemTime(&ft, &systime); -} - -static int64_t systemtime_to_ticks(const SYSTEMTIME& systime) -{ - FILETIME ft {0, 0}; - SystemTimeToFileTime(&systime, &ft); - ULARGE_INTEGER li; - li.LowPart = ft.dwLowDateTime; - li.HighPart = ft.dwHighDateTime; - return li.QuadPart; -} - -static int64_t systemtime_to_unix_time(const SYSTEMTIME& systime) -{ - return systemtime_to_ticks(systime) / WINDOWS_TICKS_PER_SEC - - SECS_BETWEEN_1601_1970; -} - -struct TRANSITIONS_INFO { - TIME_ZONE_INFORMATION tzi; - SYSTEMTIME standard_local; - SYSTEMTIME daylight_local; -}; - -/* Checks whether the daylight saving time is in effect at the given time. - `tzi` could be calculated here, but is passed along to avoid recomputing - it. */ -static bool is_daylight_time( - const DYNAMIC_TIME_ZONE_INFORMATION& dtzi, - TRANSITIONS_INFO& trans, - const SYSTEMTIME& time) -{ - // it means that daylight saving time is not supported at all - if (trans.tzi.StandardDate.wMonth == 0) { - return false; - } - /* translate the "date" values stored in `tzi` into real dates of - transitions to and from the daylight saving time. */ - get_transition_date(time.wYear, trans.tzi.StandardDate, trans.standard_local); - get_transition_date(time.wYear, trans.tzi.DaylightDate, trans.daylight_local); - /* Two things happen here: - * All the relevant dates are converted to a number of ticks an some - unified scale, counted in seconds. This is done so that we are able - to easily add to and compare between dates. - * `standard_local` and `daylight_local` are represented as dates in the - local time that was active *just before* the transition. For example, - `standard_local` contains the date of the transition to the standard - time, as seen by a person that is currently on the daylight saving - time. So, in order for the dates to be on the same scale, the biases - that are assumed to be currently active are negated. */ - int64_t standard = systemtime_to_ticks(trans.standard_local) / - WINDOWS_TICKS_PER_SEC + (trans.tzi.Bias + trans.tzi.DaylightBias) * 60; - int64_t daylight = systemtime_to_ticks(trans.daylight_local) / - WINDOWS_TICKS_PER_SEC + (trans.tzi.Bias + trans.tzi.StandardBias) * 60; - int64_t time_secs = systemtime_to_ticks(time) / - WINDOWS_TICKS_PER_SEC; - /* Maybe `else` is never hit, but I've seen no indication of that assumption - in the documentation. */ - if (daylight < standard) { - // The year is |STANDARD|DAYLIGHT|STANDARD| - return time_secs < standard && time_secs >= daylight; - } else { - // The year is |DAYLIGHT|STANDARD|DAYLIGHT| - return time_secs < standard || time_secs >= daylight; - } -} - -// Get the UTC offset for a given timezone at a given time. -static int offset_at_systime(DYNAMIC_TIME_ZONE_INFORMATION& dtzi, - TRANSITIONS_INFO& ts, - const SYSTEMTIME& systime) -{ - bool result = GetTimeZoneInformationForYear(systime.wYear, &dtzi, &ts.tzi); - if (!result) { - return INT_MAX; - } - auto bias = ts.tzi.Bias; - if (is_daylight_time(dtzi, ts, systime)) { - bias += ts.tzi.DaylightBias; - } else { - bias += ts.tzi.StandardBias; - } - return -bias * 60; -} - -extern "C" { - -bool current_time(int64_t *sec, int32_t *nano) -{ - timespec tm; - int error = clock_gettime(CLOCK_REALTIME, &tm); - if (error) { - return false; - } - *sec = tm.tv_sec; - *nano = tm.tv_nsec; - return true; -} - -char * get_system_timezone(TZID* id) -{ - DYNAMIC_TIME_ZONE_INFORMATION dtzi{}; - auto result = GetDynamicTimeZoneInformation(&dtzi); - if (result == TIME_ZONE_ID_INVALID) - return nullptr; - auto key = key_to_string(dtzi); - auto name = native_name_to_standard_name(key); - if (name == nullptr) { - *id = TZID_INVALID; - return nullptr; - } else { - *id = id_by_name(name); - return check_allocation(strdup(name)); - } -} - -char ** available_zone_ids() -{ - std::set known_native_names, known_ids; - DYNAMIC_TIME_ZONE_INFORMATION dtzi{}; - for (DWORD dwResult = 0, i = 0; dwResult != ERROR_NO_MORE_ITEMS; ++i) { - dwResult = EnumDynamicTimeZoneInformation(i, &dtzi); - if (dwResult == ERROR_SUCCESS) { - known_native_names.insert(key_to_string(dtzi)); - } - } - for (auto it = standard_to_windows.begin(); - it != standard_to_windows.end(); ++it) - { - if (known_native_names.count(it->second)) { - known_ids.insert(it->first); - } - } - char ** zones = check_allocation( - (char **)malloc(sizeof(char *) * (known_ids.size() + 1))); - zones[known_ids.size()] = nullptr; - unsigned int i = 0; - for (auto it = known_ids.begin(); it != known_ids.end(); ++it) { - zones[i] = check_allocation(strdup(it->c_str())); - ++i; - } - return zones; -} - -int offset_at_instant(TZID zone_id, int64_t epoch_sec) -{ - DYNAMIC_TIME_ZONE_INFORMATION dtzi{}; - bool result = time_zone_by_id(zone_id, dtzi); - if (!result) { - return INT_MAX; - } - SYSTEMTIME systime; - unix_time_to_systemtime(epoch_sec, systime); - TRANSITIONS_INFO ts{}; - return offset_at_systime(dtzi, ts, systime); -} - -TZID timezone_by_name(const char *zone_name) -{ - DYNAMIC_TIME_ZONE_INFORMATION dtzi{}; - TZID id = id_by_name(zone_name); - if (time_zone_by_id(id, dtzi)) { - return id; - } else { - return TZID_INVALID; - } -} - -static int offset_at_datetime_impl(TZID zone_id, int64_t epoch_sec, int *offset, -GAP_HANDLING gap_handling) -{ - DYNAMIC_TIME_ZONE_INFORMATION dtzi{}; - bool result = time_zone_by_id(zone_id, dtzi); - if (!result) { - return INT_MAX; - } - SYSTEMTIME localtime, utctime, adjusted; - unix_time_to_systemtime(epoch_sec, localtime); - TzSpecificLocalTimeToSystemTimeEx(&dtzi, &localtime, &utctime); - TRANSITIONS_INFO trans{}; - *offset = offset_at_systime(dtzi, trans, utctime); - SystemTimeToTzSpecificLocalTimeEx(&dtzi, &utctime, &adjusted); - /* We don't use `epoch_sec` instead of `systemtime_to_unix_time(localtime) - because `unix_time_to_systemtime(epoch_sec, localtime)` above could - overflow the range of instants representable in WinAPI, and then the - difference from `epoch_sec` would be large, potentially causing problems. - If it happened, we don't return an error as we don't really care which - result to return: timezone database information outside of [1970; current - time) is not accurate anyway, and WinAPI supports dates in years [1601; - 30827], which should be enough for all practical purposes. */ - const auto transition_duration = (int)(systemtime_to_unix_time(adjusted) - - systemtime_to_unix_time(localtime)); - if (transition_duration == 0) - return 0; - switch (gap_handling) { - case GAP_HANDLING_MOVE_FORWARD: - return transition_duration; - case GAP_HANDLING_NEXT_CORRECT: - /* Let x, y in {daylight, standard} - If a gap happened, then - xEnd + xOffset < utctime < yBegin + yOffset - What we need to return is - yBegin + yOffset - epoch_sec - To learn whether we crossed from daylight to standard or vice versa: - xEnd = yBegin - epsilon => yOffset + epsilon > xOffset - Thus, we crossed from the lower offset to the bigger one. So, - return (daylight.offset > standard.offset ? - daylight.begin + daylight.offset : - standard.begin + standard.offset) - epoch_sec */ - if (trans.tzi.DaylightBias < trans.tzi.StandardBias) { - return systemtime_to_unix_time(trans.daylight_local) - + trans.tzi.StandardBias - trans.tzi.DaylightBias - - epoch_sec + 1; - } else { - return systemtime_to_unix_time(trans.standard_local) - + trans.tzi.DaylightBias - trans.tzi.StandardBias - - epoch_sec + 1; - } - default: - // impossible - *offset = INT_MAX; - return 0; - } -} - -int offset_at_datetime(TZID zone_id, int64_t epoch_sec, int *offset) -{ - return offset_at_datetime_impl(zone_id, epoch_sec, offset, - GAP_HANDLING_MOVE_FORWARD); -} - -int64_t at_start_of_day(TZID zone_id, int64_t epoch_sec) -{ - int offset = 0; - int trans = offset_at_datetime_impl(zone_id, epoch_sec, &offset, - GAP_HANDLING_NEXT_CORRECT); - if (offset == INT_MAX) - return LONG_MAX; - return epoch_sec - offset + trans; -} - -} diff --git a/core/native/cinterop/date.def b/core/native/cinterop/date.def deleted file mode 100644 index b0c9605af..000000000 --- a/core/native/cinterop/date.def +++ /dev/null @@ -1,5 +0,0 @@ -package = kotlinx.datetime.internal - -# requirements of the `date` library: https://howardhinnant.github.io/date/tz.html#Installation -linkerOpts.mingw_x64 = -lole32 -linkerOpts.linux_x64 = -lpthread diff --git a/core/native/cinterop/public/cdate.h b/core/native/cinterop/public/cdate.h deleted file mode 100644 index 09f9e6e01..000000000 --- a/core/native/cinterop/public/cdate.h +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. - * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. - */ -// This file specifies the native interface for datetime information queries. -#pragma once -#include -#include -#include - -typedef size_t TZID; -const TZID TZID_INVALID = SIZE_MAX; - -enum GAP_HANDLING { - GAP_HANDLING_MOVE_FORWARD, - GAP_HANDLING_NEXT_CORRECT, -}; - -// Returns true if successful. -bool current_time(int64_t *sec, int32_t *nano); - -/* Returns a string that must be freed by the caller, or null. - If something is returned, `id` has the id of the timezone. */ -char * get_system_timezone(TZID* id); - -/* Returns an array of strings. The end of the array is marked with a NULL. - The array and its contents must be freed by the caller. - In case of an error, NULL is returned. */ -char ** available_zone_ids(); - -// returns the offset, or INT_MAX if there's a problem with the time zone. -int offset_at_instant(TZID zone, int64_t epoch_sec); - -// returns the id of the timezone or TZID_INVALID in case of an error. -TZID timezone_by_name(const char *zone_name); - -/* Sets the result in "offset"; in case an existing value in "offset" is an - acceptable one, leaves it untouched. Returns the number of seconds that the - caller needs to add to their existing estimation of date, which is needed in - case the time does not exist, having fallen in the gap. - In case of an error, "offset" is set to INT_MAX. */ -int offset_at_datetime(TZID zone, int64_t epoch_sec, int *offset); - -int64_t at_start_of_day(TZID zone, int64_t midnight_epoch_sec); diff --git a/core/native/cinterop/public/helper_macros.hpp b/core/native/cinterop/public/helper_macros.hpp deleted file mode 100644 index cd3c680b0..000000000 --- a/core/native/cinterop/public/helper_macros.hpp +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright 2016-2020 JetBrains s.r.o. - * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. - */ -#pragma once -#include - -/* Check the given pointer to see if it's null. If so, fail, printing to - stderr that insufficient memory is available. */ -template -static inline T* check_allocation(T* value) -{ - if (value == nullptr) { - fprintf(stderr, "Insufficient memory available\n"); - abort(); - } - return value; -} diff --git a/core/native/cinterop/public/windows_zones.hpp b/core/native/cinterop/public/windows_zones.hpp deleted file mode 100644 index 1a69f0e5c..000000000 --- a/core/native/cinterop/public/windows_zones.hpp +++ /dev/null @@ -1,1066 +0,0 @@ -// generated with gradle task `downloadWindowsZonesMapping` -#include -#include -static const std::unordered_map standard_to_windows = { - { "Africa/Abidjan", "Greenwich Standard Time" }, - { "Africa/Accra", "Greenwich Standard Time" }, - { "Africa/Addis_Ababa", "E. Africa Standard Time" }, - { "Africa/Algiers", "W. Central Africa Standard Time" }, - { "Africa/Asmera", "E. Africa Standard Time" }, - { "Africa/Bamako", "Greenwich Standard Time" }, - { "Africa/Bangui", "W. Central Africa Standard Time" }, - { "Africa/Banjul", "Greenwich Standard Time" }, - { "Africa/Bissau", "Greenwich Standard Time" }, - { "Africa/Blantyre", "South Africa Standard Time" }, - { "Africa/Brazzaville", "W. Central Africa Standard Time" }, - { "Africa/Bujumbura", "South Africa Standard Time" }, - { "Africa/Cairo", "Egypt Standard Time" }, - { "Africa/Casablanca", "Morocco Standard Time" }, - { "Africa/Ceuta", "Romance Standard Time" }, - { "Africa/Conakry", "Greenwich Standard Time" }, - { "Africa/Dakar", "Greenwich Standard Time" }, - { "Africa/Dar_es_Salaam", "E. Africa Standard Time" }, - { "Africa/Djibouti", "E. Africa Standard Time" }, - { "Africa/Douala", "W. Central Africa Standard Time" }, - { "Africa/El_Aaiun", "Morocco Standard Time" }, - { "Africa/Freetown", "Greenwich Standard Time" }, - { "Africa/Gaborone", "South Africa Standard Time" }, - { "Africa/Harare", "South Africa Standard Time" }, - { "Africa/Johannesburg", "South Africa Standard Time" }, - { "Africa/Juba", "South Sudan Standard Time" }, - { "Africa/Kampala", "E. Africa Standard Time" }, - { "Africa/Khartoum", "Sudan Standard Time" }, - { "Africa/Kigali", "South Africa Standard Time" }, - { "Africa/Kinshasa", "W. Central Africa Standard Time" }, - { "Africa/Lagos", "W. Central Africa Standard Time" }, - { "Africa/Libreville", "W. Central Africa Standard Time" }, - { "Africa/Lome", "Greenwich Standard Time" }, - { "Africa/Luanda", "W. Central Africa Standard Time" }, - { "Africa/Lubumbashi", "South Africa Standard Time" }, - { "Africa/Lusaka", "South Africa Standard Time" }, - { "Africa/Malabo", "W. Central Africa Standard Time" }, - { "Africa/Maputo", "South Africa Standard Time" }, - { "Africa/Maseru", "South Africa Standard Time" }, - { "Africa/Mbabane", "South Africa Standard Time" }, - { "Africa/Mogadishu", "E. Africa Standard Time" }, - { "Africa/Monrovia", "Greenwich Standard Time" }, - { "Africa/Nairobi", "E. Africa Standard Time" }, - { "Africa/Ndjamena", "W. Central Africa Standard Time" }, - { "Africa/Niamey", "W. Central Africa Standard Time" }, - { "Africa/Nouakchott", "Greenwich Standard Time" }, - { "Africa/Ouagadougou", "Greenwich Standard Time" }, - { "Africa/Porto-Novo", "W. Central Africa Standard Time" }, - { "Africa/Sao_Tome", "Sao Tome Standard Time" }, - { "Africa/Tripoli", "Libya Standard Time" }, - { "Africa/Tunis", "W. Central Africa Standard Time" }, - { "Africa/Windhoek", "Namibia Standard Time" }, - { "America/Adak", "Aleutian Standard Time" }, - { "America/Anchorage", "Alaskan Standard Time" }, - { "America/Anguilla", "SA Western Standard Time" }, - { "America/Antigua", "SA Western Standard Time" }, - { "America/Araguaina", "Tocantins Standard Time" }, - { "America/Argentina/La_Rioja", "Argentina Standard Time" }, - { "America/Argentina/Rio_Gallegos", "Argentina Standard Time" }, - { "America/Argentina/Salta", "Argentina Standard Time" }, - { "America/Argentina/San_Juan", "Argentina Standard Time" }, - { "America/Argentina/San_Luis", "Argentina Standard Time" }, - { "America/Argentina/Tucuman", "Argentina Standard Time" }, - { "America/Argentina/Ushuaia", "Argentina Standard Time" }, - { "America/Aruba", "SA Western Standard Time" }, - { "America/Asuncion", "Paraguay Standard Time" }, - { "America/Bahia", "Bahia Standard Time" }, - { "America/Bahia_Banderas", "Central Standard Time (Mexico)" }, - { "America/Barbados", "SA Western Standard Time" }, - { "America/Belem", "SA Eastern Standard Time" }, - { "America/Belize", "Central America Standard Time" }, - { "America/Blanc-Sablon", "SA Western Standard Time" }, - { "America/Boa_Vista", "SA Western Standard Time" }, - { "America/Bogota", "SA Pacific Standard Time" }, - { "America/Boise", "Mountain Standard Time" }, - { "America/Buenos_Aires", "Argentina Standard Time" }, - { "America/Cambridge_Bay", "Mountain Standard Time" }, - { "America/Campo_Grande", "Central Brazilian Standard Time" }, - { "America/Cancun", "Eastern Standard Time (Mexico)" }, - { "America/Caracas", "Venezuela Standard Time" }, - { "America/Catamarca", "Argentina Standard Time" }, - { "America/Cayenne", "SA Eastern Standard Time" }, - { "America/Cayman", "SA Pacific Standard Time" }, - { "America/Chicago", "Central Standard Time" }, - { "America/Chihuahua", "Mountain Standard Time (Mexico)" }, - { "America/Coral_Harbour", "SA Pacific Standard Time" }, - { "America/Cordoba", "Argentina Standard Time" }, - { "America/Costa_Rica", "Central America Standard Time" }, - { "America/Creston", "US Mountain Standard Time" }, - { "America/Cuiaba", "Central Brazilian Standard Time" }, - { "America/Curacao", "SA Western Standard Time" }, - { "America/Danmarkshavn", "Greenwich Standard Time" }, - { "America/Dawson", "Yukon Standard Time" }, - { "America/Dawson_Creek", "US Mountain Standard Time" }, - { "America/Denver", "Mountain Standard Time" }, - { "America/Detroit", "Eastern Standard Time" }, - { "America/Dominica", "SA Western Standard Time" }, - { "America/Edmonton", "Mountain Standard Time" }, - { "America/Eirunepe", "SA Pacific Standard Time" }, - { "America/El_Salvador", "Central America Standard Time" }, - { "America/Fort_Nelson", "US Mountain Standard Time" }, - { "America/Fortaleza", "SA Eastern Standard Time" }, - { "America/Glace_Bay", "Atlantic Standard Time" }, - { "America/Godthab", "Greenland Standard Time" }, - { "America/Goose_Bay", "Atlantic Standard Time" }, - { "America/Grand_Turk", "Turks And Caicos Standard Time" }, - { "America/Grenada", "SA Western Standard Time" }, - { "America/Guadeloupe", "SA Western Standard Time" }, - { "America/Guatemala", "Central America Standard Time" }, - { "America/Guayaquil", "SA Pacific Standard Time" }, - { "America/Guyana", "SA Western Standard Time" }, - { "America/Halifax", "Atlantic Standard Time" }, - { "America/Havana", "Cuba Standard Time" }, - { "America/Hermosillo", "US Mountain Standard Time" }, - { "America/Indiana/Knox", "Central Standard Time" }, - { "America/Indiana/Marengo", "US Eastern Standard Time" }, - { "America/Indiana/Petersburg", "Eastern Standard Time" }, - { "America/Indiana/Tell_City", "Central Standard Time" }, - { "America/Indiana/Vevay", "US Eastern Standard Time" }, - { "America/Indiana/Vincennes", "Eastern Standard Time" }, - { "America/Indiana/Winamac", "Eastern Standard Time" }, - { "America/Indianapolis", "US Eastern Standard Time" }, - { "America/Inuvik", "Mountain Standard Time" }, - { "America/Iqaluit", "Eastern Standard Time" }, - { "America/Jamaica", "SA Pacific Standard Time" }, - { "America/Jujuy", "Argentina Standard Time" }, - { "America/Juneau", "Alaskan Standard Time" }, - { "America/Kentucky/Monticello", "Eastern Standard Time" }, - { "America/Kralendijk", "SA Western Standard Time" }, - { "America/La_Paz", "SA Western Standard Time" }, - { "America/Lima", "SA Pacific Standard Time" }, - { "America/Los_Angeles", "Pacific Standard Time" }, - { "America/Louisville", "Eastern Standard Time" }, - { "America/Lower_Princes", "SA Western Standard Time" }, - { "America/Maceio", "SA Eastern Standard Time" }, - { "America/Managua", "Central America Standard Time" }, - { "America/Manaus", "SA Western Standard Time" }, - { "America/Marigot", "SA Western Standard Time" }, - { "America/Martinique", "SA Western Standard Time" }, - { "America/Matamoros", "Central Standard Time" }, - { "America/Mazatlan", "Mountain Standard Time (Mexico)" }, - { "America/Mendoza", "Argentina Standard Time" }, - { "America/Menominee", "Central Standard Time" }, - { "America/Merida", "Central Standard Time (Mexico)" }, - { "America/Metlakatla", "Alaskan Standard Time" }, - { "America/Mexico_City", "Central Standard Time (Mexico)" }, - { "America/Miquelon", "Saint Pierre Standard Time" }, - { "America/Moncton", "Atlantic Standard Time" }, - { "America/Monterrey", "Central Standard Time (Mexico)" }, - { "America/Montevideo", "Montevideo Standard Time" }, - { "America/Montreal", "Eastern Standard Time" }, - { "America/Montserrat", "SA Western Standard Time" }, - { "America/Nassau", "Eastern Standard Time" }, - { "America/New_York", "Eastern Standard Time" }, - { "America/Nipigon", "Eastern Standard Time" }, - { "America/Nome", "Alaskan Standard Time" }, - { "America/Noronha", "UTC-02" }, - { "America/North_Dakota/Beulah", "Central Standard Time" }, - { "America/North_Dakota/Center", "Central Standard Time" }, - { "America/North_Dakota/New_Salem", "Central Standard Time" }, - { "America/Ojinaga", "Mountain Standard Time" }, - { "America/Panama", "SA Pacific Standard Time" }, - { "America/Pangnirtung", "Eastern Standard Time" }, - { "America/Paramaribo", "SA Eastern Standard Time" }, - { "America/Phoenix", "US Mountain Standard Time" }, - { "America/Port-au-Prince", "Haiti Standard Time" }, - { "America/Port_of_Spain", "SA Western Standard Time" }, - { "America/Porto_Velho", "SA Western Standard Time" }, - { "America/Puerto_Rico", "SA Western Standard Time" }, - { "America/Punta_Arenas", "Magallanes Standard Time" }, - { "America/Rainy_River", "Central Standard Time" }, - { "America/Rankin_Inlet", "Central Standard Time" }, - { "America/Recife", "SA Eastern Standard Time" }, - { "America/Regina", "Canada Central Standard Time" }, - { "America/Resolute", "Central Standard Time" }, - { "America/Rio_Branco", "SA Pacific Standard Time" }, - { "America/Santa_Isabel", "Pacific Standard Time (Mexico)" }, - { "America/Santarem", "SA Eastern Standard Time" }, - { "America/Santiago", "Pacific SA Standard Time" }, - { "America/Santo_Domingo", "SA Western Standard Time" }, - { "America/Sao_Paulo", "E. South America Standard Time" }, - { "America/Scoresbysund", "Azores Standard Time" }, - { "America/Sitka", "Alaskan Standard Time" }, - { "America/St_Barthelemy", "SA Western Standard Time" }, - { "America/St_Johns", "Newfoundland Standard Time" }, - { "America/St_Kitts", "SA Western Standard Time" }, - { "America/St_Lucia", "SA Western Standard Time" }, - { "America/St_Thomas", "SA Western Standard Time" }, - { "America/St_Vincent", "SA Western Standard Time" }, - { "America/Swift_Current", "Canada Central Standard Time" }, - { "America/Tegucigalpa", "Central America Standard Time" }, - { "America/Thule", "Atlantic Standard Time" }, - { "America/Thunder_Bay", "Eastern Standard Time" }, - { "America/Tijuana", "Pacific Standard Time (Mexico)" }, - { "America/Toronto", "Eastern Standard Time" }, - { "America/Tortola", "SA Western Standard Time" }, - { "America/Vancouver", "Pacific Standard Time" }, - { "America/Whitehorse", "Yukon Standard Time" }, - { "America/Winnipeg", "Central Standard Time" }, - { "America/Yakutat", "Alaskan Standard Time" }, - { "America/Yellowknife", "Mountain Standard Time" }, - { "Antarctica/Casey", "Central Pacific Standard Time" }, - { "Antarctica/Davis", "SE Asia Standard Time" }, - { "Antarctica/DumontDUrville", "West Pacific Standard Time" }, - { "Antarctica/Macquarie", "Tasmania Standard Time" }, - { "Antarctica/Mawson", "West Asia Standard Time" }, - { "Antarctica/McMurdo", "New Zealand Standard Time" }, - { "Antarctica/Palmer", "SA Eastern Standard Time" }, - { "Antarctica/Rothera", "SA Eastern Standard Time" }, - { "Antarctica/Syowa", "E. Africa Standard Time" }, - { "Antarctica/Vostok", "Central Asia Standard Time" }, - { "Arctic/Longyearbyen", "W. Europe Standard Time" }, - { "Asia/Aden", "Arab Standard Time" }, - { "Asia/Almaty", "Central Asia Standard Time" }, - { "Asia/Amman", "Jordan Standard Time" }, - { "Asia/Anadyr", "Russia Time Zone 11" }, - { "Asia/Aqtau", "West Asia Standard Time" }, - { "Asia/Aqtobe", "West Asia Standard Time" }, - { "Asia/Ashgabat", "West Asia Standard Time" }, - { "Asia/Atyrau", "West Asia Standard Time" }, - { "Asia/Baghdad", "Arabic Standard Time" }, - { "Asia/Bahrain", "Arab Standard Time" }, - { "Asia/Baku", "Azerbaijan Standard Time" }, - { "Asia/Bangkok", "SE Asia Standard Time" }, - { "Asia/Barnaul", "Altai Standard Time" }, - { "Asia/Beirut", "Middle East Standard Time" }, - { "Asia/Bishkek", "Central Asia Standard Time" }, - { "Asia/Brunei", "Singapore Standard Time" }, - { "Asia/Calcutta", "India Standard Time" }, - { "Asia/Chita", "Transbaikal Standard Time" }, - { "Asia/Choibalsan", "Ulaanbaatar Standard Time" }, - { "Asia/Colombo", "Sri Lanka Standard Time" }, - { "Asia/Damascus", "Syria Standard Time" }, - { "Asia/Dhaka", "Bangladesh Standard Time" }, - { "Asia/Dili", "Tokyo Standard Time" }, - { "Asia/Dubai", "Arabian Standard Time" }, - { "Asia/Dushanbe", "West Asia Standard Time" }, - { "Asia/Famagusta", "GTB Standard Time" }, - { "Asia/Gaza", "West Bank Standard Time" }, - { "Asia/Hebron", "West Bank Standard Time" }, - { "Asia/Hong_Kong", "China Standard Time" }, - { "Asia/Hovd", "W. Mongolia Standard Time" }, - { "Asia/Irkutsk", "North Asia East Standard Time" }, - { "Asia/Jakarta", "SE Asia Standard Time" }, - { "Asia/Jayapura", "Tokyo Standard Time" }, - { "Asia/Jerusalem", "Israel Standard Time" }, - { "Asia/Kabul", "Afghanistan Standard Time" }, - { "Asia/Kamchatka", "Russia Time Zone 11" }, - { "Asia/Karachi", "Pakistan Standard Time" }, - { "Asia/Katmandu", "Nepal Standard Time" }, - { "Asia/Khandyga", "Yakutsk Standard Time" }, - { "Asia/Krasnoyarsk", "North Asia Standard Time" }, - { "Asia/Kuala_Lumpur", "Singapore Standard Time" }, - { "Asia/Kuching", "Singapore Standard Time" }, - { "Asia/Kuwait", "Arab Standard Time" }, - { "Asia/Macau", "China Standard Time" }, - { "Asia/Magadan", "Magadan Standard Time" }, - { "Asia/Makassar", "Singapore Standard Time" }, - { "Asia/Manila", "Singapore Standard Time" }, - { "Asia/Muscat", "Arabian Standard Time" }, - { "Asia/Nicosia", "GTB Standard Time" }, - { "Asia/Novokuznetsk", "North Asia Standard Time" }, - { "Asia/Novosibirsk", "N. Central Asia Standard Time" }, - { "Asia/Omsk", "Omsk Standard Time" }, - { "Asia/Oral", "West Asia Standard Time" }, - { "Asia/Phnom_Penh", "SE Asia Standard Time" }, - { "Asia/Pontianak", "SE Asia Standard Time" }, - { "Asia/Pyongyang", "North Korea Standard Time" }, - { "Asia/Qatar", "Arab Standard Time" }, - { "Asia/Qostanay", "Central Asia Standard Time" }, - { "Asia/Qyzylorda", "Qyzylorda Standard Time" }, - { "Asia/Rangoon", "Myanmar Standard Time" }, - { "Asia/Riyadh", "Arab Standard Time" }, - { "Asia/Saigon", "SE Asia Standard Time" }, - { "Asia/Sakhalin", "Sakhalin Standard Time" }, - { "Asia/Samarkand", "West Asia Standard Time" }, - { "Asia/Seoul", "Korea Standard Time" }, - { "Asia/Shanghai", "China Standard Time" }, - { "Asia/Singapore", "Singapore Standard Time" }, - { "Asia/Srednekolymsk", "Russia Time Zone 10" }, - { "Asia/Taipei", "Taipei Standard Time" }, - { "Asia/Tashkent", "West Asia Standard Time" }, - { "Asia/Tbilisi", "Georgian Standard Time" }, - { "Asia/Tehran", "Iran Standard Time" }, - { "Asia/Thimphu", "Bangladesh Standard Time" }, - { "Asia/Tokyo", "Tokyo Standard Time" }, - { "Asia/Tomsk", "Tomsk Standard Time" }, - { "Asia/Ulaanbaatar", "Ulaanbaatar Standard Time" }, - { "Asia/Urumqi", "Central Asia Standard Time" }, - { "Asia/Ust-Nera", "Vladivostok Standard Time" }, - { "Asia/Vientiane", "SE Asia Standard Time" }, - { "Asia/Vladivostok", "Vladivostok Standard Time" }, - { "Asia/Yakutsk", "Yakutsk Standard Time" }, - { "Asia/Yekaterinburg", "Ekaterinburg Standard Time" }, - { "Asia/Yerevan", "Caucasus Standard Time" }, - { "Atlantic/Azores", "Azores Standard Time" }, - { "Atlantic/Bermuda", "Atlantic Standard Time" }, - { "Atlantic/Canary", "GMT Standard Time" }, - { "Atlantic/Cape_Verde", "Cape Verde Standard Time" }, - { "Atlantic/Faeroe", "GMT Standard Time" }, - { "Atlantic/Madeira", "GMT Standard Time" }, - { "Atlantic/Reykjavik", "Greenwich Standard Time" }, - { "Atlantic/South_Georgia", "UTC-02" }, - { "Atlantic/St_Helena", "Greenwich Standard Time" }, - { "Atlantic/Stanley", "SA Eastern Standard Time" }, - { "Australia/Adelaide", "Cen. Australia Standard Time" }, - { "Australia/Brisbane", "E. Australia Standard Time" }, - { "Australia/Broken_Hill", "Cen. Australia Standard Time" }, - { "Australia/Currie", "Tasmania Standard Time" }, - { "Australia/Darwin", "AUS Central Standard Time" }, - { "Australia/Eucla", "Aus Central W. Standard Time" }, - { "Australia/Hobart", "Tasmania Standard Time" }, - { "Australia/Lindeman", "E. Australia Standard Time" }, - { "Australia/Lord_Howe", "Lord Howe Standard Time" }, - { "Australia/Melbourne", "AUS Eastern Standard Time" }, - { "Australia/Perth", "W. Australia Standard Time" }, - { "Australia/Sydney", "AUS Eastern Standard Time" }, - { "CST6CDT", "Central Standard Time" }, - { "EST5EDT", "Eastern Standard Time" }, - { "Etc/GMT", "UTC" }, - { "Etc/GMT+1", "Cape Verde Standard Time" }, - { "Etc/GMT+10", "Hawaiian Standard Time" }, - { "Etc/GMT+11", "UTC-11" }, - { "Etc/GMT+12", "Dateline Standard Time" }, - { "Etc/GMT+2", "UTC-02" }, - { "Etc/GMT+3", "SA Eastern Standard Time" }, - { "Etc/GMT+4", "SA Western Standard Time" }, - { "Etc/GMT+5", "SA Pacific Standard Time" }, - { "Etc/GMT+6", "Central America Standard Time" }, - { "Etc/GMT+7", "US Mountain Standard Time" }, - { "Etc/GMT+8", "UTC-08" }, - { "Etc/GMT+9", "UTC-09" }, - { "Etc/GMT-1", "W. Central Africa Standard Time" }, - { "Etc/GMT-10", "West Pacific Standard Time" }, - { "Etc/GMT-11", "Central Pacific Standard Time" }, - { "Etc/GMT-12", "UTC+12" }, - { "Etc/GMT-13", "UTC+13" }, - { "Etc/GMT-14", "Line Islands Standard Time" }, - { "Etc/GMT-2", "South Africa Standard Time" }, - { "Etc/GMT-3", "E. Africa Standard Time" }, - { "Etc/GMT-4", "Arabian Standard Time" }, - { "Etc/GMT-5", "West Asia Standard Time" }, - { "Etc/GMT-6", "Central Asia Standard Time" }, - { "Etc/GMT-7", "SE Asia Standard Time" }, - { "Etc/GMT-8", "Singapore Standard Time" }, - { "Etc/GMT-9", "Tokyo Standard Time" }, - { "Etc/UTC", "UTC" }, - { "Europe/Amsterdam", "W. Europe Standard Time" }, - { "Europe/Andorra", "W. Europe Standard Time" }, - { "Europe/Astrakhan", "Astrakhan Standard Time" }, - { "Europe/Athens", "GTB Standard Time" }, - { "Europe/Belgrade", "Central Europe Standard Time" }, - { "Europe/Berlin", "W. Europe Standard Time" }, - { "Europe/Bratislava", "Central Europe Standard Time" }, - { "Europe/Brussels", "Romance Standard Time" }, - { "Europe/Bucharest", "GTB Standard Time" }, - { "Europe/Budapest", "Central Europe Standard Time" }, - { "Europe/Busingen", "W. Europe Standard Time" }, - { "Europe/Chisinau", "E. Europe Standard Time" }, - { "Europe/Copenhagen", "Romance Standard Time" }, - { "Europe/Dublin", "GMT Standard Time" }, - { "Europe/Gibraltar", "W. Europe Standard Time" }, - { "Europe/Guernsey", "GMT Standard Time" }, - { "Europe/Helsinki", "FLE Standard Time" }, - { "Europe/Isle_of_Man", "GMT Standard Time" }, - { "Europe/Istanbul", "Turkey Standard Time" }, - { "Europe/Jersey", "GMT Standard Time" }, - { "Europe/Kaliningrad", "Kaliningrad Standard Time" }, - { "Europe/Kiev", "FLE Standard Time" }, - { "Europe/Kirov", "Russian Standard Time" }, - { "Europe/Lisbon", "GMT Standard Time" }, - { "Europe/Ljubljana", "Central Europe Standard Time" }, - { "Europe/London", "GMT Standard Time" }, - { "Europe/Luxembourg", "W. Europe Standard Time" }, - { "Europe/Madrid", "Romance Standard Time" }, - { "Europe/Malta", "W. Europe Standard Time" }, - { "Europe/Mariehamn", "FLE Standard Time" }, - { "Europe/Minsk", "Belarus Standard Time" }, - { "Europe/Monaco", "W. Europe Standard Time" }, - { "Europe/Moscow", "Russian Standard Time" }, - { "Europe/Oslo", "W. Europe Standard Time" }, - { "Europe/Paris", "Romance Standard Time" }, - { "Europe/Podgorica", "Central Europe Standard Time" }, - { "Europe/Prague", "Central Europe Standard Time" }, - { "Europe/Riga", "FLE Standard Time" }, - { "Europe/Rome", "W. Europe Standard Time" }, - { "Europe/Samara", "Russia Time Zone 3" }, - { "Europe/San_Marino", "W. Europe Standard Time" }, - { "Europe/Sarajevo", "Central European Standard Time" }, - { "Europe/Saratov", "Saratov Standard Time" }, - { "Europe/Simferopol", "Russian Standard Time" }, - { "Europe/Skopje", "Central European Standard Time" }, - { "Europe/Sofia", "FLE Standard Time" }, - { "Europe/Stockholm", "W. Europe Standard Time" }, - { "Europe/Tallinn", "FLE Standard Time" }, - { "Europe/Tirane", "Central Europe Standard Time" }, - { "Europe/Ulyanovsk", "Astrakhan Standard Time" }, - { "Europe/Uzhgorod", "FLE Standard Time" }, - { "Europe/Vaduz", "W. Europe Standard Time" }, - { "Europe/Vatican", "W. Europe Standard Time" }, - { "Europe/Vienna", "W. Europe Standard Time" }, - { "Europe/Vilnius", "FLE Standard Time" }, - { "Europe/Volgograd", "Volgograd Standard Time" }, - { "Europe/Warsaw", "Central European Standard Time" }, - { "Europe/Zagreb", "Central European Standard Time" }, - { "Europe/Zaporozhye", "FLE Standard Time" }, - { "Europe/Zurich", "W. Europe Standard Time" }, - { "Indian/Antananarivo", "E. Africa Standard Time" }, - { "Indian/Chagos", "Central Asia Standard Time" }, - { "Indian/Christmas", "SE Asia Standard Time" }, - { "Indian/Cocos", "Myanmar Standard Time" }, - { "Indian/Comoro", "E. Africa Standard Time" }, - { "Indian/Kerguelen", "West Asia Standard Time" }, - { "Indian/Mahe", "Mauritius Standard Time" }, - { "Indian/Maldives", "West Asia Standard Time" }, - { "Indian/Mauritius", "Mauritius Standard Time" }, - { "Indian/Mayotte", "E. Africa Standard Time" }, - { "Indian/Reunion", "Mauritius Standard Time" }, - { "MST7MDT", "Mountain Standard Time" }, - { "PST8PDT", "Pacific Standard Time" }, - { "Pacific/Apia", "Samoa Standard Time" }, - { "Pacific/Auckland", "New Zealand Standard Time" }, - { "Pacific/Bougainville", "Bougainville Standard Time" }, - { "Pacific/Chatham", "Chatham Islands Standard Time" }, - { "Pacific/Easter", "Easter Island Standard Time" }, - { "Pacific/Efate", "Central Pacific Standard Time" }, - { "Pacific/Enderbury", "UTC+13" }, - { "Pacific/Fakaofo", "UTC+13" }, - { "Pacific/Fiji", "Fiji Standard Time" }, - { "Pacific/Funafuti", "UTC+12" }, - { "Pacific/Galapagos", "Central America Standard Time" }, - { "Pacific/Gambier", "UTC-09" }, - { "Pacific/Guadalcanal", "Central Pacific Standard Time" }, - { "Pacific/Guam", "West Pacific Standard Time" }, - { "Pacific/Honolulu", "Hawaiian Standard Time" }, - { "Pacific/Johnston", "Hawaiian Standard Time" }, - { "Pacific/Kiritimati", "Line Islands Standard Time" }, - { "Pacific/Kosrae", "Central Pacific Standard Time" }, - { "Pacific/Kwajalein", "UTC+12" }, - { "Pacific/Majuro", "UTC+12" }, - { "Pacific/Marquesas", "Marquesas Standard Time" }, - { "Pacific/Midway", "UTC-11" }, - { "Pacific/Nauru", "UTC+12" }, - { "Pacific/Niue", "UTC-11" }, - { "Pacific/Norfolk", "Norfolk Standard Time" }, - { "Pacific/Noumea", "Central Pacific Standard Time" }, - { "Pacific/Pago_Pago", "UTC-11" }, - { "Pacific/Palau", "Tokyo Standard Time" }, - { "Pacific/Pitcairn", "UTC-08" }, - { "Pacific/Ponape", "Central Pacific Standard Time" }, - { "Pacific/Port_Moresby", "West Pacific Standard Time" }, - { "Pacific/Rarotonga", "Hawaiian Standard Time" }, - { "Pacific/Saipan", "West Pacific Standard Time" }, - { "Pacific/Tahiti", "Hawaiian Standard Time" }, - { "Pacific/Tarawa", "UTC+12" }, - { "Pacific/Tongatapu", "Tonga Standard Time" }, - { "Pacific/Truk", "West Pacific Standard Time" }, - { "Pacific/Wake", "UTC+12" }, - { "Pacific/Wallis", "UTC+12" }, -}; -static const std::unordered_map windows_to_standard = { - { "AUS Central Standard Time", "Australia/Darwin" }, - { "AUS Eastern Standard Time", "Australia/Sydney" }, - { "Afghanistan Standard Time", "Asia/Kabul" }, - { "Alaskan Standard Time", "America/Anchorage" }, - { "Aleutian Standard Time", "America/Adak" }, - { "Altai Standard Time", "Asia/Barnaul" }, - { "Arab Standard Time", "Asia/Riyadh" }, - { "Arabian Standard Time", "Asia/Dubai" }, - { "Arabic Standard Time", "Asia/Baghdad" }, - { "Argentina Standard Time", "America/Buenos_Aires" }, - { "Astrakhan Standard Time", "Europe/Astrakhan" }, - { "Atlantic Standard Time", "America/Halifax" }, - { "Aus Central W. Standard Time", "Australia/Eucla" }, - { "Azerbaijan Standard Time", "Asia/Baku" }, - { "Azores Standard Time", "Atlantic/Azores" }, - { "Bahia Standard Time", "America/Bahia" }, - { "Bangladesh Standard Time", "Asia/Dhaka" }, - { "Belarus Standard Time", "Europe/Minsk" }, - { "Bougainville Standard Time", "Pacific/Bougainville" }, - { "Canada Central Standard Time", "America/Regina" }, - { "Cape Verde Standard Time", "Atlantic/Cape_Verde" }, - { "Caucasus Standard Time", "Asia/Yerevan" }, - { "Cen. Australia Standard Time", "Australia/Adelaide" }, - { "Central America Standard Time", "America/Guatemala" }, - { "Central Asia Standard Time", "Asia/Almaty" }, - { "Central Brazilian Standard Time", "America/Cuiaba" }, - { "Central Europe Standard Time", "Europe/Budapest" }, - { "Central European Standard Time", "Europe/Warsaw" }, - { "Central Pacific Standard Time", "Pacific/Guadalcanal" }, - { "Central Standard Time", "America/Chicago" }, - { "Central Standard Time (Mexico)", "America/Mexico_City" }, - { "Chatham Islands Standard Time", "Pacific/Chatham" }, - { "China Standard Time", "Asia/Shanghai" }, - { "Cuba Standard Time", "America/Havana" }, - { "Dateline Standard Time", "Etc/GMT+12" }, - { "E. Africa Standard Time", "Africa/Nairobi" }, - { "E. Australia Standard Time", "Australia/Brisbane" }, - { "E. Europe Standard Time", "Europe/Chisinau" }, - { "E. South America Standard Time", "America/Sao_Paulo" }, - { "Easter Island Standard Time", "Pacific/Easter" }, - { "Eastern Standard Time", "America/New_York" }, - { "Eastern Standard Time (Mexico)", "America/Cancun" }, - { "Egypt Standard Time", "Africa/Cairo" }, - { "Ekaterinburg Standard Time", "Asia/Yekaterinburg" }, - { "FLE Standard Time", "Europe/Kiev" }, - { "Fiji Standard Time", "Pacific/Fiji" }, - { "GMT Standard Time", "Europe/London" }, - { "GTB Standard Time", "Europe/Bucharest" }, - { "Georgian Standard Time", "Asia/Tbilisi" }, - { "Greenland Standard Time", "America/Godthab" }, - { "Greenwich Standard Time", "Atlantic/Reykjavik" }, - { "Haiti Standard Time", "America/Port-au-Prince" }, - { "Hawaiian Standard Time", "Pacific/Honolulu" }, - { "India Standard Time", "Asia/Calcutta" }, - { "Iran Standard Time", "Asia/Tehran" }, - { "Israel Standard Time", "Asia/Jerusalem" }, - { "Jordan Standard Time", "Asia/Amman" }, - { "Kaliningrad Standard Time", "Europe/Kaliningrad" }, - { "Korea Standard Time", "Asia/Seoul" }, - { "Libya Standard Time", "Africa/Tripoli" }, - { "Line Islands Standard Time", "Pacific/Kiritimati" }, - { "Lord Howe Standard Time", "Australia/Lord_Howe" }, - { "Magadan Standard Time", "Asia/Magadan" }, - { "Magallanes Standard Time", "America/Punta_Arenas" }, - { "Marquesas Standard Time", "Pacific/Marquesas" }, - { "Mauritius Standard Time", "Indian/Mauritius" }, - { "Middle East Standard Time", "Asia/Beirut" }, - { "Montevideo Standard Time", "America/Montevideo" }, - { "Morocco Standard Time", "Africa/Casablanca" }, - { "Mountain Standard Time", "America/Denver" }, - { "Mountain Standard Time (Mexico)", "America/Chihuahua" }, - { "Myanmar Standard Time", "Asia/Rangoon" }, - { "N. Central Asia Standard Time", "Asia/Novosibirsk" }, - { "Namibia Standard Time", "Africa/Windhoek" }, - { "Nepal Standard Time", "Asia/Katmandu" }, - { "New Zealand Standard Time", "Pacific/Auckland" }, - { "Newfoundland Standard Time", "America/St_Johns" }, - { "Norfolk Standard Time", "Pacific/Norfolk" }, - { "North Asia East Standard Time", "Asia/Irkutsk" }, - { "North Asia Standard Time", "Asia/Krasnoyarsk" }, - { "North Korea Standard Time", "Asia/Pyongyang" }, - { "Omsk Standard Time", "Asia/Omsk" }, - { "Pacific SA Standard Time", "America/Santiago" }, - { "Pacific Standard Time", "America/Los_Angeles" }, - { "Pacific Standard Time (Mexico)", "America/Tijuana" }, - { "Pakistan Standard Time", "Asia/Karachi" }, - { "Paraguay Standard Time", "America/Asuncion" }, - { "Qyzylorda Standard Time", "Asia/Qyzylorda" }, - { "Romance Standard Time", "Europe/Paris" }, - { "Russia Time Zone 10", "Asia/Srednekolymsk" }, - { "Russia Time Zone 11", "Asia/Kamchatka" }, - { "Russia Time Zone 3", "Europe/Samara" }, - { "Russian Standard Time", "Europe/Moscow" }, - { "SA Eastern Standard Time", "America/Cayenne" }, - { "SA Pacific Standard Time", "America/Bogota" }, - { "SA Western Standard Time", "America/La_Paz" }, - { "SE Asia Standard Time", "Asia/Bangkok" }, - { "Saint Pierre Standard Time", "America/Miquelon" }, - { "Sakhalin Standard Time", "Asia/Sakhalin" }, - { "Samoa Standard Time", "Pacific/Apia" }, - { "Sao Tome Standard Time", "Africa/Sao_Tome" }, - { "Saratov Standard Time", "Europe/Saratov" }, - { "Singapore Standard Time", "Asia/Singapore" }, - { "South Africa Standard Time", "Africa/Johannesburg" }, - { "South Sudan Standard Time", "Africa/Juba" }, - { "Sri Lanka Standard Time", "Asia/Colombo" }, - { "Sudan Standard Time", "Africa/Khartoum" }, - { "Syria Standard Time", "Asia/Damascus" }, - { "Taipei Standard Time", "Asia/Taipei" }, - { "Tasmania Standard Time", "Australia/Hobart" }, - { "Tocantins Standard Time", "America/Araguaina" }, - { "Tokyo Standard Time", "Asia/Tokyo" }, - { "Tomsk Standard Time", "Asia/Tomsk" }, - { "Tonga Standard Time", "Pacific/Tongatapu" }, - { "Transbaikal Standard Time", "Asia/Chita" }, - { "Turkey Standard Time", "Europe/Istanbul" }, - { "Turks And Caicos Standard Time", "America/Grand_Turk" }, - { "US Eastern Standard Time", "America/Indianapolis" }, - { "US Mountain Standard Time", "America/Phoenix" }, - { "UTC", "Etc/UTC" }, - { "UTC+12", "Etc/GMT-12" }, - { "UTC+13", "Etc/GMT-13" }, - { "UTC-02", "Etc/GMT+2" }, - { "UTC-08", "Etc/GMT+8" }, - { "UTC-09", "Etc/GMT+9" }, - { "UTC-11", "Etc/GMT+11" }, - { "Ulaanbaatar Standard Time", "Asia/Ulaanbaatar" }, - { "Venezuela Standard Time", "America/Caracas" }, - { "Vladivostok Standard Time", "Asia/Vladivostok" }, - { "Volgograd Standard Time", "Europe/Volgograd" }, - { "W. Australia Standard Time", "Australia/Perth" }, - { "W. Central Africa Standard Time", "Africa/Lagos" }, - { "W. Europe Standard Time", "Europe/Berlin" }, - { "W. Mongolia Standard Time", "Asia/Hovd" }, - { "West Asia Standard Time", "Asia/Tashkent" }, - { "West Bank Standard Time", "Asia/Hebron" }, - { "West Pacific Standard Time", "Pacific/Port_Moresby" }, - { "Yakutsk Standard Time", "Asia/Yakutsk" }, - { "Yukon Standard Time", "America/Whitehorse" }, -}; -static const std::unordered_map zone_ids = { - { "Africa/Abidjan", 0 }, - { "Africa/Accra", 1 }, - { "Africa/Addis_Ababa", 2 }, - { "Africa/Algiers", 3 }, - { "Africa/Asmera", 4 }, - { "Africa/Bamako", 5 }, - { "Africa/Bangui", 6 }, - { "Africa/Banjul", 7 }, - { "Africa/Bissau", 8 }, - { "Africa/Blantyre", 9 }, - { "Africa/Brazzaville", 10 }, - { "Africa/Bujumbura", 11 }, - { "Africa/Cairo", 12 }, - { "Africa/Casablanca", 13 }, - { "Africa/Ceuta", 14 }, - { "Africa/Conakry", 15 }, - { "Africa/Dakar", 16 }, - { "Africa/Dar_es_Salaam", 17 }, - { "Africa/Djibouti", 18 }, - { "Africa/Douala", 19 }, - { "Africa/El_Aaiun", 20 }, - { "Africa/Freetown", 21 }, - { "Africa/Gaborone", 22 }, - { "Africa/Harare", 23 }, - { "Africa/Johannesburg", 24 }, - { "Africa/Juba", 25 }, - { "Africa/Kampala", 26 }, - { "Africa/Khartoum", 27 }, - { "Africa/Kigali", 28 }, - { "Africa/Kinshasa", 29 }, - { "Africa/Lagos", 30 }, - { "Africa/Libreville", 31 }, - { "Africa/Lome", 32 }, - { "Africa/Luanda", 33 }, - { "Africa/Lubumbashi", 34 }, - { "Africa/Lusaka", 35 }, - { "Africa/Malabo", 36 }, - { "Africa/Maputo", 37 }, - { "Africa/Maseru", 38 }, - { "Africa/Mbabane", 39 }, - { "Africa/Mogadishu", 40 }, - { "Africa/Monrovia", 41 }, - { "Africa/Nairobi", 42 }, - { "Africa/Ndjamena", 43 }, - { "Africa/Niamey", 44 }, - { "Africa/Nouakchott", 45 }, - { "Africa/Ouagadougou", 46 }, - { "Africa/Porto-Novo", 47 }, - { "Africa/Sao_Tome", 48 }, - { "Africa/Tripoli", 49 }, - { "Africa/Tunis", 50 }, - { "Africa/Windhoek", 51 }, - { "America/Adak", 52 }, - { "America/Anchorage", 53 }, - { "America/Anguilla", 54 }, - { "America/Antigua", 55 }, - { "America/Araguaina", 56 }, - { "America/Argentina/La_Rioja", 57 }, - { "America/Argentina/Rio_Gallegos", 58 }, - { "America/Argentina/Salta", 59 }, - { "America/Argentina/San_Juan", 60 }, - { "America/Argentina/San_Luis", 61 }, - { "America/Argentina/Tucuman", 62 }, - { "America/Argentina/Ushuaia", 63 }, - { "America/Aruba", 64 }, - { "America/Asuncion", 65 }, - { "America/Bahia", 66 }, - { "America/Bahia_Banderas", 67 }, - { "America/Barbados", 68 }, - { "America/Belem", 69 }, - { "America/Belize", 70 }, - { "America/Blanc-Sablon", 71 }, - { "America/Boa_Vista", 72 }, - { "America/Bogota", 73 }, - { "America/Boise", 74 }, - { "America/Buenos_Aires", 75 }, - { "America/Cambridge_Bay", 76 }, - { "America/Campo_Grande", 77 }, - { "America/Cancun", 78 }, - { "America/Caracas", 79 }, - { "America/Catamarca", 80 }, - { "America/Cayenne", 81 }, - { "America/Cayman", 82 }, - { "America/Chicago", 83 }, - { "America/Chihuahua", 84 }, - { "America/Coral_Harbour", 85 }, - { "America/Cordoba", 86 }, - { "America/Costa_Rica", 87 }, - { "America/Creston", 88 }, - { "America/Cuiaba", 89 }, - { "America/Curacao", 90 }, - { "America/Danmarkshavn", 91 }, - { "America/Dawson", 92 }, - { "America/Dawson_Creek", 93 }, - { "America/Denver", 94 }, - { "America/Detroit", 95 }, - { "America/Dominica", 96 }, - { "America/Edmonton", 97 }, - { "America/Eirunepe", 98 }, - { "America/El_Salvador", 99 }, - { "America/Fort_Nelson", 100 }, - { "America/Fortaleza", 101 }, - { "America/Glace_Bay", 102 }, - { "America/Godthab", 103 }, - { "America/Goose_Bay", 104 }, - { "America/Grand_Turk", 105 }, - { "America/Grenada", 106 }, - { "America/Guadeloupe", 107 }, - { "America/Guatemala", 108 }, - { "America/Guayaquil", 109 }, - { "America/Guyana", 110 }, - { "America/Halifax", 111 }, - { "America/Havana", 112 }, - { "America/Hermosillo", 113 }, - { "America/Indiana/Knox", 114 }, - { "America/Indiana/Marengo", 115 }, - { "America/Indiana/Petersburg", 116 }, - { "America/Indiana/Tell_City", 117 }, - { "America/Indiana/Vevay", 118 }, - { "America/Indiana/Vincennes", 119 }, - { "America/Indiana/Winamac", 120 }, - { "America/Indianapolis", 121 }, - { "America/Inuvik", 122 }, - { "America/Iqaluit", 123 }, - { "America/Jamaica", 124 }, - { "America/Jujuy", 125 }, - { "America/Juneau", 126 }, - { "America/Kentucky/Monticello", 127 }, - { "America/Kralendijk", 128 }, - { "America/La_Paz", 129 }, - { "America/Lima", 130 }, - { "America/Los_Angeles", 131 }, - { "America/Louisville", 132 }, - { "America/Lower_Princes", 133 }, - { "America/Maceio", 134 }, - { "America/Managua", 135 }, - { "America/Manaus", 136 }, - { "America/Marigot", 137 }, - { "America/Martinique", 138 }, - { "America/Matamoros", 139 }, - { "America/Mazatlan", 140 }, - { "America/Mendoza", 141 }, - { "America/Menominee", 142 }, - { "America/Merida", 143 }, - { "America/Metlakatla", 144 }, - { "America/Mexico_City", 145 }, - { "America/Miquelon", 146 }, - { "America/Moncton", 147 }, - { "America/Monterrey", 148 }, - { "America/Montevideo", 149 }, - { "America/Montreal", 150 }, - { "America/Montserrat", 151 }, - { "America/Nassau", 152 }, - { "America/New_York", 153 }, - { "America/Nipigon", 154 }, - { "America/Nome", 155 }, - { "America/Noronha", 156 }, - { "America/North_Dakota/Beulah", 157 }, - { "America/North_Dakota/Center", 158 }, - { "America/North_Dakota/New_Salem", 159 }, - { "America/Ojinaga", 160 }, - { "America/Panama", 161 }, - { "America/Pangnirtung", 162 }, - { "America/Paramaribo", 163 }, - { "America/Phoenix", 164 }, - { "America/Port-au-Prince", 165 }, - { "America/Port_of_Spain", 166 }, - { "America/Porto_Velho", 167 }, - { "America/Puerto_Rico", 168 }, - { "America/Punta_Arenas", 169 }, - { "America/Rainy_River", 170 }, - { "America/Rankin_Inlet", 171 }, - { "America/Recife", 172 }, - { "America/Regina", 173 }, - { "America/Resolute", 174 }, - { "America/Rio_Branco", 175 }, - { "America/Santa_Isabel", 176 }, - { "America/Santarem", 177 }, - { "America/Santiago", 178 }, - { "America/Santo_Domingo", 179 }, - { "America/Sao_Paulo", 180 }, - { "America/Scoresbysund", 181 }, - { "America/Sitka", 182 }, - { "America/St_Barthelemy", 183 }, - { "America/St_Johns", 184 }, - { "America/St_Kitts", 185 }, - { "America/St_Lucia", 186 }, - { "America/St_Thomas", 187 }, - { "America/St_Vincent", 188 }, - { "America/Swift_Current", 189 }, - { "America/Tegucigalpa", 190 }, - { "America/Thule", 191 }, - { "America/Thunder_Bay", 192 }, - { "America/Tijuana", 193 }, - { "America/Toronto", 194 }, - { "America/Tortola", 195 }, - { "America/Vancouver", 196 }, - { "America/Whitehorse", 197 }, - { "America/Winnipeg", 198 }, - { "America/Yakutat", 199 }, - { "America/Yellowknife", 200 }, - { "Antarctica/Casey", 201 }, - { "Antarctica/Davis", 202 }, - { "Antarctica/DumontDUrville", 203 }, - { "Antarctica/Macquarie", 204 }, - { "Antarctica/Mawson", 205 }, - { "Antarctica/McMurdo", 206 }, - { "Antarctica/Palmer", 207 }, - { "Antarctica/Rothera", 208 }, - { "Antarctica/Syowa", 209 }, - { "Antarctica/Vostok", 210 }, - { "Arctic/Longyearbyen", 211 }, - { "Asia/Aden", 212 }, - { "Asia/Almaty", 213 }, - { "Asia/Amman", 214 }, - { "Asia/Anadyr", 215 }, - { "Asia/Aqtau", 216 }, - { "Asia/Aqtobe", 217 }, - { "Asia/Ashgabat", 218 }, - { "Asia/Atyrau", 219 }, - { "Asia/Baghdad", 220 }, - { "Asia/Bahrain", 221 }, - { "Asia/Baku", 222 }, - { "Asia/Bangkok", 223 }, - { "Asia/Barnaul", 224 }, - { "Asia/Beirut", 225 }, - { "Asia/Bishkek", 226 }, - { "Asia/Brunei", 227 }, - { "Asia/Calcutta", 228 }, - { "Asia/Chita", 229 }, - { "Asia/Choibalsan", 230 }, - { "Asia/Colombo", 231 }, - { "Asia/Damascus", 232 }, - { "Asia/Dhaka", 233 }, - { "Asia/Dili", 234 }, - { "Asia/Dubai", 235 }, - { "Asia/Dushanbe", 236 }, - { "Asia/Famagusta", 237 }, - { "Asia/Gaza", 238 }, - { "Asia/Hebron", 239 }, - { "Asia/Hong_Kong", 240 }, - { "Asia/Hovd", 241 }, - { "Asia/Irkutsk", 242 }, - { "Asia/Jakarta", 243 }, - { "Asia/Jayapura", 244 }, - { "Asia/Jerusalem", 245 }, - { "Asia/Kabul", 246 }, - { "Asia/Kamchatka", 247 }, - { "Asia/Karachi", 248 }, - { "Asia/Katmandu", 249 }, - { "Asia/Khandyga", 250 }, - { "Asia/Krasnoyarsk", 251 }, - { "Asia/Kuala_Lumpur", 252 }, - { "Asia/Kuching", 253 }, - { "Asia/Kuwait", 254 }, - { "Asia/Macau", 255 }, - { "Asia/Magadan", 256 }, - { "Asia/Makassar", 257 }, - { "Asia/Manila", 258 }, - { "Asia/Muscat", 259 }, - { "Asia/Nicosia", 260 }, - { "Asia/Novokuznetsk", 261 }, - { "Asia/Novosibirsk", 262 }, - { "Asia/Omsk", 263 }, - { "Asia/Oral", 264 }, - { "Asia/Phnom_Penh", 265 }, - { "Asia/Pontianak", 266 }, - { "Asia/Pyongyang", 267 }, - { "Asia/Qatar", 268 }, - { "Asia/Qostanay", 269 }, - { "Asia/Qyzylorda", 270 }, - { "Asia/Rangoon", 271 }, - { "Asia/Riyadh", 272 }, - { "Asia/Saigon", 273 }, - { "Asia/Sakhalin", 274 }, - { "Asia/Samarkand", 275 }, - { "Asia/Seoul", 276 }, - { "Asia/Shanghai", 277 }, - { "Asia/Singapore", 278 }, - { "Asia/Srednekolymsk", 279 }, - { "Asia/Taipei", 280 }, - { "Asia/Tashkent", 281 }, - { "Asia/Tbilisi", 282 }, - { "Asia/Tehran", 283 }, - { "Asia/Thimphu", 284 }, - { "Asia/Tokyo", 285 }, - { "Asia/Tomsk", 286 }, - { "Asia/Ulaanbaatar", 287 }, - { "Asia/Urumqi", 288 }, - { "Asia/Ust-Nera", 289 }, - { "Asia/Vientiane", 290 }, - { "Asia/Vladivostok", 291 }, - { "Asia/Yakutsk", 292 }, - { "Asia/Yekaterinburg", 293 }, - { "Asia/Yerevan", 294 }, - { "Atlantic/Azores", 295 }, - { "Atlantic/Bermuda", 296 }, - { "Atlantic/Canary", 297 }, - { "Atlantic/Cape_Verde", 298 }, - { "Atlantic/Faeroe", 299 }, - { "Atlantic/Madeira", 300 }, - { "Atlantic/Reykjavik", 301 }, - { "Atlantic/South_Georgia", 302 }, - { "Atlantic/St_Helena", 303 }, - { "Atlantic/Stanley", 304 }, - { "Australia/Adelaide", 305 }, - { "Australia/Brisbane", 306 }, - { "Australia/Broken_Hill", 307 }, - { "Australia/Currie", 308 }, - { "Australia/Darwin", 309 }, - { "Australia/Eucla", 310 }, - { "Australia/Hobart", 311 }, - { "Australia/Lindeman", 312 }, - { "Australia/Lord_Howe", 313 }, - { "Australia/Melbourne", 314 }, - { "Australia/Perth", 315 }, - { "Australia/Sydney", 316 }, - { "CST6CDT", 317 }, - { "EST5EDT", 318 }, - { "Etc/GMT", 319 }, - { "Etc/GMT+1", 320 }, - { "Etc/GMT+10", 321 }, - { "Etc/GMT+11", 322 }, - { "Etc/GMT+12", 323 }, - { "Etc/GMT+2", 324 }, - { "Etc/GMT+3", 325 }, - { "Etc/GMT+4", 326 }, - { "Etc/GMT+5", 327 }, - { "Etc/GMT+6", 328 }, - { "Etc/GMT+7", 329 }, - { "Etc/GMT+8", 330 }, - { "Etc/GMT+9", 331 }, - { "Etc/GMT-1", 332 }, - { "Etc/GMT-10", 333 }, - { "Etc/GMT-11", 334 }, - { "Etc/GMT-12", 335 }, - { "Etc/GMT-13", 336 }, - { "Etc/GMT-14", 337 }, - { "Etc/GMT-2", 338 }, - { "Etc/GMT-3", 339 }, - { "Etc/GMT-4", 340 }, - { "Etc/GMT-5", 341 }, - { "Etc/GMT-6", 342 }, - { "Etc/GMT-7", 343 }, - { "Etc/GMT-8", 344 }, - { "Etc/GMT-9", 345 }, - { "Etc/UTC", 346 }, - { "Europe/Amsterdam", 347 }, - { "Europe/Andorra", 348 }, - { "Europe/Astrakhan", 349 }, - { "Europe/Athens", 350 }, - { "Europe/Belgrade", 351 }, - { "Europe/Berlin", 352 }, - { "Europe/Bratislava", 353 }, - { "Europe/Brussels", 354 }, - { "Europe/Bucharest", 355 }, - { "Europe/Budapest", 356 }, - { "Europe/Busingen", 357 }, - { "Europe/Chisinau", 358 }, - { "Europe/Copenhagen", 359 }, - { "Europe/Dublin", 360 }, - { "Europe/Gibraltar", 361 }, - { "Europe/Guernsey", 362 }, - { "Europe/Helsinki", 363 }, - { "Europe/Isle_of_Man", 364 }, - { "Europe/Istanbul", 365 }, - { "Europe/Jersey", 366 }, - { "Europe/Kaliningrad", 367 }, - { "Europe/Kiev", 368 }, - { "Europe/Kirov", 369 }, - { "Europe/Lisbon", 370 }, - { "Europe/Ljubljana", 371 }, - { "Europe/London", 372 }, - { "Europe/Luxembourg", 373 }, - { "Europe/Madrid", 374 }, - { "Europe/Malta", 375 }, - { "Europe/Mariehamn", 376 }, - { "Europe/Minsk", 377 }, - { "Europe/Monaco", 378 }, - { "Europe/Moscow", 379 }, - { "Europe/Oslo", 380 }, - { "Europe/Paris", 381 }, - { "Europe/Podgorica", 382 }, - { "Europe/Prague", 383 }, - { "Europe/Riga", 384 }, - { "Europe/Rome", 385 }, - { "Europe/Samara", 386 }, - { "Europe/San_Marino", 387 }, - { "Europe/Sarajevo", 388 }, - { "Europe/Saratov", 389 }, - { "Europe/Simferopol", 390 }, - { "Europe/Skopje", 391 }, - { "Europe/Sofia", 392 }, - { "Europe/Stockholm", 393 }, - { "Europe/Tallinn", 394 }, - { "Europe/Tirane", 395 }, - { "Europe/Ulyanovsk", 396 }, - { "Europe/Uzhgorod", 397 }, - { "Europe/Vaduz", 398 }, - { "Europe/Vatican", 399 }, - { "Europe/Vienna", 400 }, - { "Europe/Vilnius", 401 }, - { "Europe/Volgograd", 402 }, - { "Europe/Warsaw", 403 }, - { "Europe/Zagreb", 404 }, - { "Europe/Zaporozhye", 405 }, - { "Europe/Zurich", 406 }, - { "Indian/Antananarivo", 407 }, - { "Indian/Chagos", 408 }, - { "Indian/Christmas", 409 }, - { "Indian/Cocos", 410 }, - { "Indian/Comoro", 411 }, - { "Indian/Kerguelen", 412 }, - { "Indian/Mahe", 413 }, - { "Indian/Maldives", 414 }, - { "Indian/Mauritius", 415 }, - { "Indian/Mayotte", 416 }, - { "Indian/Reunion", 417 }, - { "MST7MDT", 418 }, - { "PST8PDT", 419 }, - { "Pacific/Apia", 420 }, - { "Pacific/Auckland", 421 }, - { "Pacific/Bougainville", 422 }, - { "Pacific/Chatham", 423 }, - { "Pacific/Easter", 424 }, - { "Pacific/Efate", 425 }, - { "Pacific/Enderbury", 426 }, - { "Pacific/Fakaofo", 427 }, - { "Pacific/Fiji", 428 }, - { "Pacific/Funafuti", 429 }, - { "Pacific/Galapagos", 430 }, - { "Pacific/Gambier", 431 }, - { "Pacific/Guadalcanal", 432 }, - { "Pacific/Guam", 433 }, - { "Pacific/Honolulu", 434 }, - { "Pacific/Johnston", 435 }, - { "Pacific/Kiritimati", 436 }, - { "Pacific/Kosrae", 437 }, - { "Pacific/Kwajalein", 438 }, - { "Pacific/Majuro", 439 }, - { "Pacific/Marquesas", 440 }, - { "Pacific/Midway", 441 }, - { "Pacific/Nauru", 442 }, - { "Pacific/Niue", 443 }, - { "Pacific/Norfolk", 444 }, - { "Pacific/Noumea", 445 }, - { "Pacific/Pago_Pago", 446 }, - { "Pacific/Palau", 447 }, - { "Pacific/Pitcairn", 448 }, - { "Pacific/Ponape", 449 }, - { "Pacific/Port_Moresby", 450 }, - { "Pacific/Rarotonga", 451 }, - { "Pacific/Saipan", 452 }, - { "Pacific/Tahiti", 453 }, - { "Pacific/Tarawa", 454 }, - { "Pacific/Tongatapu", 455 }, - { "Pacific/Truk", 456 }, - { "Pacific/Wake", 457 }, - { "Pacific/Wallis", 458 }, -}; diff --git a/core/native/cinterop_actuals/TimeZoneNative.kt b/core/native/cinterop_actuals/TimeZoneNative.kt deleted file mode 100644 index 0d87660ef..000000000 --- a/core/native/cinterop_actuals/TimeZoneNative.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2019-2020 JetBrains s.r.o. - * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. - */ - -@file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) - -package kotlinx.datetime - -import kotlinx.datetime.internal.* -import kotlinx.cinterop.* -import platform.posix.free - -internal actual class RegionTimeZone(private val tzid: TZID, actual override val id: String): TimeZone() { - actual companion object { - actual fun of(zoneId: String): RegionTimeZone { - val tzid = timezone_by_name(zoneId) - if (tzid == TZID_INVALID) { - throw IllegalTimeZoneException("No timezone found with zone ID '$zoneId'") - } - return RegionTimeZone(tzid, zoneId) - } - - actual fun currentSystemDefault(): RegionTimeZone = memScoped { - val tzid = alloc() - val string = get_system_timezone(tzid.ptr) - ?: throw RuntimeException("Failed to get the system timezone.") - val kotlinString = string.toKString() - free(string) - RegionTimeZone(tzid.value, kotlinString) - } - - actual val availableZoneIds: Set - get() { - val set = mutableSetOf("UTC") - val zones = available_zone_ids() - ?: throw RuntimeException("Failed to get the list of available timezones") - var ptr = zones - while (true) { - val cur = ptr.pointed.value ?: break - val zoneName = cur.toKString() - set.add(zoneName) - free(cur) - ptr = (ptr + 1)!! - } - free(zones) - return set - } - } - - actual override fun atStartOfDay(date: LocalDate): Instant = memScoped { - val ldt = LocalDateTime(date, LocalTime.MIN) - val epochSeconds = ldt.toEpochSecond(UtcOffset.ZERO) - val midnightInstantSeconds = at_start_of_day(tzid, epochSeconds) - if (midnightInstantSeconds == Long.MAX_VALUE) { - throw RuntimeException("Unable to acquire the time of start of day at $date for zone $this") - } - Instant(midnightInstantSeconds, 0) - } - - actual override fun atZone(dateTime: LocalDateTime, preferred: UtcOffset?): ZonedDateTime = memScoped { - val epochSeconds = dateTime.toEpochSecond(UtcOffset.ZERO) - val offset = alloc() - offset.value = preferred?.totalSeconds ?: Int.MAX_VALUE - val transitionDuration = offset_at_datetime(tzid, epochSeconds, offset.ptr) - if (offset.value == Int.MAX_VALUE) { - throw RuntimeException("Unable to acquire the offset at $dateTime for zone ${this@RegionTimeZone}") - } - val correctedDateTime = try { - dateTime.plusSeconds(transitionDuration) - } catch (e: IllegalArgumentException) { - throw DateTimeArithmeticException("Overflow whet correcting the date-time to not be in the transition gap", e) - } catch (e: ArithmeticException) { - throw RuntimeException("Anomalously long timezone transition gap reported", e) - } - ZonedDateTime(correctedDateTime, this@RegionTimeZone, UtcOffset.ofSeconds(offset.value)) - } - - actual override fun offsetAtImpl(instant: Instant): UtcOffset { - val offset = offset_at_instant(tzid, instant.epochSeconds) - if (offset == Int.MAX_VALUE) { - throw RuntimeException("Unable to acquire the offset at instant $instant for zone $this") - } - return UtcOffset.ofSeconds(offset) - } - -} - -internal actual fun currentTime(): Instant = memScoped { - val seconds = alloc() - val nanoseconds = alloc() - val result = current_time(seconds.ptr, nanoseconds.ptr) - try { - require(result) - require(nanoseconds.value >= 0 && nanoseconds.value < NANOS_PER_ONE) - Instant(seconds.value, nanoseconds.value) - } catch (e: IllegalArgumentException) { - throw IllegalStateException("The readings from the system clock are not representable as an Instant") - } -} diff --git a/core/windows/cinterop/definitions.def b/core/windows/cinterop/definitions.def new file mode 100644 index 000000000..cf6f1e7a0 --- /dev/null +++ b/core/windows/cinterop/definitions.def @@ -0,0 +1 @@ +package = kotlinx.datetime.internal \ No newline at end of file diff --git a/core/windows/cinterop/definitions.h b/core/windows/cinterop/definitions.h new file mode 100644 index 000000000..ac87abbb9 --- /dev/null +++ b/core/windows/cinterop/definitions.h @@ -0,0 +1,11 @@ +#define WIN32_LEAN_AND_MEAN +#include +#include + +typedef struct _REG_TZI_FORMAT { + LONG Bias; + LONG StandardBias; + LONG DaylightBias; + SYSTEMTIME StandardDate; + SYSTEMTIME DaylightDate; +} REG_TZI_FORMAT; \ No newline at end of file diff --git a/core/windows/src/TimeZoneNative.kt b/core/windows/src/TimeZoneNative.kt new file mode 100644 index 000000000..95df051c9 --- /dev/null +++ b/core/windows/src/TimeZoneNative.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2019-2023 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:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) +package kotlinx.datetime + +import kotlinx.cinterop.* +import kotlinx.datetime.internal.* +import platform.posix.* +import platform.windows.* + +internal actual class RegionTimeZone(private val tzid: TimeZoneRules, actual override val id: String) : TimeZone() { + actual companion object { + actual fun of(zoneId: String): RegionTimeZone = try { + RegionTimeZone(tzdbInRegistry.rulesForId(zoneId), zoneId) + } catch (e: Exception) { + throw IllegalTimeZoneException("Invalid zone ID: $zoneId", e) + } + + actual fun currentSystemDefault(): RegionTimeZone { + val (name, zoneRules) = tzdbInRegistry.currentSystemDefault() + return RegionTimeZone(zoneRules, name) + } + + actual val availableZoneIds: Set + get() = tzdbInRegistry.availableTimeZoneIds() + } + + actual override fun atStartOfDay(date: LocalDate): Instant = memScoped { + val ldt = LocalDateTime(date, LocalTime.MIN) + when (val info = tzid.infoAtDatetime(ldt)) { + is OffsetInfo.Regular -> ldt.toInstant(info.offset) + is OffsetInfo.Gap -> info.start + is OffsetInfo.Overlap -> ldt.toInstant(info.offsetBefore) + } + } + + actual override fun atZone(dateTime: LocalDateTime, preferred: UtcOffset?): ZonedDateTime = + when (val info = tzid.infoAtDatetime(dateTime)) { + is OffsetInfo.Regular -> ZonedDateTime(dateTime, this, info.offset) + is OffsetInfo.Gap -> { + try { + ZonedDateTime(dateTime.plusSeconds(info.transitionDurationSeconds), this, info.offsetAfter) + } catch (e: IllegalArgumentException) { + throw DateTimeArithmeticException( + "Overflow whet correcting the date-time to not be in the transition gap", + e + ) + } + } + + is OffsetInfo.Overlap -> ZonedDateTime(dateTime, this, + if (info.offsetAfter == preferred) info.offsetAfter else info.offsetBefore) + } + + actual override fun offsetAtImpl(instant: Instant): UtcOffset = tzid.infoAtInstant(instant) +} + +private val tzdbInRegistry = TzdbInRegistry() + +internal actual fun currentTime(): Instant = memScoped { + val tm = alloc() + val error = clock_gettime(CLOCK_REALTIME, tm.ptr) + check(error == 0) { "Error when reading the system clock: ${strerror(errno)}" } + try { + require(tm.tv_nsec in 0 until NANOS_PER_ONE) + Instant(tm.tv_sec, tm.tv_nsec) + } catch (e: IllegalArgumentException) { + throw IllegalStateException("The readings from the system clock (${tm.tv_sec} seconds, ${tm.tv_nsec} nanoseconds) are not representable as an Instant") + } +} diff --git a/core/windows/src/TzdbInRegistry.kt b/core/windows/src/TzdbInRegistry.kt new file mode 100644 index 000000000..7a741ac16 --- /dev/null +++ b/core/windows/src/TzdbInRegistry.kt @@ -0,0 +1,215 @@ +/* + * Copyright 2019-2023 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:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) +package kotlinx.datetime + +import kotlinx.cinterop.* +import kotlinx.datetime.internal.* +import platform.windows.* + +internal class TzdbInRegistry { + + // TODO: starting version 1703 of Windows 10, the ICU library is also bundled, with more accurate/ timezone information. + // When Kotlin/Native drops support for Windows 7, we should investigate moving to the ICU. + private val windowsToRules: Map = buildMap { + processTimeZonesInRegistry { name, recurring, historic -> + val transitions = recurring.transitions?.let { listOf(it.first, it.second) } ?: emptyList() + val rules = if (historic.isEmpty()) { + TimeZoneRules(recurring.standardOffset, RecurringZoneRules(transitions)) + } else { + val transitionEpochSeconds = mutableListOf() + val offsets = mutableListOf(historic[0].second.standardOffset) + for ((year, record) in historic) { + if (record.transitions == null) continue + val (trans1, trans2) = record.transitions + val transTime1 = trans1.transitionDateTime.toInstant(year, trans1.offsetBefore) + val transTime2 = trans2.transitionDateTime.toInstant(year, trans2.offsetBefore) + if (transTime2 >= transTime1) { + transitionEpochSeconds.add(transTime1.epochSeconds) + offsets.add(trans1.offsetAfter) + transitionEpochSeconds.add(transTime2.epochSeconds) + offsets.add(trans2.offsetAfter) + } else { + transitionEpochSeconds.add(transTime2.epochSeconds) + offsets.add(trans2.offsetAfter) + transitionEpochSeconds.add(transTime1.epochSeconds) + offsets.add(trans1.offsetAfter) + } + } + TimeZoneRules(transitionEpochSeconds, offsets, RecurringZoneRules(transitions)) + } + put(name, rules) + } + } + + internal fun rulesForId(id: String): TimeZoneRules { + val standardName = standardToWindows[id] ?: throw IllegalTimeZoneException("Unknown time zone $id") + return windowsToRules[standardName] + ?: throw IllegalTimeZoneException("The rules for time zone $id are absent in the Windows registry") + } + + internal fun availableTimeZoneIds(): Set = standardToWindows.filter { + windowsToRules.containsKey(it.value) + }.keys + + internal fun currentSystemDefault(): Pair = memScoped { + val dtzi = alloc() + val result = GetDynamicTimeZoneInformation(dtzi.ptr) + check(result != TIME_ZONE_ID_INVALID) { "The current system time zone is invalid: ${getLastWindowsError()}" } + val windowsName = dtzi.TimeZoneKeyName.toKStringFromUtf16() + val ianaTzName = if (windowsName == "Coordinated Universal Time") "UTC" else windowsToStandard[windowsName] + ?: 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()) { + tz + } else { + // the user explicitly disabled DST transitions, so + TimeZoneRules(UtcOffset(minutes = -(dtzi.Bias + dtzi.StandardBias)), RecurringZoneRules(emptyList())) + } + } +} + +/* The maximum length of the registry key name for timezones. Taken from + https://docs.microsoft.com/en-us/windows/win32/api/timezoneapi/ns-timezoneapi-dynamic_time_zone_information + */ +private const val MAX_KEY_LENGTH = 128 + +internal fun processTimeZonesInRegistry(onTimeZone: (String, PerYearZoneRulesData, List>) -> Unit) { + memScoped { + withRegistryKey(HKEY_LOCAL_MACHINE!!, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones", { err -> + throw IllegalStateException("Error while opening the registry to fetch the time zones (err = $err): ${getLastWindowsError()}") + }) { hKey -> + var index = 0u + while (true) { + val windowsTzName = allocArray(MAX_KEY_LENGTH + 1) + val bufSize = alloc().apply { value = MAX_KEY_LENGTH.toUInt() + 1u } + when (RegEnumKeyExW(hKey, index++, windowsTzName, bufSize.ptr, null, null, null, null)) { + ERROR_SUCCESS -> { + withRegistryKey(hKey, windowsTzName.toKString(), { err -> + throw IllegalStateException( + "Error while opening the registry to fetch the time zone '${windowsTzName.toKString()} (err = $err)': ${getLastWindowsError()}" + ) + }) { tzHKey -> + // first, read the current data from the TZI value + val tziRecord = getRegistryValue(tzHKey, "TZI") + val historicData = try { + readHistoricDataFromRegistry(tzHKey) + } catch (e: IllegalStateException) { + emptyList() + } + onTimeZone(windowsTzName.toKString(), tziRecord.toZoneRules(), historicData) + } + } + ERROR_MORE_DATA -> throw IllegalStateException("The name of a time zone in the registry was too long") + ERROR_NO_MORE_ITEMS -> break + else -> throw IllegalStateException("Error when reading a time zone from the registry: ${getLastWindowsError()}") + } + } + } + } +} + +/** + * Reads the historic data in the "Dynamic DST" subkey, if present. + * + * [tzHKey] is an open registry key pointing to the timezone record. + * + * Returns pairs of years and the corresponding rules in effect for those years. + * + * @throws IllegalStateException if the 'Dynamic DST' key is present but malformed. + */ +private fun MemScope.readHistoricDataFromRegistry(tzHKey: HKEY): List> { + return withRegistryKey(tzHKey, "Dynamic DST", { emptyList() }) { dynDstHKey -> + val firstEntry = getRegistryValue(dynDstHKey, "FirstEntry").value.toInt() + val lastEntry = getRegistryValue(dynDstHKey, "LastEntry").value.toInt() + (firstEntry..lastEntry).map { year -> + year to getRegistryValue(dynDstHKey, year.toString()).toZoneRules() + } + } +} + +private inline fun MemScope.withRegistryKey(hKey: HKEY, subKeyName: String, onError: (Int) -> T, block: (HKEY) -> T): T { + val subHKey: HKEYVar = alloc() + val err = RegOpenKeyExW(hKey, subKeyName, 0u, KEY_READ.toUInt(), subHKey.ptr) + return if (err != ERROR_SUCCESS) { onError(err) } else { + try { + block(subHKey.value!!) + } finally { + RegCloseKey(subHKey.value) + } + } +} + +private inline fun MemScope.getRegistryValue(hKey: HKEY, valueName: String): T { + val buffer = alloc() + val cbData = alloc().apply { value = sizeOf().convert() } + val err = RegQueryValueExW(hKey, valueName, null, null, buffer.reinterpret().ptr, cbData.ptr) + check(err == ERROR_SUCCESS) { "The expected Windows registry value '$valueName' could not be accessed (err = $err)': ${getLastWindowsError()}" } + check(cbData.value.toLong() == sizeOf()) { "Expected '$valueName' to have size ${sizeOf()}, but got ${cbData.value}" } + return buffer +} + +private fun _REG_TZI_FORMAT.toZoneRules(): PerYearZoneRulesData { + val standardOffset = UtcOffset(minutes = -(StandardBias + Bias)) + val daylightOffset = UtcOffset(minutes = -(DaylightBias + Bias)) + if (DaylightDate.wMonth == 0.convert()) { + return PerYearZoneRulesData(standardOffset, null) + } + val changeToDst = RecurringZoneRules.Rule(DaylightDate.toMonthDayTime(), standardOffset, daylightOffset) + val changeToStd = RecurringZoneRules.Rule(StandardDate.toMonthDayTime(), daylightOffset, standardOffset) + return PerYearZoneRulesData(standardOffset, changeToDst to changeToStd) +} + +/* this code is explained at +https://docs.microsoft.com/en-us/windows/win32/api/timezoneapi/ns-timezoneapi-time_zone_information +in the section about `StandardDate`. +In short, the `StandardDate` structure uses `SYSTEMTIME` in a... non-conventional way. +This function translates that representation to one representing a proper date at a given year. +*/ +private fun SYSTEMTIME.toMonthDayTime(): MonthDayTime { + val month = Month(wMonth.toInt()) + val localTime = if (wHour.toInt() == 23 && wMinute.toInt() == 59 && wSecond.toInt() == 59 && wMilliseconds.toInt() == 999) { + MonthDayTime.TransitionLocaltime(24, 0, 0) + } else { + MonthDayTime.TransitionLocaltime(wHour.toInt(), wMinute.toInt(), wSecond.toInt()) + } + val transitionDay: MonthDayOfYear.TransitionDay = if (wYear != 0.toUShort()) { + // if the year is not 0, this is the absolute time. + MonthDayOfYear.TransitionDay.ExactlyDayOfMonth(wDay.toInt()) + } else { + /* otherwise, the transition happens yearly at the specified month at the specified day of the week. */ + // The number of the occurrence of the specified day of week in the month, or the special value "5" to denote + // the last such occurrence. + val dowOccurrenceNumber = wDay.toInt() + val dayOfWeek = if (wDayOfWeek == 0.toUShort()) DayOfWeek.SUNDAY else DayOfWeek(wDayOfWeek.toInt()) + if (dowOccurrenceNumber == 5) MonthDayOfYear.TransitionDay.Last(dayOfWeek, null) + else MonthDayOfYear.TransitionDay.Nth(dayOfWeek, dowOccurrenceNumber) + } + return MonthDayTime(MonthDayOfYear(month, transitionDay), localTime, MonthDayTime.OffsetResolver.WallClockOffset) +} + +internal class PerYearZoneRulesData( + val standardOffset: UtcOffset, + val transitions: Pair, RecurringZoneRules.Rule>?, +) { + override fun toString(): String = "standard offset is $standardOffset" + (transitions?.let { + ", the transitions: ${it.first}, ${it.second}" + } ?: "") +} + +private fun getLastWindowsError(): String = memScoped { + val buf = alloc>() + FormatMessage!!( + (FORMAT_MESSAGE_ALLOCATE_BUFFER or FORMAT_MESSAGE_FROM_SYSTEM or FORMAT_MESSAGE_IGNORE_INSERTS).toUInt(), + null, + GetLastError(), + 0u, + buf.ptr.reinterpret(), + 0u, + null, + ) + buf.value!!.toKStringFromUtf16().also { LocalFree(buf.ptr) } +} diff --git a/core/windows/src/WindowsZoneNames.kt b/core/windows/src/WindowsZoneNames.kt new file mode 100644 index 000000000..2396f074a --- /dev/null +++ b/core/windows/src/WindowsZoneNames.kt @@ -0,0 +1,606 @@ +// generated with gradle task `downloadWindowsZonesMapping` +package kotlinx.datetime +internal val standardToWindows: Map = mutableMapOf( + "Africa/Abidjan" to "Greenwich Standard Time", + "Africa/Accra" to "Greenwich Standard Time", + "Africa/Addis_Ababa" to "E. Africa Standard Time", + "Africa/Algiers" to "W. Central Africa Standard Time", + "Africa/Asmera" to "E. Africa Standard Time", + "Africa/Bamako" to "Greenwich Standard Time", + "Africa/Bangui" to "W. Central Africa Standard Time", + "Africa/Banjul" to "Greenwich Standard Time", + "Africa/Bissau" to "Greenwich Standard Time", + "Africa/Blantyre" to "South Africa Standard Time", + "Africa/Brazzaville" to "W. Central Africa Standard Time", + "Africa/Bujumbura" to "South Africa Standard Time", + "Africa/Cairo" to "Egypt Standard Time", + "Africa/Casablanca" to "Morocco Standard Time", + "Africa/Ceuta" to "Romance Standard Time", + "Africa/Conakry" to "Greenwich Standard Time", + "Africa/Dakar" to "Greenwich Standard Time", + "Africa/Dar_es_Salaam" to "E. Africa Standard Time", + "Africa/Djibouti" to "E. Africa Standard Time", + "Africa/Douala" to "W. Central Africa Standard Time", + "Africa/El_Aaiun" to "Morocco Standard Time", + "Africa/Freetown" to "Greenwich Standard Time", + "Africa/Gaborone" to "South Africa Standard Time", + "Africa/Harare" to "South Africa Standard Time", + "Africa/Johannesburg" to "South Africa Standard Time", + "Africa/Juba" to "South Sudan Standard Time", + "Africa/Kampala" to "E. Africa Standard Time", + "Africa/Khartoum" to "Sudan Standard Time", + "Africa/Kigali" to "South Africa Standard Time", + "Africa/Kinshasa" to "W. Central Africa Standard Time", + "Africa/Lagos" to "W. Central Africa Standard Time", + "Africa/Libreville" to "W. Central Africa Standard Time", + "Africa/Lome" to "Greenwich Standard Time", + "Africa/Luanda" to "W. Central Africa Standard Time", + "Africa/Lubumbashi" to "South Africa Standard Time", + "Africa/Lusaka" to "South Africa Standard Time", + "Africa/Malabo" to "W. Central Africa Standard Time", + "Africa/Maputo" to "South Africa Standard Time", + "Africa/Maseru" to "South Africa Standard Time", + "Africa/Mbabane" to "South Africa Standard Time", + "Africa/Mogadishu" to "E. Africa Standard Time", + "Africa/Monrovia" to "Greenwich Standard Time", + "Africa/Nairobi" to "E. Africa Standard Time", + "Africa/Ndjamena" to "W. Central Africa Standard Time", + "Africa/Niamey" to "W. Central Africa Standard Time", + "Africa/Nouakchott" to "Greenwich Standard Time", + "Africa/Ouagadougou" to "Greenwich Standard Time", + "Africa/Porto-Novo" to "W. Central Africa Standard Time", + "Africa/Sao_Tome" to "Sao Tome Standard Time", + "Africa/Tripoli" to "Libya Standard Time", + "Africa/Tunis" to "W. Central Africa Standard Time", + "Africa/Windhoek" to "Namibia Standard Time", + "America/Adak" to "Aleutian Standard Time", + "America/Anchorage" to "Alaskan Standard Time", + "America/Anguilla" to "SA Western Standard Time", + "America/Antigua" to "SA Western Standard Time", + "America/Araguaina" to "Tocantins Standard Time", + "America/Argentina/La_Rioja" to "Argentina Standard Time", + "America/Argentina/Rio_Gallegos" to "Argentina Standard Time", + "America/Argentina/Salta" to "Argentina Standard Time", + "America/Argentina/San_Juan" to "Argentina Standard Time", + "America/Argentina/San_Luis" to "Argentina Standard Time", + "America/Argentina/Tucuman" to "Argentina Standard Time", + "America/Argentina/Ushuaia" to "Argentina Standard Time", + "America/Aruba" to "SA Western Standard Time", + "America/Asuncion" to "Paraguay Standard Time", + "America/Bahia" to "Bahia Standard Time", + "America/Bahia_Banderas" to "Central Standard Time (Mexico)", + "America/Barbados" to "SA Western Standard Time", + "America/Belem" to "SA Eastern Standard Time", + "America/Belize" to "Central America Standard Time", + "America/Blanc-Sablon" to "SA Western Standard Time", + "America/Boa_Vista" to "SA Western Standard Time", + "America/Bogota" to "SA Pacific Standard Time", + "America/Boise" to "Mountain Standard Time", + "America/Buenos_Aires" to "Argentina Standard Time", + "America/Cambridge_Bay" to "Mountain Standard Time", + "America/Campo_Grande" to "Central Brazilian Standard Time", + "America/Cancun" to "Eastern Standard Time (Mexico)", + "America/Caracas" to "Venezuela Standard Time", + "America/Catamarca" to "Argentina Standard Time", + "America/Cayenne" to "SA Eastern Standard Time", + "America/Cayman" to "SA Pacific Standard Time", + "America/Chicago" to "Central Standard Time", + "America/Chihuahua" to "Central Standard Time (Mexico)", + "America/Ciudad_Juarez" to "Mountain Standard Time", + "America/Coral_Harbour" to "SA Pacific Standard Time", + "America/Cordoba" to "Argentina Standard Time", + "America/Costa_Rica" to "Central America Standard Time", + "America/Creston" to "US Mountain Standard Time", + "America/Cuiaba" to "Central Brazilian Standard Time", + "America/Curacao" to "SA Western Standard Time", + "America/Danmarkshavn" to "Greenwich Standard Time", + "America/Dawson" to "Yukon Standard Time", + "America/Dawson_Creek" to "US Mountain Standard Time", + "America/Denver" to "Mountain Standard Time", + "America/Detroit" to "Eastern Standard Time", + "America/Dominica" to "SA Western Standard Time", + "America/Edmonton" to "Mountain Standard Time", + "America/Eirunepe" to "SA Pacific Standard Time", + "America/El_Salvador" to "Central America Standard Time", + "America/Fort_Nelson" to "US Mountain Standard Time", + "America/Fortaleza" to "SA Eastern Standard Time", + "America/Glace_Bay" to "Atlantic Standard Time", + "America/Godthab" to "Greenland Standard Time", + "America/Goose_Bay" to "Atlantic Standard Time", + "America/Grand_Turk" to "Turks And Caicos Standard Time", + "America/Grenada" to "SA Western Standard Time", + "America/Guadeloupe" to "SA Western Standard Time", + "America/Guatemala" to "Central America Standard Time", + "America/Guayaquil" to "SA Pacific Standard Time", + "America/Guyana" to "SA Western Standard Time", + "America/Halifax" to "Atlantic Standard Time", + "America/Havana" to "Cuba Standard Time", + "America/Hermosillo" to "US Mountain Standard Time", + "America/Indiana/Knox" to "Central Standard Time", + "America/Indiana/Marengo" to "US Eastern Standard Time", + "America/Indiana/Petersburg" to "Eastern Standard Time", + "America/Indiana/Tell_City" to "Central Standard Time", + "America/Indiana/Vevay" to "US Eastern Standard Time", + "America/Indiana/Vincennes" to "Eastern Standard Time", + "America/Indiana/Winamac" to "Eastern Standard Time", + "America/Indianapolis" to "US Eastern Standard Time", + "America/Inuvik" to "Mountain Standard Time", + "America/Iqaluit" to "Eastern Standard Time", + "America/Jamaica" to "SA Pacific Standard Time", + "America/Jujuy" to "Argentina Standard Time", + "America/Juneau" to "Alaskan Standard Time", + "America/Kentucky/Monticello" to "Eastern Standard Time", + "America/Kralendijk" to "SA Western Standard Time", + "America/La_Paz" to "SA Western Standard Time", + "America/Lima" to "SA Pacific Standard Time", + "America/Los_Angeles" to "Pacific Standard Time", + "America/Louisville" to "Eastern Standard Time", + "America/Lower_Princes" to "SA Western Standard Time", + "America/Maceio" to "SA Eastern Standard Time", + "America/Managua" to "Central America Standard Time", + "America/Manaus" to "SA Western Standard Time", + "America/Marigot" to "SA Western Standard Time", + "America/Martinique" to "SA Western Standard Time", + "America/Matamoros" to "Central Standard Time", + "America/Mazatlan" to "Mountain Standard Time (Mexico)", + "America/Mendoza" to "Argentina Standard Time", + "America/Menominee" to "Central Standard Time", + "America/Merida" to "Central Standard Time (Mexico)", + "America/Metlakatla" to "Alaskan Standard Time", + "America/Mexico_City" to "Central Standard Time (Mexico)", + "America/Miquelon" to "Saint Pierre Standard Time", + "America/Moncton" to "Atlantic Standard Time", + "America/Monterrey" to "Central Standard Time (Mexico)", + "America/Montevideo" to "Montevideo Standard Time", + "America/Montreal" to "Eastern Standard Time", + "America/Montserrat" to "SA Western Standard Time", + "America/Nassau" to "Eastern Standard Time", + "America/New_York" to "Eastern Standard Time", + "America/Nipigon" to "Eastern Standard Time", + "America/Nome" to "Alaskan Standard Time", + "America/Noronha" to "UTC-02", + "America/North_Dakota/Beulah" to "Central Standard Time", + "America/North_Dakota/Center" to "Central Standard Time", + "America/North_Dakota/New_Salem" to "Central Standard Time", + "America/Ojinaga" to "Central Standard Time", + "America/Panama" to "SA Pacific Standard Time", + "America/Pangnirtung" to "Eastern Standard Time", + "America/Paramaribo" to "SA Eastern Standard Time", + "America/Phoenix" to "US Mountain Standard Time", + "America/Port-au-Prince" to "Haiti Standard Time", + "America/Port_of_Spain" to "SA Western Standard Time", + "America/Porto_Velho" to "SA Western Standard Time", + "America/Puerto_Rico" to "SA Western Standard Time", + "America/Punta_Arenas" to "Magallanes Standard Time", + "America/Rainy_River" to "Central Standard Time", + "America/Rankin_Inlet" to "Central Standard Time", + "America/Recife" to "SA Eastern Standard Time", + "America/Regina" to "Canada Central Standard Time", + "America/Resolute" to "Central Standard Time", + "America/Rio_Branco" to "SA Pacific Standard Time", + "America/Santa_Isabel" to "Pacific Standard Time (Mexico)", + "America/Santarem" to "SA Eastern Standard Time", + "America/Santiago" to "Pacific SA Standard Time", + "America/Santo_Domingo" to "SA Western Standard Time", + "America/Sao_Paulo" to "E. South America Standard Time", + "America/Scoresbysund" to "Azores Standard Time", + "America/Sitka" to "Alaskan Standard Time", + "America/St_Barthelemy" to "SA Western Standard Time", + "America/St_Johns" to "Newfoundland Standard Time", + "America/St_Kitts" to "SA Western Standard Time", + "America/St_Lucia" to "SA Western Standard Time", + "America/St_Thomas" to "SA Western Standard Time", + "America/St_Vincent" to "SA Western Standard Time", + "America/Swift_Current" to "Canada Central Standard Time", + "America/Tegucigalpa" to "Central America Standard Time", + "America/Thule" to "Atlantic Standard Time", + "America/Thunder_Bay" to "Eastern Standard Time", + "America/Tijuana" to "Pacific Standard Time (Mexico)", + "America/Toronto" to "Eastern Standard Time", + "America/Tortola" to "SA Western Standard Time", + "America/Vancouver" to "Pacific Standard Time", + "America/Whitehorse" to "Yukon Standard Time", + "America/Winnipeg" to "Central Standard Time", + "America/Yakutat" to "Alaskan Standard Time", + "America/Yellowknife" to "Mountain Standard Time", + "Antarctica/Casey" to "Central Pacific Standard Time", + "Antarctica/Davis" to "SE Asia Standard Time", + "Antarctica/DumontDUrville" to "West Pacific Standard Time", + "Antarctica/Macquarie" to "Tasmania Standard Time", + "Antarctica/Mawson" to "West Asia Standard Time", + "Antarctica/McMurdo" to "New Zealand Standard Time", + "Antarctica/Palmer" to "SA Eastern Standard Time", + "Antarctica/Rothera" to "SA Eastern Standard Time", + "Antarctica/Syowa" to "E. Africa Standard Time", + "Antarctica/Vostok" to "Central Asia Standard Time", + "Arctic/Longyearbyen" to "W. Europe Standard Time", + "Asia/Aden" to "Arab Standard Time", + "Asia/Almaty" to "Central Asia Standard Time", + "Asia/Amman" to "Jordan Standard Time", + "Asia/Anadyr" to "Russia Time Zone 11", + "Asia/Aqtau" to "West Asia Standard Time", + "Asia/Aqtobe" to "West Asia Standard Time", + "Asia/Ashgabat" to "West Asia Standard Time", + "Asia/Atyrau" to "West Asia Standard Time", + "Asia/Baghdad" to "Arabic Standard Time", + "Asia/Bahrain" to "Arab Standard Time", + "Asia/Baku" to "Azerbaijan Standard Time", + "Asia/Bangkok" to "SE Asia Standard Time", + "Asia/Barnaul" to "Altai Standard Time", + "Asia/Beirut" to "Middle East Standard Time", + "Asia/Bishkek" to "Central Asia Standard Time", + "Asia/Brunei" to "Singapore Standard Time", + "Asia/Calcutta" to "India Standard Time", + "Asia/Chita" to "Transbaikal Standard Time", + "Asia/Choibalsan" to "Ulaanbaatar Standard Time", + "Asia/Colombo" to "Sri Lanka Standard Time", + "Asia/Damascus" to "Syria Standard Time", + "Asia/Dhaka" to "Bangladesh Standard Time", + "Asia/Dili" to "Tokyo Standard Time", + "Asia/Dubai" to "Arabian Standard Time", + "Asia/Dushanbe" to "West Asia Standard Time", + "Asia/Famagusta" to "GTB Standard Time", + "Asia/Gaza" to "West Bank Standard Time", + "Asia/Hebron" to "West Bank Standard Time", + "Asia/Hong_Kong" to "China Standard Time", + "Asia/Hovd" to "W. Mongolia Standard Time", + "Asia/Irkutsk" to "North Asia East Standard Time", + "Asia/Jakarta" to "SE Asia Standard Time", + "Asia/Jayapura" to "Tokyo Standard Time", + "Asia/Jerusalem" to "Israel Standard Time", + "Asia/Kabul" to "Afghanistan Standard Time", + "Asia/Kamchatka" to "Russia Time Zone 11", + "Asia/Karachi" to "Pakistan Standard Time", + "Asia/Katmandu" to "Nepal Standard Time", + "Asia/Khandyga" to "Yakutsk Standard Time", + "Asia/Krasnoyarsk" to "North Asia Standard Time", + "Asia/Kuala_Lumpur" to "Singapore Standard Time", + "Asia/Kuching" to "Singapore Standard Time", + "Asia/Kuwait" to "Arab Standard Time", + "Asia/Macau" to "China Standard Time", + "Asia/Magadan" to "Magadan Standard Time", + "Asia/Makassar" to "Singapore Standard Time", + "Asia/Manila" to "Singapore Standard Time", + "Asia/Muscat" to "Arabian Standard Time", + "Asia/Nicosia" to "GTB Standard Time", + "Asia/Novokuznetsk" to "North Asia Standard Time", + "Asia/Novosibirsk" to "N. Central Asia Standard Time", + "Asia/Omsk" to "Omsk Standard Time", + "Asia/Oral" to "West Asia Standard Time", + "Asia/Phnom_Penh" to "SE Asia Standard Time", + "Asia/Pontianak" to "SE Asia Standard Time", + "Asia/Pyongyang" to "North Korea Standard Time", + "Asia/Qatar" to "Arab Standard Time", + "Asia/Qostanay" to "Central Asia Standard Time", + "Asia/Qyzylorda" to "Qyzylorda Standard Time", + "Asia/Rangoon" to "Myanmar Standard Time", + "Asia/Riyadh" to "Arab Standard Time", + "Asia/Saigon" to "SE Asia Standard Time", + "Asia/Sakhalin" to "Sakhalin Standard Time", + "Asia/Samarkand" to "West Asia Standard Time", + "Asia/Seoul" to "Korea Standard Time", + "Asia/Shanghai" to "China Standard Time", + "Asia/Singapore" to "Singapore Standard Time", + "Asia/Srednekolymsk" to "Russia Time Zone 10", + "Asia/Taipei" to "Taipei Standard Time", + "Asia/Tashkent" to "West Asia Standard Time", + "Asia/Tbilisi" to "Georgian Standard Time", + "Asia/Tehran" to "Iran Standard Time", + "Asia/Thimphu" to "Bangladesh Standard Time", + "Asia/Tokyo" to "Tokyo Standard Time", + "Asia/Tomsk" to "Tomsk Standard Time", + "Asia/Ulaanbaatar" to "Ulaanbaatar Standard Time", + "Asia/Urumqi" to "Central Asia Standard Time", + "Asia/Ust-Nera" to "Vladivostok Standard Time", + "Asia/Vientiane" to "SE Asia Standard Time", + "Asia/Vladivostok" to "Vladivostok Standard Time", + "Asia/Yakutsk" to "Yakutsk Standard Time", + "Asia/Yekaterinburg" to "Ekaterinburg Standard Time", + "Asia/Yerevan" to "Caucasus Standard Time", + "Atlantic/Azores" to "Azores Standard Time", + "Atlantic/Bermuda" to "Atlantic Standard Time", + "Atlantic/Canary" to "GMT Standard Time", + "Atlantic/Cape_Verde" to "Cape Verde Standard Time", + "Atlantic/Faeroe" to "GMT Standard Time", + "Atlantic/Madeira" to "GMT Standard Time", + "Atlantic/Reykjavik" to "Greenwich Standard Time", + "Atlantic/South_Georgia" to "UTC-02", + "Atlantic/St_Helena" to "Greenwich Standard Time", + "Atlantic/Stanley" to "SA Eastern Standard Time", + "Australia/Adelaide" to "Cen. Australia Standard Time", + "Australia/Brisbane" to "E. Australia Standard Time", + "Australia/Broken_Hill" to "Cen. Australia Standard Time", + "Australia/Currie" to "Tasmania Standard Time", + "Australia/Darwin" to "AUS Central Standard Time", + "Australia/Eucla" to "Aus Central W. Standard Time", + "Australia/Hobart" to "Tasmania Standard Time", + "Australia/Lindeman" to "E. Australia Standard Time", + "Australia/Lord_Howe" to "Lord Howe Standard Time", + "Australia/Melbourne" to "AUS Eastern Standard Time", + "Australia/Perth" to "W. Australia Standard Time", + "Australia/Sydney" to "AUS Eastern Standard Time", + "CST6CDT" to "Central Standard Time", + "EST5EDT" to "Eastern Standard Time", + "Etc/GMT" to "UTC", + "Etc/GMT+1" to "Cape Verde Standard Time", + "Etc/GMT+10" to "Hawaiian Standard Time", + "Etc/GMT+11" to "UTC-11", + "Etc/GMT+12" to "Dateline Standard Time", + "Etc/GMT+2" to "UTC-02", + "Etc/GMT+3" to "SA Eastern Standard Time", + "Etc/GMT+4" to "SA Western Standard Time", + "Etc/GMT+5" to "SA Pacific Standard Time", + "Etc/GMT+6" to "Central America Standard Time", + "Etc/GMT+7" to "US Mountain Standard Time", + "Etc/GMT+8" to "UTC-08", + "Etc/GMT+9" to "UTC-09", + "Etc/GMT-1" to "W. Central Africa Standard Time", + "Etc/GMT-10" to "West Pacific Standard Time", + "Etc/GMT-11" to "Central Pacific Standard Time", + "Etc/GMT-12" to "UTC+12", + "Etc/GMT-13" to "UTC+13", + "Etc/GMT-14" to "Line Islands Standard Time", + "Etc/GMT-2" to "South Africa Standard Time", + "Etc/GMT-3" to "E. Africa Standard Time", + "Etc/GMT-4" to "Arabian Standard Time", + "Etc/GMT-5" to "West Asia Standard Time", + "Etc/GMT-6" to "Central Asia Standard Time", + "Etc/GMT-7" to "SE Asia Standard Time", + "Etc/GMT-8" to "Singapore Standard Time", + "Etc/GMT-9" to "Tokyo Standard Time", + "Etc/UTC" to "UTC", + "Europe/Amsterdam" to "W. Europe Standard Time", + "Europe/Andorra" to "W. Europe Standard Time", + "Europe/Astrakhan" to "Astrakhan Standard Time", + "Europe/Athens" to "GTB Standard Time", + "Europe/Belgrade" to "Central Europe Standard Time", + "Europe/Berlin" to "W. Europe Standard Time", + "Europe/Bratislava" to "Central Europe Standard Time", + "Europe/Brussels" to "Romance Standard Time", + "Europe/Bucharest" to "GTB Standard Time", + "Europe/Budapest" to "Central Europe Standard Time", + "Europe/Busingen" to "W. Europe Standard Time", + "Europe/Chisinau" to "E. Europe Standard Time", + "Europe/Copenhagen" to "Romance Standard Time", + "Europe/Dublin" to "GMT Standard Time", + "Europe/Gibraltar" to "W. Europe Standard Time", + "Europe/Guernsey" to "GMT Standard Time", + "Europe/Helsinki" to "FLE Standard Time", + "Europe/Isle_of_Man" to "GMT Standard Time", + "Europe/Istanbul" to "Turkey Standard Time", + "Europe/Jersey" to "GMT Standard Time", + "Europe/Kaliningrad" to "Kaliningrad Standard Time", + "Europe/Kiev" to "FLE Standard Time", + "Europe/Kirov" to "Russian Standard Time", + "Europe/Lisbon" to "GMT Standard Time", + "Europe/Ljubljana" to "Central Europe Standard Time", + "Europe/London" to "GMT Standard Time", + "Europe/Luxembourg" to "W. Europe Standard Time", + "Europe/Madrid" to "Romance Standard Time", + "Europe/Malta" to "W. Europe Standard Time", + "Europe/Mariehamn" to "FLE Standard Time", + "Europe/Minsk" to "Belarus Standard Time", + "Europe/Monaco" to "W. Europe Standard Time", + "Europe/Moscow" to "Russian Standard Time", + "Europe/Oslo" to "W. Europe Standard Time", + "Europe/Paris" to "Romance Standard Time", + "Europe/Podgorica" to "Central Europe Standard Time", + "Europe/Prague" to "Central Europe Standard Time", + "Europe/Riga" to "FLE Standard Time", + "Europe/Rome" to "W. Europe Standard Time", + "Europe/Samara" to "Russia Time Zone 3", + "Europe/San_Marino" to "W. Europe Standard Time", + "Europe/Sarajevo" to "Central European Standard Time", + "Europe/Saratov" to "Saratov Standard Time", + "Europe/Simferopol" to "Russian Standard Time", + "Europe/Skopje" to "Central European Standard Time", + "Europe/Sofia" to "FLE Standard Time", + "Europe/Stockholm" to "W. Europe Standard Time", + "Europe/Tallinn" to "FLE Standard Time", + "Europe/Tirane" to "Central Europe Standard Time", + "Europe/Ulyanovsk" to "Astrakhan Standard Time", + "Europe/Uzhgorod" to "FLE Standard Time", + "Europe/Vaduz" to "W. Europe Standard Time", + "Europe/Vatican" to "W. Europe Standard Time", + "Europe/Vienna" to "W. Europe Standard Time", + "Europe/Vilnius" to "FLE Standard Time", + "Europe/Volgograd" to "Volgograd Standard Time", + "Europe/Warsaw" to "Central European Standard Time", + "Europe/Zagreb" to "Central European Standard Time", + "Europe/Zaporozhye" to "FLE Standard Time", + "Europe/Zurich" to "W. Europe Standard Time", + "Indian/Antananarivo" to "E. Africa Standard Time", + "Indian/Chagos" to "Central Asia Standard Time", + "Indian/Christmas" to "SE Asia Standard Time", + "Indian/Cocos" to "Myanmar Standard Time", + "Indian/Comoro" to "E. Africa Standard Time", + "Indian/Kerguelen" to "West Asia Standard Time", + "Indian/Mahe" to "Mauritius Standard Time", + "Indian/Maldives" to "West Asia Standard Time", + "Indian/Mauritius" to "Mauritius Standard Time", + "Indian/Mayotte" to "E. Africa Standard Time", + "Indian/Reunion" to "Mauritius Standard Time", + "MST7MDT" to "Mountain Standard Time", + "PST8PDT" to "Pacific Standard Time", + "Pacific/Apia" to "Samoa Standard Time", + "Pacific/Auckland" to "New Zealand Standard Time", + "Pacific/Bougainville" to "Bougainville Standard Time", + "Pacific/Chatham" to "Chatham Islands Standard Time", + "Pacific/Easter" to "Easter Island Standard Time", + "Pacific/Efate" to "Central Pacific Standard Time", + "Pacific/Enderbury" to "UTC+13", + "Pacific/Fakaofo" to "UTC+13", + "Pacific/Fiji" to "Fiji Standard Time", + "Pacific/Funafuti" to "UTC+12", + "Pacific/Galapagos" to "Central America Standard Time", + "Pacific/Gambier" to "UTC-09", + "Pacific/Guadalcanal" to "Central Pacific Standard Time", + "Pacific/Guam" to "West Pacific Standard Time", + "Pacific/Honolulu" to "Hawaiian Standard Time", + "Pacific/Johnston" to "Hawaiian Standard Time", + "Pacific/Kiritimati" to "Line Islands Standard Time", + "Pacific/Kosrae" to "Central Pacific Standard Time", + "Pacific/Kwajalein" to "UTC+12", + "Pacific/Majuro" to "UTC+12", + "Pacific/Marquesas" to "Marquesas Standard Time", + "Pacific/Midway" to "UTC-11", + "Pacific/Nauru" to "UTC+12", + "Pacific/Niue" to "UTC-11", + "Pacific/Norfolk" to "Norfolk Standard Time", + "Pacific/Noumea" to "Central Pacific Standard Time", + "Pacific/Pago_Pago" to "UTC-11", + "Pacific/Palau" to "Tokyo Standard Time", + "Pacific/Pitcairn" to "UTC-08", + "Pacific/Ponape" to "Central Pacific Standard Time", + "Pacific/Port_Moresby" to "West Pacific Standard Time", + "Pacific/Rarotonga" to "Hawaiian Standard Time", + "Pacific/Saipan" to "West Pacific Standard Time", + "Pacific/Tahiti" to "Hawaiian Standard Time", + "Pacific/Tarawa" to "UTC+12", + "Pacific/Tongatapu" to "Tonga Standard Time", + "Pacific/Truk" to "West Pacific Standard Time", + "Pacific/Wake" to "UTC+12", + "Pacific/Wallis" to "UTC+12", + "UTC" to "UTC", +) +internal val windowsToStandard: Map = mutableMapOf( + "AUS Central Standard Time" to "Australia/Darwin", + "AUS Eastern Standard Time" to "Australia/Sydney", + "Afghanistan Standard Time" to "Asia/Kabul", + "Alaskan Standard Time" to "America/Anchorage", + "Aleutian Standard Time" to "America/Adak", + "Altai Standard Time" to "Asia/Barnaul", + "Arab Standard Time" to "Asia/Riyadh", + "Arabian Standard Time" to "Asia/Dubai", + "Arabic Standard Time" to "Asia/Baghdad", + "Argentina Standard Time" to "America/Buenos_Aires", + "Astrakhan Standard Time" to "Europe/Astrakhan", + "Atlantic Standard Time" to "America/Halifax", + "Aus Central W. Standard Time" to "Australia/Eucla", + "Azerbaijan Standard Time" to "Asia/Baku", + "Azores Standard Time" to "Atlantic/Azores", + "Bahia Standard Time" to "America/Bahia", + "Bangladesh Standard Time" to "Asia/Dhaka", + "Belarus Standard Time" to "Europe/Minsk", + "Bougainville Standard Time" to "Pacific/Bougainville", + "Canada Central Standard Time" to "America/Regina", + "Cape Verde Standard Time" to "Atlantic/Cape_Verde", + "Caucasus Standard Time" to "Asia/Yerevan", + "Cen. Australia Standard Time" to "Australia/Adelaide", + "Central America Standard Time" to "America/Guatemala", + "Central Asia Standard Time" to "Asia/Almaty", + "Central Brazilian Standard Time" to "America/Cuiaba", + "Central Europe Standard Time" to "Europe/Budapest", + "Central European Standard Time" to "Europe/Warsaw", + "Central Pacific Standard Time" to "Pacific/Guadalcanal", + "Central Standard Time" to "America/Chicago", + "Central Standard Time (Mexico)" to "America/Mexico_City", + "Chatham Islands Standard Time" to "Pacific/Chatham", + "China Standard Time" to "Asia/Shanghai", + "Cuba Standard Time" to "America/Havana", + "Dateline Standard Time" to "Etc/GMT+12", + "E. Africa Standard Time" to "Africa/Nairobi", + "E. Australia Standard Time" to "Australia/Brisbane", + "E. Europe Standard Time" to "Europe/Chisinau", + "E. South America Standard Time" to "America/Sao_Paulo", + "Easter Island Standard Time" to "Pacific/Easter", + "Eastern Standard Time" to "America/New_York", + "Eastern Standard Time (Mexico)" to "America/Cancun", + "Egypt Standard Time" to "Africa/Cairo", + "Ekaterinburg Standard Time" to "Asia/Yekaterinburg", + "FLE Standard Time" to "Europe/Kiev", + "Fiji Standard Time" to "Pacific/Fiji", + "GMT Standard Time" to "Europe/London", + "GTB Standard Time" to "Europe/Bucharest", + "Georgian Standard Time" to "Asia/Tbilisi", + "Greenland Standard Time" to "America/Godthab", + "Greenwich Standard Time" to "Atlantic/Reykjavik", + "Haiti Standard Time" to "America/Port-au-Prince", + "Hawaiian Standard Time" to "Pacific/Honolulu", + "India Standard Time" to "Asia/Calcutta", + "Iran Standard Time" to "Asia/Tehran", + "Israel Standard Time" to "Asia/Jerusalem", + "Jordan Standard Time" to "Asia/Amman", + "Kaliningrad Standard Time" to "Europe/Kaliningrad", + "Korea Standard Time" to "Asia/Seoul", + "Libya Standard Time" to "Africa/Tripoli", + "Line Islands Standard Time" to "Pacific/Kiritimati", + "Lord Howe Standard Time" to "Australia/Lord_Howe", + "Magadan Standard Time" to "Asia/Magadan", + "Magallanes Standard Time" to "America/Punta_Arenas", + "Marquesas Standard Time" to "Pacific/Marquesas", + "Mauritius Standard Time" to "Indian/Mauritius", + "Middle East Standard Time" to "Asia/Beirut", + "Montevideo Standard Time" to "America/Montevideo", + "Morocco Standard Time" to "Africa/Casablanca", + "Mountain Standard Time" to "America/Denver", + "Mountain Standard Time (Mexico)" to "America/Mazatlan", + "Myanmar Standard Time" to "Asia/Rangoon", + "N. Central Asia Standard Time" to "Asia/Novosibirsk", + "Namibia Standard Time" to "Africa/Windhoek", + "Nepal Standard Time" to "Asia/Katmandu", + "New Zealand Standard Time" to "Pacific/Auckland", + "Newfoundland Standard Time" to "America/St_Johns", + "Norfolk Standard Time" to "Pacific/Norfolk", + "North Asia East Standard Time" to "Asia/Irkutsk", + "North Asia Standard Time" to "Asia/Krasnoyarsk", + "North Korea Standard Time" to "Asia/Pyongyang", + "Omsk Standard Time" to "Asia/Omsk", + "Pacific SA Standard Time" to "America/Santiago", + "Pacific Standard Time" to "America/Los_Angeles", + "Pacific Standard Time (Mexico)" to "America/Tijuana", + "Pakistan Standard Time" to "Asia/Karachi", + "Paraguay Standard Time" to "America/Asuncion", + "Qyzylorda Standard Time" to "Asia/Qyzylorda", + "Romance Standard Time" to "Europe/Paris", + "Russia Time Zone 10" to "Asia/Srednekolymsk", + "Russia Time Zone 11" to "Asia/Kamchatka", + "Russia Time Zone 3" to "Europe/Samara", + "Russian Standard Time" to "Europe/Moscow", + "SA Eastern Standard Time" to "America/Cayenne", + "SA Pacific Standard Time" to "America/Bogota", + "SA Western Standard Time" to "America/La_Paz", + "SE Asia Standard Time" to "Asia/Bangkok", + "Saint Pierre Standard Time" to "America/Miquelon", + "Sakhalin Standard Time" to "Asia/Sakhalin", + "Samoa Standard Time" to "Pacific/Apia", + "Sao Tome Standard Time" to "Africa/Sao_Tome", + "Saratov Standard Time" to "Europe/Saratov", + "Singapore Standard Time" to "Asia/Singapore", + "South Africa Standard Time" to "Africa/Johannesburg", + "South Sudan Standard Time" to "Africa/Juba", + "Sri Lanka Standard Time" to "Asia/Colombo", + "Sudan Standard Time" to "Africa/Khartoum", + "Syria Standard Time" to "Asia/Damascus", + "Taipei Standard Time" to "Asia/Taipei", + "Tasmania Standard Time" to "Australia/Hobart", + "Tocantins Standard Time" to "America/Araguaina", + "Tokyo Standard Time" to "Asia/Tokyo", + "Tomsk Standard Time" to "Asia/Tomsk", + "Tonga Standard Time" to "Pacific/Tongatapu", + "Transbaikal Standard Time" to "Asia/Chita", + "Turkey Standard Time" to "Europe/Istanbul", + "Turks And Caicos Standard Time" to "America/Grand_Turk", + "US Eastern Standard Time" to "America/Indianapolis", + "US Mountain Standard Time" to "America/Phoenix", + "UTC" to "UTC", + "UTC+12" to "Etc/GMT-12", + "UTC+13" to "Etc/GMT-13", + "UTC-02" to "Etc/GMT+2", + "UTC-08" to "Etc/GMT+8", + "UTC-09" to "Etc/GMT+9", + "UTC-11" to "Etc/GMT+11", + "Ulaanbaatar Standard Time" to "Asia/Ulaanbaatar", + "Venezuela Standard Time" to "America/Caracas", + "Vladivostok Standard Time" to "Asia/Vladivostok", + "Volgograd Standard Time" to "Europe/Volgograd", + "W. Australia Standard Time" to "Australia/Perth", + "W. Central Africa Standard Time" to "Africa/Lagos", + "W. Europe Standard Time" to "Europe/Berlin", + "W. Mongolia Standard Time" to "Asia/Hovd", + "West Asia Standard Time" to "Asia/Tashkent", + "West Bank Standard Time" to "Asia/Hebron", + "West Pacific Standard Time" to "Pacific/Port_Moresby", + "Yakutsk Standard Time" to "Asia/Yakutsk", + "Yukon Standard Time" to "America/Whitehorse", +) diff --git a/thirdparty/date b/thirdparty/date deleted file mode 160000 index 9a0ee2542..000000000 --- a/thirdparty/date +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9a0ee2542848ab8625984fc8cdbfb9b5414c0082