@@ -16,30 +16,52 @@ internal class TzdbInRegistry: TimeZoneDatabase {
16
16
// When Kotlin/Native drops support for Windows 7, we should investigate moving to the ICU.
17
17
private val windowsToRules: Map <String , TimeZoneRules > = buildMap {
18
18
processTimeZonesInRegistry { name, recurring, historic ->
19
- val transitions = recurring.transitions?.let { listOf (it.first, it.second) } ? : emptyList()
20
- val rules = if (historic.isEmpty()) {
21
- TimeZoneRules (recurring.standardOffset, RecurringZoneRules (transitions))
22
- } else {
19
+ val recurringRules = RecurringZoneRules (recurring.transitions)
20
+ val rules = run {
21
+ val offsets = mutableListOf<UtcOffset >()
23
22
val transitionEpochSeconds = mutableListOf<Long >()
24
- val offsets = mutableListOf (historic[0 ].second.standardOffset)
25
23
for ((year, record) in historic) {
26
- if (record.transitions == null ) continue
27
- val (trans1, trans2) = record.transitions
28
- val transTime1 = trans1.transitionDateTime.toInstant(year, trans1.offsetBefore)
29
- val transTime2 = trans2.transitionDateTime.toInstant(year, trans2.offsetBefore)
30
- if (transTime2 >= transTime1) {
31
- transitionEpochSeconds.add(transTime1.epochSeconds)
32
- offsets.add(trans1.offsetAfter)
33
- transitionEpochSeconds.add(transTime2.epochSeconds)
34
- offsets.add(trans2.offsetAfter)
35
- } else {
36
- transitionEpochSeconds.add(transTime2.epochSeconds)
37
- offsets.add(trans2.offsetAfter)
38
- transitionEpochSeconds.add(transTime1.epochSeconds)
39
- offsets.add(trans1.offsetAfter)
24
+ when (record) {
25
+ is PerYearZoneRulesDataWithoutTransitions -> {
26
+ val lastOffset = offsets.lastOrNull()
27
+ if (lastOffset != record.standardOffset) {
28
+ lastOffset?.let {
29
+ transitionEpochSeconds.add(START_OF_YEAR .toInstant(year, it).epochSeconds)
30
+ }
31
+ offsets.add(record.standardOffset)
32
+ }
33
+ }
34
+ is PerYearZoneRulesDataWithTransitions -> {
35
+ val toDstTime = record.daylightTransitionTime.toLocalDateTime(year)
36
+ val toStdTime = record.standardTransitionTime.toLocalDateTime(year)
37
+ val initialOffset =
38
+ if (toDstTime < toStdTime) record.standardOffset else record.daylightOffset
39
+ if (offsets.isEmpty()) { offsets.add(initialOffset) }
40
+ val newYearTransition: RecurringZoneRules .Rule <MonthDayTime > =
41
+ RecurringZoneRules .Rule (
42
+ transitionDateTime = START_OF_YEAR ,
43
+ offsetBefore = offsets.last(),
44
+ offsetAfter = initialOffset,
45
+ )
46
+ // The order is important: newYearTransition must be overwritten by the explicit Jan 1st transition
47
+ mapOf (
48
+ LocalDate (year, Month .JANUARY , 1 ).atTime(0 , 0 ) to newYearTransition,
49
+ toDstTime to record.daylightTransition,
50
+ toStdTime to record.standardTransition
51
+ ).toList().sortedBy(Pair <LocalDateTime , * >::first).forEach { (time, transition) ->
52
+ // occasionally, there are transitions that have no effect at all, whose purpose is
53
+ // just to fulfill the contract that there are either two or zero transitions per year.
54
+ // We skip such transitions entirely to simplify handling.
55
+ if (offsets.last() != transition.offsetAfter) {
56
+ transitionEpochSeconds.add(time.toInstant(transition.offsetBefore).epochSeconds)
57
+ offsets.add(transition.offsetAfter)
58
+ }
59
+ }
60
+ }
40
61
}
41
62
}
42
- TimeZoneRules (transitionEpochSeconds, offsets, RecurringZoneRules (transitions))
63
+ if (offsets.isEmpty()) { offsets.add(recurring.offsetAtYearStart(2020 )) }
64
+ TimeZoneRules (transitionEpochSeconds, offsets, recurringRules)
43
65
}
44
66
put(name, rules)
45
67
}
@@ -216,14 +238,14 @@ private class RegistryTimeZoneInfoBuffer private constructor(
216
238
val standardDate = (buffer + 12 )!! .reinterpret<SYSTEMTIME >().pointed
217
239
val daylightDate = (buffer + 28 )!! .reinterpret<SYSTEMTIME >().pointed
218
240
// calculating the things we're interested in
219
- val standardOffset = UtcOffset (minutes = - (standardBias + bias))
220
- val daylightOffset = UtcOffset (minutes = - (daylightBias + bias))
221
241
if (daylightDate.wMonth == 0 .convert<WORD >()) {
222
- return PerYearZoneRulesData (standardOffset, null )
242
+ return PerYearZoneRulesDataWithoutTransitions ( UtcOffset (minutes = - bias) )
223
243
}
224
- val changeToDst = RecurringZoneRules .Rule (daylightDate.toMonthDayTime(), standardOffset, daylightOffset)
225
- val changeToStd = RecurringZoneRules .Rule (standardDate.toMonthDayTime(), daylightOffset, standardOffset)
226
- return PerYearZoneRulesData (standardOffset, changeToDst to changeToStd)
244
+ val standardOffset = UtcOffset (minutes = - (standardBias + bias))
245
+ val daylightOffset = UtcOffset (minutes = - (daylightBias + bias))
246
+ val changeToDst = daylightDate.toMonthDayTime()
247
+ val changeToStd = standardDate.toMonthDayTime()
248
+ return PerYearZoneRulesDataWithTransitions (standardOffset, daylightOffset, changeToDst, changeToStd)
227
249
}
228
250
}
229
251
@@ -270,13 +292,42 @@ private fun SYSTEMTIME.toMonthDayTime(): MonthDayTime {
270
292
return MonthDayTime (MonthDayOfYear (month, transitionDay), localTime, MonthDayTime .OffsetResolver .WallClockOffset )
271
293
}
272
294
273
- private class PerYearZoneRulesData (
295
+ private sealed interface PerYearZoneRulesData {
296
+ val transitions: List <RecurringZoneRules .Rule <MonthDayTime >>
297
+ fun offsetAtYearStart (year : Int ): UtcOffset
298
+ }
299
+
300
+ private class PerYearZoneRulesDataWithoutTransitions (
301
+ val standardOffset : UtcOffset
302
+ ) : PerYearZoneRulesData {
303
+ override val transitions: List <RecurringZoneRules .Rule <MonthDayTime >>
304
+ get() = emptyList()
305
+
306
+ override fun offsetAtYearStart (year : Int ): UtcOffset = standardOffset
307
+
308
+ override fun toString (): String = " standard offset is $standardOffset "
309
+ }
310
+
311
+ private class PerYearZoneRulesDataWithTransitions (
274
312
val standardOffset : UtcOffset ,
275
- val transitions : Pair <RecurringZoneRules .Rule <MonthDayTime >, RecurringZoneRules .Rule <MonthDayTime >>? ,
276
- ) {
277
- override fun toString (): String = " standard offset is $standardOffset " + (transitions?.let {
278
- " , the transitions: ${it.first} , ${it.second} "
279
- } ? : " " )
313
+ val daylightOffset : UtcOffset ,
314
+ val daylightTransitionTime : MonthDayTime ,
315
+ val standardTransitionTime : MonthDayTime ,
316
+ ) : PerYearZoneRulesData {
317
+ override val transitions: List <RecurringZoneRules .Rule <MonthDayTime >>
318
+ get() = listOf (daylightTransition, standardTransition)
319
+
320
+ val daylightTransition get() =
321
+ RecurringZoneRules .Rule (daylightTransitionTime, offsetBefore = standardOffset, offsetAfter = daylightOffset)
322
+
323
+ val standardTransition get() =
324
+ RecurringZoneRules .Rule (standardTransitionTime, offsetBefore = daylightOffset, offsetAfter = standardOffset)
325
+
326
+ override fun offsetAtYearStart (year : Int ): UtcOffset = standardOffset
327
+
328
+ override fun toString (): String = " standard offset is $standardOffset " +
329
+ " , daylight offset is $daylightOffset " +
330
+ " , the transitions: $daylightTransitionTime , $standardTransitionTime "
280
331
}
281
332
282
333
private fun getLastWindowsError (): String = memScoped {
@@ -292,3 +343,9 @@ private fun getLastWindowsError(): String = memScoped {
292
343
)
293
344
buf.value!! .toKStringFromUtf16().also { LocalFree (buf.ptr) }
294
345
}
346
+
347
+ private val START_OF_YEAR = MonthDayTime (
348
+ date = JulianDayOfYear (0 ),
349
+ time = MonthDayTime .TransitionLocaltime (0 ),
350
+ offset = MonthDayTime .OffsetResolver .WallClockOffset ,
351
+ )
0 commit comments