Skip to content

Commit b15c973

Browse files
Return FixedOffsetTimeZone when DST is disabled on Windows (#583)
Align timezone behavior with Java by returning a FixedOffsetTimeZone instead of a RegionTimeZone when DynamicDaylightTimeDisabled is set. Previously, Windows users with DST disabled would receive a RegionTimeZone with a confusing name. Now they receive a properly-typed FixedOffsetTimeZone with a clear GMT-prefixed name (e.g., GMT+01:00). Changes: - Extract asTimeZone(prefix) utility for consistent timezone creation - Refactor timezone ID parsing to use new utility - Extract currentSystemDefaultFromDtzi helper for testability - Add comprehensive tests for both DST enabled/disabled scenarios Fixes #575
1 parent 90be731 commit b15c973

File tree

4 files changed

+36
-15
lines changed

4 files changed

+36
-15
lines changed

core/commonKotlin/src/TimeZone.kt

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,17 +54,11 @@ public actual open class TimeZone internal constructor() {
5454
) {
5555
val prefix = zoneId.take(3)
5656
val offset = lenientOffsetFormat.parse(zoneId.substring(3))
57-
return when (offset.totalSeconds) {
58-
0 -> FixedOffsetTimeZone(offset, prefix)
59-
else -> FixedOffsetTimeZone(offset, "$prefix$offset")
60-
}
57+
return offset.asTimeZone(prefix)
6158
}
6259
if (zoneId.startsWith("UT+") || zoneId.startsWith("UT-")) {
6360
val offset = lenientOffsetFormat.parse(zoneId.substring(2))
64-
return when (offset.totalSeconds) {
65-
0 -> FixedOffsetTimeZone(offset, "UT")
66-
else -> FixedOffsetTimeZone(offset, "UT$offset")
67-
}
61+
return offset.asTimeZone("UT")
6862
}
6963
} catch (e: DateTimeFormatException) {
7064
throw IllegalTimeZoneException(e)

core/commonKotlin/src/UtcOffset.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,9 @@ public actual fun UtcOffset(hours: Int? = null, minutes: Int? = null, seconds: I
108108
UtcOffset.ofSeconds(seconds ?: 0)
109109
}
110110
}
111+
112+
internal fun UtcOffset.asTimeZone(prefix: String): FixedOffsetTimeZone =
113+
when (totalSeconds) {
114+
0 -> FixedOffsetTimeZone(this, prefix)
115+
else -> FixedOffsetTimeZone(this, "$prefix$this")
116+
}

core/windows/src/internal/TzdbInRegistry.kt

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,18 +90,19 @@ internal class TzdbInRegistry: TimeZoneDatabase {
9090
val dtzi = alloc<DYNAMIC_TIME_ZONE_INFORMATION>()
9191
val result = GetDynamicTimeZoneInformation(dtzi.ptr)
9292
check(result != TIME_ZONE_ID_INVALID) { "The current system time zone is invalid: ${getLastWindowsError()}" }
93+
return currentSystemDefaultFromDtzi(dtzi)
94+
}
95+
96+
internal fun currentSystemDefaultFromDtzi(dtzi: DYNAMIC_TIME_ZONE_INFORMATION): Pair<String, TimeZone> {
9397
val windowsName = dtzi.TimeZoneKeyName.toKStringFromUtf16()
9498
val ianaTzName = if (windowsName == "Coordinated Universal Time") "UTC" else windowsToStandard[windowsName]
9599
?: throw IllegalStateException("Unknown time zone name '$windowsName'")
96100
val tz = windowsToRules[windowsName]
97101
check(tz != null) { "The system time zone is set to a value rules for which are not known: '$windowsName'" }
98-
val rules = if (dtzi.DynamicDaylightTimeDisabled == 0.convert<BOOLEAN>()) {
99-
tz
100-
} else {
101-
// the user explicitly disabled DST transitions, so
102-
TimeZoneRulesCommon(UtcOffset(minutes = -(dtzi.Bias + dtzi.StandardBias)), RecurringZoneRules(emptyList()))
103-
}
104-
return ianaTzName to RegionTimeZone(rules, ianaTzName)
102+
return ianaTzName to if (dtzi.DynamicDaylightTimeDisabled == 0.convert<BOOLEAN>())
103+
RegionTimeZone(tz, ianaTzName)
104+
else // the user explicitly disabled DST transitions, so
105+
UtcOffset(minutes = -(dtzi.Bias + dtzi.StandardBias)).asTimeZone("GMT")
105106
}
106107
}
107108

core/windows/test/TimeZoneRulesCompleteTest.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,26 @@ import kotlin.time.Clock
1919

2020
class TimeZoneRulesCompleteTest {
2121

22+
@Test
23+
fun dynamicDaylightTimeDisabled() {
24+
memScoped {
25+
val dtzi = alloc<DYNAMIC_TIME_ZONE_INFORMATION>()
26+
val result = GetDynamicTimeZoneInformation(dtzi.ptr)
27+
check(result != TIME_ZONE_ID_INVALID) { "The current system time zone is invalid: ${getLastWindowsError()}" }
28+
29+
val tzdb = TzdbInRegistry()
30+
31+
dtzi.DynamicDaylightTimeDisabled = 0u
32+
val (_, tzWithDst) = tzdb.currentSystemDefaultFromDtzi(dtzi)
33+
assertTrue(tzWithDst is RegionTimeZone, "Expected RegionTimeZone, got ${tzWithDst::class}")
34+
35+
dtzi.DynamicDaylightTimeDisabled = 1u
36+
val (_, tzWithoutDst) = tzdb.currentSystemDefaultFromDtzi(dtzi)
37+
assertTrue(tzWithoutDst is FixedOffsetTimeZone, "Expected FixedOffsetTimeZone, got ${tzWithoutDst::class}")
38+
assertTrue(tzWithoutDst.toString().startsWith("GMT"), "Expected GMT timezone, got $tzWithoutDst")
39+
}
40+
}
41+
2242
/** Tests that all transitions that our system recognizes are actually there. */
2343
@OptIn(ExperimentalStdlibApi::class)
2444
@Test

0 commit comments

Comments
 (0)