Skip to content

Commit a7b7a49

Browse files
committed
Avoid Double rounding in JS tzdb implementation
1 parent 3828bc4 commit a7b7a49

File tree

1 file changed

+30
-25
lines changed

1 file changed

+30
-25
lines changed

core/commonJs/src/internal/Platform.kt

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ package kotlinx.datetime.internal
88
import kotlinx.datetime.*
99
import kotlinx.datetime.UtcOffset
1010
import kotlinx.datetime.internal.JSJoda.ZoneId
11-
import kotlin.math.roundToInt
12-
import kotlin.math.roundToLong
1311

1412
private val tzdb: Result<TimeZoneDatabase?> = runCatching {
1513
/**
@@ -25,49 +23,56 @@ private val tzdb: Result<TimeZoneDatabase?> = runCatching {
2523
in 'A'..'X' -> char - 'A' + 36
2624
else -> throw IllegalArgumentException("Invalid character: $char")
2725
}
28-
fun unpackBase60(string: String): Double {
26+
/** converts a base60 number of minutes to a whole number of seconds */
27+
fun base60MinutesInSeconds(string: String): Long {
2928
var i = 0
3029
var parts = string.split('.')
31-
val whole = parts[0]
32-
var multiplier = 1.0
33-
var out = 0.0
34-
var sign = 1
3530

3631
// handle negative numbers
37-
if (string.startsWith('-')) {
32+
val sign = if (string.startsWith('-')) {
3833
i = 1
39-
sign = -1
34+
-1
35+
} else {
36+
1
4037
}
4138

42-
// handle digits before the decimal
39+
var wholeMinutes: Long = 0
40+
val whole = parts[0]
41+
// handle digits before the decimal (whole minutes)
4342
for (ix in i..whole.lastIndex) {
44-
out = 60 * out + charCodeToInt(whole[ix])
43+
wholeMinutes = 60 * wholeMinutes + charCodeToInt(whole[ix])
4544
}
4645

47-
// handle digits after the decimal
48-
parts.getOrNull(1)?.let { fractional ->
49-
for (c in fractional) {
50-
multiplier = multiplier / 60.0
51-
out += charCodeToInt(c) * multiplier
46+
// handle digits after the decimal (seconds and less)
47+
val seconds = parts.getOrNull(1)?.let { fractional ->
48+
when (fractional.length) {
49+
1 -> charCodeToInt(fractional[0]) // single digit, representing seconds
50+
0 -> 0 // actually no fractional part
51+
else -> {
52+
charCodeToInt(fractional[0]) + charCodeToInt(fractional[1]).let {
53+
if (it >= 30) 1 else 0 // rounding the seconds digit
54+
}
55+
}
5256
}
53-
}
57+
} ?: 0
5458

55-
return out * sign
59+
return (wholeMinutes * SECONDS_PER_MINUTE + seconds) * sign
5660
}
5761

5862
val zones = mutableMapOf<String, TimeZoneRules>()
5963
val (zonesPacked, linksPacked) = readTzdb() ?: return@runCatching null
6064
for (zone in zonesPacked) {
6165
val components = zone.split('|')
62-
val offsets = components[2].split(' ').map { unpackBase60(it) }
63-
val indices = components[3].map { charCodeToInt(it) }
64-
val lengthsOfPeriodsWithOffsets = components[4].split(' ').map {
65-
(unpackBase60(it) * SECONDS_PER_MINUTE * MILLIS_PER_ONE).roundToLong() / // minutes to milliseconds
66-
MILLIS_PER_ONE // but we only need seconds
66+
val offsets = components[2].split(' ').map {
67+
UtcOffset(null, null, -base60MinutesInSeconds(it).toInt())
6768
}
69+
val indices = components[3].map { charCodeToInt(it) }
70+
val lengthsOfPeriodsWithOffsets = components[4].split(' ').map(::base60MinutesInSeconds)
6871
zones[components[0]] = TimeZoneRules(
69-
transitionEpochSeconds = lengthsOfPeriodsWithOffsets.runningReduce(Long::plus).take<Long>(indices.size - 1),
70-
offsets = indices.map { UtcOffset(null, null, -(offsets[it] * 60).roundToInt()) },
72+
transitionEpochSeconds = lengthsOfPeriodsWithOffsets.runningReduce(Long::plus).let {
73+
if (it.size == indices.size - 1) it else it.take<Long>(indices.size - 1)
74+
},
75+
offsets = indices.map { offsets[it] },
7176
recurringZoneRules = null
7277
)
7378
}

0 commit comments

Comments
 (0)