diff --git a/core/native/src/internal/TimeZoneRules.kt b/core/native/src/internal/TimeZoneRules.kt index c712ff4a..0d56aceb 100644 --- a/core/native/src/internal/TimeZoneRules.kt +++ b/core/native/src/internal/TimeZoneRules.kt @@ -115,6 +115,21 @@ internal class TimeZoneRules( val offsetAfter = offsets[transitionIndex + 1] return OffsetInfo(transitionInstant, offsetBefore, offsetAfter) } + + override fun toString(): String = buildString { + for (i in transitionEpochSeconds.indices) { + append(offsets[i]) + append(" until ") + append(Instant.fromEpochSeconds(transitionEpochSeconds[i])) + append(", ") + } + append("then ") + append(offsets.last()) + if (recurringZoneRules != null) { + append(", after that ") + append(recurringZoneRules) + } + } } internal class RecurringZoneRules( diff --git a/core/windows/src/internal/TzdbInRegistry.kt b/core/windows/src/internal/TzdbInRegistry.kt index e0c0bd31..f5781da6 100644 --- a/core/windows/src/internal/TzdbInRegistry.kt +++ b/core/windows/src/internal/TzdbInRegistry.kt @@ -60,7 +60,16 @@ internal class TzdbInRegistry: TimeZoneDatabase { } } } - if (offsets.isEmpty()) { offsets.add(recurring.offsetAtYearStart()) } + offsets.lastOrNull()?.let { lastOffset -> + /* If there are already some offsets, we can not add a new offset without defining a transition to + it. The moment when we start using the recurring rules is the first year that does not have any + historic data provided. */ + val firstYearWithRecurringRules = historic.last().first + 1 + val newYearInLastOffset = LocalDate(firstYearWithRecurringRules, Month.JANUARY, 1).atTime(0, 0) + .toInstant(lastOffset) + transitionEpochSeconds.add(newYearInLastOffset.epochSeconds) + } + offsets.add(recurring.offsetAtYearStart()) TimeZoneRules(transitionEpochSeconds, offsets, recurringRules) } put(name, rules) diff --git a/core/windows/test/TimeZoneRulesCompleteTest.kt b/core/windows/test/TimeZoneRulesCompleteTest.kt index c08bbbbc..34d3b868 100644 --- a/core/windows/test/TimeZoneRulesCompleteTest.kt +++ b/core/windows/test/TimeZoneRulesCompleteTest.kt @@ -7,6 +7,7 @@ package kotlinx.datetime.test import kotlinx.cinterop.* +import kotlinx.cinterop.ptr import kotlinx.datetime.* import kotlinx.datetime.internal.* import platform.windows.* @@ -16,6 +17,7 @@ import kotlin.time.Duration.Companion.milliseconds class TimeZoneRulesCompleteTest { /** Tests that all transitions that our system recognizes are actually there. */ + @OptIn(ExperimentalStdlibApi::class) @Test fun iterateOverAllTimezones() { val tzdb = TzdbInRegistry() @@ -42,7 +44,38 @@ class TimeZoneRulesCompleteTest { val ldt = instant.toLocalDateTime(dtzi, inputSystemtime.ptr, outputSystemtime.ptr) val offset = rules.infoAtInstant(instant) val ourLdt = instant.toLocalDateTime(offset) - assertEquals(ldt, ourLdt, "in zone $windowsName, at $instant (our guess at the offset is $offset)") + if (ldt != ourLdt) { + val offsetsAccordingToWindows = buildList { + var date = LocalDate(ldt.year, Month.JANUARY, 1) + while (date.year == ldt.year) { + val instant = date.atTime(0, 0).toInstant(UtcOffset.ZERO) + val ldtAccordingToWindows = + instant.toLocalDateTime(dtzi, inputSystemtime.ptr, outputSystemtime.ptr) + val offsetAccordingToWindows = + (ldtAccordingToWindows.toInstant(UtcOffset.ZERO) - instant).inWholeSeconds + add(date to offsetAccordingToWindows) + date = date.plus(1, DateTimeUnit.DAY) + } + } + val rawData = memScoped { + val hKey = alloc() + RegOpenKeyExW(HKEY_LOCAL_MACHINE!!, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones\\$windowsName", 0u, KEY_READ.toUInt(), hKey.ptr) + try { + val cbDataBuffer = alloc() + val SIZE_BYTES = 44 + val zoneInfoBuffer = allocArray(SIZE_BYTES) + cbDataBuffer.value = SIZE_BYTES.convert() + RegQueryValueExW(hKey.value, "TZI", null, null, zoneInfoBuffer, cbDataBuffer.ptr) + zoneInfoBuffer.readBytes(SIZE_BYTES).toHexString() + } finally { + RegCloseKey(hKey.value) + } + } + throw AssertionError( + "Expected $ldt, got $ourLdt in zone $windowsName at $instant (our guess at the offset is $offset)." + + "The rules are $rules, and the offsets throughout the year according to Windows are: $offsetsAccordingToWindows; the raw data for the recurring rules is $rawData" + ) + } } fun checkTransition(instant: Instant) { checkAtInstant(instant - 2.milliseconds) @@ -120,5 +153,6 @@ private fun SYSTEMTIME.toLocalDateTime(): LocalDateTime = ) private val strangeTimeZones = listOf( - "Morocco Standard Time", "West Bank Standard Time", "Iran Standard Time", "Syria Standard Time" + "Morocco Standard Time", "West Bank Standard Time", "Iran Standard Time", "Syria Standard Time", + "Paraguay Standard Time" )