Skip to content

Commit dba92f9

Browse files
authored
Native (Kotlin): Fix a bug with Instant.plus (#3)
* Native (Kotlin): Fix a bug with `Instant.plus` There was a problem with `Instant.plus` failing to adjust an intermediate date due to offset changing. This led to behaviour different from other platforms and `plus` and `periodUntil` not being inverse operations.
1 parent a435b41 commit dba92f9

File tree

5 files changed

+45
-35
lines changed

5 files changed

+45
-35
lines changed

core/commonTest/src/InstantTest.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,15 @@ class InstantTest {
127127
assertEquals(offset1, offset3)
128128
}
129129

130+
@Test
131+
fun changingTimeZoneRules() {
132+
val start = Instant.parse("1991-01-25T23:15:15.855Z")
133+
val end = Instant.parse("2006-04-24T22:07:32.561Z")
134+
val diff = start.periodUntil(end, TimeZone.of("Europe/Moscow"))
135+
val end2 = start.plus(diff, TimeZone.of("Europe/Moscow"))
136+
assertEquals(end, end2)
137+
}
138+
130139
@Test
131140
fun diffInvariant() {
132141
repeat(1000) {

core/nativeMain/src/Instant.kt

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -164,17 +164,15 @@ public actual class Instant internal constructor(internal val epochSeconds: Long
164164
}
165165

166166
actual companion object {
167-
actual fun now(): Instant {
168-
return memScoped {
169-
val timespecBuf = alloc<timespec>()
170-
val error = clock_gettime(CLOCK_REALTIME, timespecBuf.ptr)
171-
assertEquals(0, error)
172-
// according to https://en.cppreference.com/w/c/chrono/timespec,
173-
// tv_nsec in [0; 10^9), so no need to call [ofEpochSecond].
174-
val seconds = timespecBuf.tv_sec.toLong() // conversion is needed on some platforms
175-
val nanosec = timespecBuf.tv_nsec.toInt()
176-
Instant(seconds, nanosec)
177-
}
167+
actual fun now(): Instant = memScoped {
168+
val timespecBuf = alloc<timespec>()
169+
val error = clock_gettime(CLOCK_REALTIME, timespecBuf.ptr)
170+
assertEquals(0, error)
171+
// according to https://en.cppreference.com/w/c/chrono/timespec,
172+
// tv_nsec in [0; 10^9), so no need to call [ofEpochSecond].
173+
val seconds = timespecBuf.tv_sec.toLong() // conversion is needed on some platforms
174+
val nanosec = timespecBuf.tv_nsec.toInt()
175+
Instant(seconds, nanosec)
178176
}
179177

180178
// org.threeten.bp.Instant#ofEpochMilli
@@ -202,24 +200,24 @@ actual fun Instant.plus(period: CalendarPeriod, zone: TimeZone): Instant {
202200
minutes.toLong() * SECONDS_PER_MINUTE,
203201
hours.toLong() * SECONDS_PER_HOUR))
204202
}
205-
val localDateTime = toLocalDateTime(zone)
203+
val localDateTime = toZonedLocalDateTime(zone)
206204
return with(period) {
207205
localDateTime
208206
.run { if (years != 0 && months == 0) plusYears(years.toLong()) else this }
209207
.run { if (months != 0) plusMonths(years * 12L + months.toLong()) else this }
210208
.run { if (days != 0) plusDays(days.toLong()) else this }
211-
}.toInstant(zone).plus(seconds, period.nanoseconds)
209+
}.toInstant().plus(seconds, period.nanoseconds)
212210
}
213211

214212
actual fun Instant.plus(value: Int, unit: CalendarUnit, zone: TimeZone): Instant =
215213
plus(value.toLong(), unit, zone)
216214

217215
actual fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone): Instant =
218216
when (unit) {
219-
CalendarUnit.YEAR -> toLocalDateTime(zone).plusYears(value).toInstant(zone)
220-
CalendarUnit.MONTH -> toLocalDateTime(zone).plusMonths(value).toInstant(zone)
221-
CalendarUnit.WEEK -> toLocalDateTime(zone).plusDays(value * 7).toInstant(zone)
222-
CalendarUnit.DAY -> toLocalDateTime(zone).plusDays(value).toInstant(zone)
217+
CalendarUnit.YEAR -> toZonedLocalDateTime(zone).plusYears(value).toInstant()
218+
CalendarUnit.MONTH -> toZonedLocalDateTime(zone).plusMonths(value).toInstant()
219+
CalendarUnit.WEEK -> toZonedLocalDateTime(zone).plusDays(value * 7).toInstant()
220+
CalendarUnit.DAY -> toZonedLocalDateTime(zone).plusDays(value).toInstant()
223221
/* From org.threeten.bp.ZonedDateTime#plusHours: the time is added to the raw LocalDateTime,
224222
then org.threeten.bp.ZonedDateTime#create is called on the absolute instant
225223
(gotten from org.threeten.bp.chrono.ChronoLocalDateTime#toEpochSecond). This, in turn,
@@ -244,8 +242,8 @@ actual fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone): Instan
244242

245243
@OptIn(ExperimentalTime::class)
246244
actual fun Instant.periodUntil(other: Instant, zone: TimeZone): CalendarPeriod {
247-
var thisLdt = with(zone) { toZonedLocalDateTime() }
248-
val otherLdt = with(zone) { other.toZonedLocalDateTime() }
245+
var thisLdt = toZonedLocalDateTime(zone)
246+
val otherLdt = other.toZonedLocalDateTime(zone)
249247

250248
val months = thisLdt.until(otherLdt, CalendarUnit.MONTH); thisLdt = thisLdt.plusMonths(months)
251249
val days = thisLdt.until(otherLdt, CalendarUnit.DAY); thisLdt = thisLdt.plusDays(days)
@@ -257,7 +255,7 @@ actual fun Instant.periodUntil(other: Instant, zone: TimeZone): CalendarPeriod {
257255
}
258256

259257
actual fun Instant.until(other: Instant, unit: CalendarUnit, zone: TimeZone): Long =
260-
with(zone) { toZonedLocalDateTime().until(other.toZonedLocalDateTime(), unit) }
258+
toZonedLocalDateTime(zone).until(other.toZonedLocalDateTime(zone), unit)
261259

262260
actual fun Instant.daysUntil(other: Instant, zone: TimeZone): Int = until(other, CalendarUnit.DAY, zone).toInt()
263261
actual fun Instant.monthsUntil(other: Instant, zone: TimeZone): Int = until(other, CalendarUnit.MONTH, zone).toInt()

core/nativeMain/src/LocalDate.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public actual class LocalDate private constructor(actual val year: Int, actual v
3030

3131
// org.threeten.bp.LocalDate#toEpochDay
3232
internal fun ofEpochDay(epochDay: Long): LocalDate {
33+
require(epochDay in -365243219162L..365241780471L)
3334
var zeroDay: Long = epochDay + DAYS_0000_TO_1970
3435
// find the march-based year
3536
zeroDay -= 60 // adjust to 0000-03-01 so leap day is at end of four year cycle

core/nativeMain/src/TimeZone.kt

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -78,17 +78,7 @@ public actual open class TimeZone internal constructor(actual val id: String) {
7878
}
7979
}
8080

81-
// org.threeten.bp.LocalDateTime#ofEpochSecond + org.threeten.bp.ZonedDateTime#create
82-
internal fun Instant.toZonedLocalDateTime(): ZonedDateTime {
83-
val localSecond: Long = epochSeconds + offset.totalSeconds // overflow caught later
84-
val localEpochDay: Long = floorDiv(localSecond, SECONDS_PER_DAY.toLong())
85-
val secsOfDay: Long = floorMod(localSecond, SECONDS_PER_DAY.toLong())
86-
val date: LocalDate = LocalDate.ofEpochDay(localEpochDay)
87-
val time: LocalTime = LocalTime.ofSecondOfDay(secsOfDay, nanos)
88-
return ZonedDateTime(LocalDateTime(date, time), this@TimeZone, offset)
89-
}
90-
91-
actual fun Instant.toLocalDateTime(): LocalDateTime = toZonedLocalDateTime().dateTime
81+
actual fun Instant.toLocalDateTime(): LocalDateTime = toZonedLocalDateTime(this@TimeZone).dateTime
9282

9383
actual open val Instant.offset: ZoneOffset
9484
get() {
@@ -99,10 +89,8 @@ public actual open class TimeZone internal constructor(actual val id: String) {
9989
return ZoneOffset(offset)
10090
}
10191

102-
actual fun LocalDateTime.toInstant(): Instant {
103-
val zoned = atZone()
104-
return Instant(zoned.dateTime.toEpochSecond(zoned.offset), nanosecond)
105-
}
92+
actual fun LocalDateTime.toInstant(): Instant =
93+
atZone().toInstant()
10694

10795
internal open fun LocalDateTime.atZone(preferred: ZoneOffset? = null): ZonedDateTime = memScoped {
10896
val epochSeconds = toEpochSecond(ZoneOffset(0))

core/nativeMain/src/ZonedDateTime.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,20 @@ internal class ZonedDateTime(val dateTime: LocalDateTime, private val zone: Time
3333
}
3434
}
3535

36+
internal fun ZonedDateTime.toInstant(): Instant =
37+
Instant(dateTime.toEpochSecond(offset), dateTime.nanosecond)
38+
39+
// org.threeten.bp.LocalDateTime#ofEpochSecond + org.threeten.bp.ZonedDateTime#create
40+
internal fun Instant.toZonedLocalDateTime(zone: TimeZone): ZonedDateTime {
41+
val currentOffset = with (zone) { offset }
42+
val localSecond: Long = epochSeconds + currentOffset.totalSeconds // overflow caught later
43+
val localEpochDay: Long = floorDiv(localSecond, SECONDS_PER_DAY.toLong())
44+
val secsOfDay: Long = floorMod(localSecond, SECONDS_PER_DAY.toLong())
45+
val date: LocalDate = LocalDate.ofEpochDay(localEpochDay)
46+
val time: LocalTime = LocalTime.ofSecondOfDay(secsOfDay, nanos)
47+
return ZonedDateTime(LocalDateTime(date, time), zone, currentOffset)
48+
}
49+
3650
// org.threeten.bp.ZonedDateTime#until
3751
// This version is simplified and to be used ONLY in case you know the timezones are equal!
3852
internal fun ZonedDateTime.until(other: ZonedDateTime, unit: CalendarUnit): Long =

0 commit comments

Comments
 (0)