From 2d8ca2585d07ef4a7738d6feb4aed4d78eb2304c Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Thu, 14 Dec 2023 15:25:59 +0100 Subject: [PATCH 1/6] Move all ZonedDateTime arithmetic outside of it --- core/native/src/Instant.kt | 31 ++++++++---- core/native/src/TimeZone.kt | 2 +- core/native/src/ZonedDateTime.kt | 55 ++-------------------- core/native/src/internal/RegionTimeZone.kt | 10 ++-- core/native/test/ThreeTenBpTimeZoneTest.kt | 6 ++- 5 files changed, 37 insertions(+), 67 deletions(-) diff --git a/core/native/src/Instant.kt b/core/native/src/Instant.kt index 32474be4..eeb261ee 100644 --- a/core/native/src/Instant.kt +++ b/core/native/src/Instant.kt @@ -161,7 +161,7 @@ private fun Instant.toZonedDateTimeFailing(zone: TimeZone): ZonedDateTime = try */ private fun Instant.toZonedDateTime(zone: TimeZone): ZonedDateTime { val currentOffset = zone.offsetAt(this) - return ZonedDateTime(toLocalDateTimeImpl(currentOffset), zone, currentOffset) + return ZonedDateTime(toLocalDateTimeImpl(currentOffset), currentOffset) } /** Check that [Instant] fits in [ZonedDateTime]. @@ -174,8 +174,8 @@ private fun Instant.check(zone: TimeZone): Instant = this@check.also { public actual fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Instant = try { with(period) { val withDate = toZonedDateTimeFailing(timeZone) - .run { if (totalMonths != 0) plus(totalMonths, DateTimeUnit.MONTH) else this } - .run { if (days != 0) plus(days, DateTimeUnit.DAY) else this } + .run { if (totalMonths != 0) timeZone.atZone(dateTime.plus(totalMonths, DateTimeUnit.MONTH), offset) else this } + .run { if (days != 0) timeZone.atZone(dateTime.plus(days, DateTimeUnit.DAY), offset) else this } withDate.toInstant() .run { if (totalNanoseconds != 0L) plus(0, totalNanoseconds).check(timeZone) else this } }.check(timeZone) @@ -197,7 +197,11 @@ public actual fun Instant.plus(value: Long, unit: DateTimeUnit, timeZone: TimeZo is DateTimeUnit.DateBased -> { if (value < Int.MIN_VALUE || value > Int.MAX_VALUE) throw ArithmeticException("Can't add a Long date-based value, as it would cause an overflow") - toZonedDateTimeFailing(timeZone).plus(value.toInt(), unit).toInstant() + val toZonedDateTimeFailing = toZonedDateTimeFailing(timeZone) + timeZone.atZone( + toZonedDateTimeFailing.dateTime.plus(value.toInt(), unit), + toZonedDateTimeFailing.offset + ).toInstant() } is DateTimeUnit.TimeBased -> check(timeZone).plus(value, unit).check(timeZone) @@ -223,11 +227,20 @@ public actual fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateT var thisLdt = toZonedDateTimeFailing(timeZone) val otherLdt = other.toZonedDateTimeFailing(timeZone) - val months = thisLdt.until(otherLdt, DateTimeUnit.MONTH).toInt() // `until` on dates never fails - thisLdt = thisLdt.plus(months, DateTimeUnit.MONTH) // won't throw: thisLdt + months <= otherLdt, which is known to be valid - val days = thisLdt.until(otherLdt, DateTimeUnit.DAY).toInt() // `until` on dates never fails - thisLdt = thisLdt.plus(days, DateTimeUnit.DAY) // won't throw: thisLdt + days <= otherLdt - val nanoseconds = thisLdt.until(otherLdt, DateTimeUnit.NANOSECOND) // |otherLdt - thisLdt| < 24h + val months = + thisLdt.dateTime.until(otherLdt.dateTime, DateTimeUnit.MONTH).toLong().toInt() // `until` on dates never fails + thisLdt = timeZone.atZone( + thisLdt.dateTime.plus(months, DateTimeUnit.MONTH), + thisLdt.offset + ) // won't throw: thisLdt + months <= otherLdt, which is known to be valid + val days = + thisLdt.dateTime.until(otherLdt.dateTime, DateTimeUnit.DAY).toLong().toInt() // `until` on dates never fails + thisLdt = timeZone.atZone( + thisLdt.dateTime.plus(days, DateTimeUnit.DAY), + thisLdt.offset + ) // won't throw: thisLdt + days <= otherLdt + val nanoseconds = + thisLdt.toInstant().until(otherLdt.toInstant(), DateTimeUnit.NANOSECOND) // |otherLdt - thisLdt| < 24h return buildDateTimePeriod(months, days, nanoseconds) } diff --git a/core/native/src/TimeZone.kt b/core/native/src/TimeZone.kt index 2a8fc3e2..f20d2d5e 100644 --- a/core/native/src/TimeZone.kt +++ b/core/native/src/TimeZone.kt @@ -120,7 +120,7 @@ public actual class FixedOffsetTimeZone internal constructor(public actual val o override fun offsetAtImpl(instant: Instant): UtcOffset = offset override fun atZone(dateTime: LocalDateTime, preferred: UtcOffset?): ZonedDateTime = - ZonedDateTime(dateTime, this, offset) + ZonedDateTime(dateTime, offset) override fun instantToLocalDateTime(instant: Instant): LocalDateTime = instant.toLocalDateTime(offset) override fun localDateTimeToInstant(dateTime: LocalDateTime): Instant = dateTime.toInstant(offset) diff --git a/core/native/src/ZonedDateTime.kt b/core/native/src/ZonedDateTime.kt index 655da440..a6fab783 100644 --- a/core/native/src/ZonedDateTime.kt +++ b/core/native/src/ZonedDateTime.kt @@ -8,67 +8,20 @@ package kotlinx.datetime -internal class ZonedDateTime(val dateTime: LocalDateTime, private val zone: TimeZone, val offset: UtcOffset) { - /** - * @throws IllegalArgumentException if the result exceeds the boundaries - * @throws ArithmeticException if arithmetic overflow occurs - */ - internal fun plus(value: Int, unit: DateTimeUnit.DateBased): ZonedDateTime = dateTime.plus(value, unit).resolve() - - // Never throws in practice - private fun LocalDateTime.resolve(): ZonedDateTime = - // workaround for https://github.com/Kotlin/kotlinx-datetime/issues/51 - if (this@resolve.toInstant(offset).toLocalDateTime(zone) == this@resolve) { - // this LocalDateTime is valid in these timezone and offset. - ZonedDateTime(this, zone, offset) - } else { - // this LDT does need proper resolving, as the instant that it would map to given the preferred offset - // is is mapped to another LDT. - zone.atZone(this, offset) - } - +internal class ZonedDateTime(val dateTime: LocalDateTime, val offset: UtcOffset) { override fun equals(other: Any?): Boolean = this === other || other is ZonedDateTime && - dateTime == other.dateTime && offset == other.offset && zone == other.zone + dateTime == other.dateTime && offset == other.offset override fun hashCode(): Int { - return dateTime.hashCode() xor offset.hashCode() xor zone.hashCode().rotateLeft(3) + return dateTime.hashCode() xor offset.hashCode() } override fun toString(): String { - var str = dateTime.toString() + offset.toString() - if (zone !is FixedOffsetTimeZone || offset !== zone.offset) { - str += "[$zone]" - } + val str = dateTime.toString() + offset.toString() return str } } internal fun ZonedDateTime.toInstant(): Instant = Instant(dateTime.toEpochSecond(offset), dateTime.nanosecond) - - -// org.threeten.bp.ZonedDateTime#until -// This version is simplified and to be used ONLY in case you know the timezones are equal! -/** - * @throws ArithmeticException on arithmetic overflow - * @throws DateTimeArithmeticException if setting [other] to the offset of [this] leads to exceeding boundaries of - * [LocalDateTime]. - */ - -internal fun ZonedDateTime.until(other: ZonedDateTime, unit: DateTimeUnit): Long = - when (unit) { - // if the time unit is date-based, the offsets are disregarded and only the dates and times are compared. - is DateTimeUnit.DateBased -> dateTime.until(other.dateTime, unit).toLong() - // if the time unit is not date-based, we need to make sure that [other] is at the same offset as [this]. - is DateTimeUnit.TimeBased -> { - val offsetDiff = offset.totalSeconds - other.offset.totalSeconds - val otherLdtAdjusted = try { - other.dateTime.plusSeconds(offsetDiff) - } catch (e: IllegalArgumentException) { - throw DateTimeArithmeticException( - "Unable to find difference between date-times, as one of them overflowed") - } - dateTime.until(otherLdtAdjusted, unit) - } - } diff --git a/core/native/src/internal/RegionTimeZone.kt b/core/native/src/internal/RegionTimeZone.kt index d4e7bc6b..c98d628b 100644 --- a/core/native/src/internal/RegionTimeZone.kt +++ b/core/native/src/internal/RegionTimeZone.kt @@ -20,10 +20,10 @@ internal class RegionTimeZone(private val tzid: TimeZoneRules, override val id: override fun atZone(dateTime: LocalDateTime, preferred: UtcOffset?): ZonedDateTime = when (val info = tzid.infoAtDatetime(dateTime)) { - is OffsetInfo.Regular -> ZonedDateTime(dateTime, this, info.offset) + is OffsetInfo.Regular -> ZonedDateTime(dateTime, info.offset) is OffsetInfo.Gap -> { try { - ZonedDateTime(dateTime.plusSeconds(info.transitionDurationSeconds), this, info.offsetAfter) + ZonedDateTime(dateTime.plusSeconds(info.transitionDurationSeconds), info.offsetAfter) } catch (e: IllegalArgumentException) { throw DateTimeArithmeticException( "Overflow whet correcting the date-time to not be in the transition gap", @@ -32,8 +32,10 @@ internal class RegionTimeZone(private val tzid: TimeZoneRules, override val id: } } - is OffsetInfo.Overlap -> ZonedDateTime(dateTime, this, - if (info.offsetAfter == preferred) info.offsetAfter else info.offsetBefore) + is OffsetInfo.Overlap -> ZonedDateTime( + dateTime, + if (info.offsetAfter == preferred) info.offsetAfter else info.offsetBefore + ) } override fun offsetAtImpl(instant: Instant): UtcOffset = tzid.infoAtInstant(instant) diff --git a/core/native/test/ThreeTenBpTimeZoneTest.kt b/core/native/test/ThreeTenBpTimeZoneTest.kt index 95ee1cf8..d1798fc2 100644 --- a/core/native/test/ThreeTenBpTimeZoneTest.kt +++ b/core/native/test/ThreeTenBpTimeZoneTest.kt @@ -40,8 +40,10 @@ class ThreeTenBpTimeZoneTest { fun overlappingLocalTime() { val t = LocalDateTime(2007, 10, 28, 2, 30, 0, 0) val zone = TimeZone.of("Europe/Paris") - assertEquals(ZonedDateTime(LocalDateTime(2007, 10, 28, 2, 30, 0, 0), - zone, UtcOffset(seconds = 2 * 3600)), zone.atZone(t)) + assertEquals(ZonedDateTime( + LocalDateTime(2007, 10, 28, 2, 30, 0, 0), + UtcOffset(seconds = 2 * 3600) + ), zone.atZone(t)) } } From f5aa7d45b2084c560c0d908bb5400509b25607be Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Thu, 14 Dec 2023 16:01:52 +0100 Subject: [PATCH 2/6] Simplify the code for Instant arithmetics --- core/native/src/Instant.kt | 74 ++++++++++------------ core/native/src/LocalDateTimeWithOffset.kt | 14 ++++ core/native/src/TimeZone.kt | 6 +- core/native/src/ZonedDateTime.kt | 27 -------- core/native/src/internal/RegionTimeZone.kt | 8 +-- core/native/test/ThreeTenBpTimeZoneTest.kt | 2 +- 6 files changed, 57 insertions(+), 74 deletions(-) create mode 100644 core/native/src/LocalDateTimeWithOffset.kt delete mode 100644 core/native/src/ZonedDateTime.kt diff --git a/core/native/src/Instant.kt b/core/native/src/Instant.kt index eeb261ee..3551e93e 100644 --- a/core/native/src/Instant.kt +++ b/core/native/src/Instant.kt @@ -150,33 +150,39 @@ public actual class Instant internal constructor(public actual val epochSeconds: } -private fun Instant.toZonedDateTimeFailing(zone: TimeZone): ZonedDateTime = try { - toZonedDateTime(zone) +private fun Instant.toLocalDateTimeFailing(offset: UtcOffset): LocalDateTime = try { + toLocalDateTimeImpl(offset) } catch (e: IllegalArgumentException) { throw DateTimeArithmeticException("Can not convert instant $this to LocalDateTime to perform computations", e) } -/** - * @throws IllegalArgumentException if the [Instant] exceeds the boundaries of [LocalDateTime] - */ -private fun Instant.toZonedDateTime(zone: TimeZone): ZonedDateTime { - val currentOffset = zone.offsetAt(this) - return ZonedDateTime(toLocalDateTimeImpl(currentOffset), currentOffset) -} - -/** Check that [Instant] fits in [ZonedDateTime]. +/** Check that [Instant] fits in [LocalDateTime]. * This is done on the results of computations for consistency with other platforms. */ private fun Instant.check(zone: TimeZone): Instant = this@check.also { - toZonedDateTimeFailing(zone) + toLocalDateTimeFailing(offsetIn(zone)) } public actual fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Instant = try { with(period) { - val withDate = toZonedDateTimeFailing(timeZone) - .run { if (totalMonths != 0) timeZone.atZone(dateTime.plus(totalMonths, DateTimeUnit.MONTH), offset) else this } - .run { if (days != 0) timeZone.atZone(dateTime.plus(days, DateTimeUnit.DAY), offset) else this } - withDate.toInstant() + val initialOffset = offsetIn(timeZone) + val initialLdt = toLocalDateTimeFailing(initialOffset) + val offsetAfterMonths: UtcOffset + val ldtAfterMonths: LocalDateTime + if (totalMonths != 0) { + val (ldt, offset) = timeZone.atZone(initialLdt.plus(totalMonths, DateTimeUnit.MONTH), initialOffset) + offsetAfterMonths = offset + ldtAfterMonths = ldt + } else { + offsetAfterMonths = initialOffset + ldtAfterMonths = initialLdt + } + val instantAfterMonthsAndDays = if (days != 0) { + timeZone.atZone(ldtAfterMonths.plus(days, DateTimeUnit.DAY), offsetAfterMonths).toInstant() + } else { + ldtAfterMonths.toInstant(offsetAfterMonths) + } + instantAfterMonthsAndDays .run { if (totalNanoseconds != 0L) plus(0, totalNanoseconds).check(timeZone) else this } }.check(timeZone) } catch (e: ArithmeticException) { @@ -197,11 +203,9 @@ public actual fun Instant.plus(value: Long, unit: DateTimeUnit, timeZone: TimeZo is DateTimeUnit.DateBased -> { if (value < Int.MIN_VALUE || value > Int.MAX_VALUE) throw ArithmeticException("Can't add a Long date-based value, as it would cause an overflow") - val toZonedDateTimeFailing = toZonedDateTimeFailing(timeZone) - timeZone.atZone( - toZonedDateTimeFailing.dateTime.plus(value.toInt(), unit), - toZonedDateTimeFailing.offset - ).toInstant() + val preferredOffset = offsetIn(timeZone) + val initialLdt = toLocalDateTimeFailing(preferredOffset) + timeZone.atZone(initialLdt.plus(value.toInt(), unit), preferredOffset).toInstant() } is DateTimeUnit.TimeBased -> check(timeZone).plus(value, unit).check(timeZone) @@ -224,23 +228,15 @@ public actual fun Instant.plus(value: Long, unit: DateTimeUnit.TimeBased): Insta } public actual fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateTimePeriod { - var thisLdt = toZonedDateTimeFailing(timeZone) - val otherLdt = other.toZonedDateTimeFailing(timeZone) - - val months = - thisLdt.dateTime.until(otherLdt.dateTime, DateTimeUnit.MONTH).toLong().toInt() // `until` on dates never fails - thisLdt = timeZone.atZone( - thisLdt.dateTime.plus(months, DateTimeUnit.MONTH), - thisLdt.offset - ) // won't throw: thisLdt + months <= otherLdt, which is known to be valid - val days = - thisLdt.dateTime.until(otherLdt.dateTime, DateTimeUnit.DAY).toLong().toInt() // `until` on dates never fails - thisLdt = timeZone.atZone( - thisLdt.dateTime.plus(days, DateTimeUnit.DAY), - thisLdt.offset - ) // won't throw: thisLdt + days <= otherLdt - val nanoseconds = - thisLdt.toInstant().until(otherLdt.toInstant(), DateTimeUnit.NANOSECOND) // |otherLdt - thisLdt| < 24h + val thisOffset1 = offsetIn(timeZone) + val thisLdt1 = toLocalDateTimeFailing(thisOffset1) + val otherLdt = other.toLocalDateTimeFailing(other.offsetIn(timeZone)) + + val months = thisLdt1.until(otherLdt, DateTimeUnit.MONTH).toLong().toInt() // `until` on dates never fails + val (thisLdt2, thisOffset2) = timeZone.atZone(thisLdt1.plus(months, DateTimeUnit.MONTH), thisOffset1) // won't throw: thisLdt + months <= otherLdt, which is known to be valid + val days = thisLdt2.until(otherLdt, DateTimeUnit.DAY).toLong().toInt() // `until` on dates never fails + val (thisLdt3, thisOffset3) = timeZone.atZone(thisLdt2.plus(days, DateTimeUnit.DAY), thisOffset2) // won't throw: thisLdt + days <= otherLdt + val nanoseconds = thisLdt3.toInstant(thisOffset3).until(other, DateTimeUnit.NANOSECOND) // |otherLdt - thisLdt| < 24h return buildDateTimePeriod(months, days, nanoseconds) } @@ -248,7 +244,7 @@ public actual fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateT public actual fun Instant.until(other: Instant, unit: DateTimeUnit, timeZone: TimeZone): Long = when (unit) { is DateTimeUnit.DateBased -> - toZonedDateTimeFailing(timeZone).dateTime.until(other.toZonedDateTimeFailing(timeZone).dateTime, unit) + toLocalDateTimeFailing(offsetIn(timeZone)).until(other.toLocalDateTimeFailing(other.offsetIn(timeZone)), unit) .toLong() is DateTimeUnit.TimeBased -> { check(timeZone); other.check(timeZone) diff --git a/core/native/src/LocalDateTimeWithOffset.kt b/core/native/src/LocalDateTimeWithOffset.kt new file mode 100644 index 00000000..4a48a5a7 --- /dev/null +++ b/core/native/src/LocalDateTimeWithOffset.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2019-2020 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ +/* Based on the ThreeTenBp project. + * Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos + */ + +package kotlinx.datetime + +internal data class LocalDateTimeWithOffset(val dateTime: LocalDateTime, val offset: UtcOffset) + +internal fun LocalDateTimeWithOffset.toInstant(): Instant = + Instant(dateTime.toEpochSecond(offset), dateTime.nanosecond) diff --git a/core/native/src/TimeZone.kt b/core/native/src/TimeZone.kt index f20d2d5e..0b0d9a45 100644 --- a/core/native/src/TimeZone.kt +++ b/core/native/src/TimeZone.kt @@ -95,7 +95,7 @@ public actual open class TimeZone internal constructor() { internal open fun localDateTimeToInstant(dateTime: LocalDateTime): Instant = atZone(dateTime).toInstant() - internal open fun atZone(dateTime: LocalDateTime, preferred: UtcOffset? = null): ZonedDateTime = + internal open fun atZone(dateTime: LocalDateTime, preferred: UtcOffset? = null): LocalDateTimeWithOffset = error("Should be overridden") override fun equals(other: Any?): Boolean = @@ -119,8 +119,8 @@ public actual class FixedOffsetTimeZone internal constructor(public actual val o override fun offsetAtImpl(instant: Instant): UtcOffset = offset - override fun atZone(dateTime: LocalDateTime, preferred: UtcOffset?): ZonedDateTime = - ZonedDateTime(dateTime, offset) + override fun atZone(dateTime: LocalDateTime, preferred: UtcOffset?): LocalDateTimeWithOffset = + LocalDateTimeWithOffset(dateTime, offset) override fun instantToLocalDateTime(instant: Instant): LocalDateTime = instant.toLocalDateTime(offset) override fun localDateTimeToInstant(dateTime: LocalDateTime): Instant = dateTime.toInstant(offset) diff --git a/core/native/src/ZonedDateTime.kt b/core/native/src/ZonedDateTime.kt deleted file mode 100644 index a6fab783..00000000 --- a/core/native/src/ZonedDateTime.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2019-2020 JetBrains s.r.o. - * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. - */ -/* Based on the ThreeTenBp project. - * Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos - */ - -package kotlinx.datetime - -internal class ZonedDateTime(val dateTime: LocalDateTime, val offset: UtcOffset) { - override fun equals(other: Any?): Boolean = - this === other || other is ZonedDateTime && - dateTime == other.dateTime && offset == other.offset - - override fun hashCode(): Int { - return dateTime.hashCode() xor offset.hashCode() - } - - override fun toString(): String { - val str = dateTime.toString() + offset.toString() - return str - } -} - -internal fun ZonedDateTime.toInstant(): Instant = - Instant(dateTime.toEpochSecond(offset), dateTime.nanosecond) diff --git a/core/native/src/internal/RegionTimeZone.kt b/core/native/src/internal/RegionTimeZone.kt index c98d628b..f782a150 100644 --- a/core/native/src/internal/RegionTimeZone.kt +++ b/core/native/src/internal/RegionTimeZone.kt @@ -18,12 +18,12 @@ internal class RegionTimeZone(private val tzid: TimeZoneRules, override val id: } } - override fun atZone(dateTime: LocalDateTime, preferred: UtcOffset?): ZonedDateTime = + override fun atZone(dateTime: LocalDateTime, preferred: UtcOffset?): LocalDateTimeWithOffset = when (val info = tzid.infoAtDatetime(dateTime)) { - is OffsetInfo.Regular -> ZonedDateTime(dateTime, info.offset) + is OffsetInfo.Regular -> LocalDateTimeWithOffset(dateTime, info.offset) is OffsetInfo.Gap -> { try { - ZonedDateTime(dateTime.plusSeconds(info.transitionDurationSeconds), info.offsetAfter) + LocalDateTimeWithOffset(dateTime.plusSeconds(info.transitionDurationSeconds), info.offsetAfter) } catch (e: IllegalArgumentException) { throw DateTimeArithmeticException( "Overflow whet correcting the date-time to not be in the transition gap", @@ -32,7 +32,7 @@ internal class RegionTimeZone(private val tzid: TimeZoneRules, override val id: } } - is OffsetInfo.Overlap -> ZonedDateTime( + is OffsetInfo.Overlap -> LocalDateTimeWithOffset( dateTime, if (info.offsetAfter == preferred) info.offsetAfter else info.offsetBefore ) diff --git a/core/native/test/ThreeTenBpTimeZoneTest.kt b/core/native/test/ThreeTenBpTimeZoneTest.kt index d1798fc2..6356378d 100644 --- a/core/native/test/ThreeTenBpTimeZoneTest.kt +++ b/core/native/test/ThreeTenBpTimeZoneTest.kt @@ -40,7 +40,7 @@ class ThreeTenBpTimeZoneTest { fun overlappingLocalTime() { val t = LocalDateTime(2007, 10, 28, 2, 30, 0, 0) val zone = TimeZone.of("Europe/Paris") - assertEquals(ZonedDateTime( + assertEquals(LocalDateTimeWithOffset( LocalDateTime(2007, 10, 28, 2, 30, 0, 0), UtcOffset(seconds = 2 * 3600) ), zone.atZone(t)) From d6a5fa353f51b4b85cb23eb917503916ebf72515 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Fri, 15 Dec 2023 11:56:08 +0100 Subject: [PATCH 3/6] Don't resolve LocalDateTime between calendar operations See #330 --- core/native/src/Instant.kt | 41 +++++++++++++------------------------- 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/core/native/src/Instant.kt b/core/native/src/Instant.kt index 3551e93e..14c0511c 100644 --- a/core/native/src/Instant.kt +++ b/core/native/src/Instant.kt @@ -166,23 +166,10 @@ private fun Instant.check(zone: TimeZone): Instant = this@check.also { public actual fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Instant = try { with(period) { val initialOffset = offsetIn(timeZone) - val initialLdt = toLocalDateTimeFailing(initialOffset) - val offsetAfterMonths: UtcOffset - val ldtAfterMonths: LocalDateTime - if (totalMonths != 0) { - val (ldt, offset) = timeZone.atZone(initialLdt.plus(totalMonths, DateTimeUnit.MONTH), initialOffset) - offsetAfterMonths = offset - ldtAfterMonths = ldt - } else { - offsetAfterMonths = initialOffset - ldtAfterMonths = initialLdt - } - val instantAfterMonthsAndDays = if (days != 0) { - timeZone.atZone(ldtAfterMonths.plus(days, DateTimeUnit.DAY), offsetAfterMonths).toInstant() - } else { - ldtAfterMonths.toInstant(offsetAfterMonths) - } - instantAfterMonthsAndDays + val newLdt = toLocalDateTimeFailing(initialOffset) + .run { if (totalMonths != 0) { plus(totalMonths, DateTimeUnit.MONTH) } else { this } } + .run { if (days != 0) { plus(days, DateTimeUnit.DAY) } else { this } } + timeZone.atZone(newLdt, preferred = initialOffset).toInstant() .run { if (totalNanoseconds != 0L) plus(0, totalNanoseconds).check(timeZone) else this } }.check(timeZone) } catch (e: ArithmeticException) { @@ -203,9 +190,9 @@ public actual fun Instant.plus(value: Long, unit: DateTimeUnit, timeZone: TimeZo is DateTimeUnit.DateBased -> { if (value < Int.MIN_VALUE || value > Int.MAX_VALUE) throw ArithmeticException("Can't add a Long date-based value, as it would cause an overflow") - val preferredOffset = offsetIn(timeZone) - val initialLdt = toLocalDateTimeFailing(preferredOffset) - timeZone.atZone(initialLdt.plus(value.toInt(), unit), preferredOffset).toInstant() + val initialOffset = offsetIn(timeZone) + val initialLdt = toLocalDateTimeFailing(initialOffset) + timeZone.atZone(initialLdt.plus(value.toInt(), unit), preferred = initialOffset).toInstant() } is DateTimeUnit.TimeBased -> check(timeZone).plus(value, unit).check(timeZone) @@ -228,15 +215,15 @@ public actual fun Instant.plus(value: Long, unit: DateTimeUnit.TimeBased): Insta } public actual fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateTimePeriod { - val thisOffset1 = offsetIn(timeZone) - val thisLdt1 = toLocalDateTimeFailing(thisOffset1) + val initialOffset = offsetIn(timeZone) + val initialLdt = toLocalDateTimeFailing(initialOffset) val otherLdt = other.toLocalDateTimeFailing(other.offsetIn(timeZone)) - val months = thisLdt1.until(otherLdt, DateTimeUnit.MONTH).toLong().toInt() // `until` on dates never fails - val (thisLdt2, thisOffset2) = timeZone.atZone(thisLdt1.plus(months, DateTimeUnit.MONTH), thisOffset1) // won't throw: thisLdt + months <= otherLdt, which is known to be valid - val days = thisLdt2.until(otherLdt, DateTimeUnit.DAY).toLong().toInt() // `until` on dates never fails - val (thisLdt3, thisOffset3) = timeZone.atZone(thisLdt2.plus(days, DateTimeUnit.DAY), thisOffset2) // won't throw: thisLdt + days <= otherLdt - val nanoseconds = thisLdt3.toInstant(thisOffset3).until(other, DateTimeUnit.NANOSECOND) // |otherLdt - thisLdt| < 24h + val months = initialLdt.until(otherLdt, DateTimeUnit.MONTH).toLong().toInt() // `until` on dates never fails + val ldtWithMonths = initialLdt.plus(months, DateTimeUnit.MONTH) // won't throw: thisLdt + months <= otherLdt, which is known to be valid + val days = ldtWithMonths.until(otherLdt, DateTimeUnit.DAY).toLong().toInt() // `until` on dates never fails + val newInstant = timeZone.atZone(ldtWithMonths.plus(days, DateTimeUnit.DAY), preferred = initialOffset).toInstant() // won't throw: thisLdt + days <= otherLdt + val nanoseconds = newInstant.until(other, DateTimeUnit.NANOSECOND) // |otherLdt - thisLdt| < 24h return buildDateTimePeriod(months, days, nanoseconds) } From 3b8ee5d7c8145d6102ba2d6522676bf95e069ec9 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Fri, 15 Dec 2023 12:01:08 +0100 Subject: [PATCH 4/6] Remove ZonedDateTime from the Native implementation --- core/native/src/Instant.kt | 6 +++--- core/native/src/LocalDateTimeWithOffset.kt | 14 -------------- core/native/src/TimeZone.kt | 10 +++------- core/native/src/internal/RegionTimeZone.kt | 9 ++++----- core/native/test/ThreeTenBpTimeZoneTest.kt | 7 ++----- 5 files changed, 12 insertions(+), 34 deletions(-) delete mode 100644 core/native/src/LocalDateTimeWithOffset.kt diff --git a/core/native/src/Instant.kt b/core/native/src/Instant.kt index 14c0511c..99404b34 100644 --- a/core/native/src/Instant.kt +++ b/core/native/src/Instant.kt @@ -169,7 +169,7 @@ public actual fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Inst val newLdt = toLocalDateTimeFailing(initialOffset) .run { if (totalMonths != 0) { plus(totalMonths, DateTimeUnit.MONTH) } else { this } } .run { if (days != 0) { plus(days, DateTimeUnit.DAY) } else { this } } - timeZone.atZone(newLdt, preferred = initialOffset).toInstant() + timeZone.localDateTimeToInstant(newLdt, preferred = initialOffset) .run { if (totalNanoseconds != 0L) plus(0, totalNanoseconds).check(timeZone) else this } }.check(timeZone) } catch (e: ArithmeticException) { @@ -192,7 +192,7 @@ public actual fun Instant.plus(value: Long, unit: DateTimeUnit, timeZone: TimeZo throw ArithmeticException("Can't add a Long date-based value, as it would cause an overflow") val initialOffset = offsetIn(timeZone) val initialLdt = toLocalDateTimeFailing(initialOffset) - timeZone.atZone(initialLdt.plus(value.toInt(), unit), preferred = initialOffset).toInstant() + timeZone.localDateTimeToInstant(initialLdt.plus(value.toInt(), unit), preferred = initialOffset) } is DateTimeUnit.TimeBased -> check(timeZone).plus(value, unit).check(timeZone) @@ -222,7 +222,7 @@ public actual fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateT val months = initialLdt.until(otherLdt, DateTimeUnit.MONTH).toLong().toInt() // `until` on dates never fails val ldtWithMonths = initialLdt.plus(months, DateTimeUnit.MONTH) // won't throw: thisLdt + months <= otherLdt, which is known to be valid val days = ldtWithMonths.until(otherLdt, DateTimeUnit.DAY).toLong().toInt() // `until` on dates never fails - val newInstant = timeZone.atZone(ldtWithMonths.plus(days, DateTimeUnit.DAY), preferred = initialOffset).toInstant() // won't throw: thisLdt + days <= otherLdt + val newInstant = timeZone.localDateTimeToInstant(ldtWithMonths.plus(days, DateTimeUnit.DAY), preferred = initialOffset) // won't throw: thisLdt + days <= otherLdt val nanoseconds = newInstant.until(other, DateTimeUnit.NANOSECOND) // |otherLdt - thisLdt| < 24h return buildDateTimePeriod(months, days, nanoseconds) diff --git a/core/native/src/LocalDateTimeWithOffset.kt b/core/native/src/LocalDateTimeWithOffset.kt deleted file mode 100644 index 4a48a5a7..00000000 --- a/core/native/src/LocalDateTimeWithOffset.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright 2019-2020 JetBrains s.r.o. - * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. - */ -/* Based on the ThreeTenBp project. - * Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos - */ - -package kotlinx.datetime - -internal data class LocalDateTimeWithOffset(val dateTime: LocalDateTime, val offset: UtcOffset) - -internal fun LocalDateTimeWithOffset.toInstant(): Instant = - Instant(dateTime.toEpochSecond(offset), dateTime.nanosecond) diff --git a/core/native/src/TimeZone.kt b/core/native/src/TimeZone.kt index 0b0d9a45..8b1ec08c 100644 --- a/core/native/src/TimeZone.kt +++ b/core/native/src/TimeZone.kt @@ -92,10 +92,7 @@ public actual open class TimeZone internal constructor() { throw DateTimeArithmeticException("Instant $instant is not representable as LocalDateTime.", e) } - internal open fun localDateTimeToInstant(dateTime: LocalDateTime): Instant = - atZone(dateTime).toInstant() - - internal open fun atZone(dateTime: LocalDateTime, preferred: UtcOffset? = null): LocalDateTimeWithOffset = + internal open fun localDateTimeToInstant(dateTime: LocalDateTime, preferred: UtcOffset? = null): Instant = error("Should be overridden") override fun equals(other: Any?): Boolean = @@ -119,11 +116,10 @@ public actual class FixedOffsetTimeZone internal constructor(public actual val o override fun offsetAtImpl(instant: Instant): UtcOffset = offset - override fun atZone(dateTime: LocalDateTime, preferred: UtcOffset?): LocalDateTimeWithOffset = - LocalDateTimeWithOffset(dateTime, offset) + override fun localDateTimeToInstant(dateTime: LocalDateTime, preferred: UtcOffset?): Instant = + dateTime.toInstant(offset) override fun instantToLocalDateTime(instant: Instant): LocalDateTime = instant.toLocalDateTime(offset) - override fun localDateTimeToInstant(dateTime: LocalDateTime): Instant = dateTime.toInstant(offset) } diff --git a/core/native/src/internal/RegionTimeZone.kt b/core/native/src/internal/RegionTimeZone.kt index f782a150..a42ee8fa 100644 --- a/core/native/src/internal/RegionTimeZone.kt +++ b/core/native/src/internal/RegionTimeZone.kt @@ -18,12 +18,12 @@ internal class RegionTimeZone(private val tzid: TimeZoneRules, override val id: } } - override fun atZone(dateTime: LocalDateTime, preferred: UtcOffset?): LocalDateTimeWithOffset = + override fun localDateTimeToInstant(dateTime: LocalDateTime, preferred: UtcOffset?): Instant = when (val info = tzid.infoAtDatetime(dateTime)) { - is OffsetInfo.Regular -> LocalDateTimeWithOffset(dateTime, info.offset) + is OffsetInfo.Regular -> dateTime.toInstant(info.offset) is OffsetInfo.Gap -> { try { - LocalDateTimeWithOffset(dateTime.plusSeconds(info.transitionDurationSeconds), info.offsetAfter) + dateTime.plusSeconds(info.transitionDurationSeconds).toInstant(info.offsetAfter) } catch (e: IllegalArgumentException) { throw DateTimeArithmeticException( "Overflow whet correcting the date-time to not be in the transition gap", @@ -32,8 +32,7 @@ internal class RegionTimeZone(private val tzid: TimeZoneRules, override val id: } } - is OffsetInfo.Overlap -> LocalDateTimeWithOffset( - dateTime, + is OffsetInfo.Overlap -> dateTime.toInstant( if (info.offsetAfter == preferred) info.offsetAfter else info.offsetBefore ) } diff --git a/core/native/test/ThreeTenBpTimeZoneTest.kt b/core/native/test/ThreeTenBpTimeZoneTest.kt index 6356378d..07b08d64 100644 --- a/core/native/test/ThreeTenBpTimeZoneTest.kt +++ b/core/native/test/ThreeTenBpTimeZoneTest.kt @@ -33,17 +33,14 @@ class ThreeTenBpTimeZoneTest { val t1 = LocalDateTime(2020, 3, 29, 2, 14, 17, 201) val t2 = LocalDateTime(2020, 3, 29, 3, 14, 17, 201) val tz = TimeZone.of("Europe/Berlin") - assertEquals(tz.atZone(t1), tz.atZone(t2)) + assertEquals(tz.localDateTimeToInstant(t1), tz.localDateTimeToInstant(t2)) } @Test fun overlappingLocalTime() { val t = LocalDateTime(2007, 10, 28, 2, 30, 0, 0) val zone = TimeZone.of("Europe/Paris") - assertEquals(LocalDateTimeWithOffset( - LocalDateTime(2007, 10, 28, 2, 30, 0, 0), - UtcOffset(seconds = 2 * 3600) - ), zone.atZone(t)) + assertEquals(t.toInstant(UtcOffset(hours = 2)), zone.localDateTimeToInstant(t)) } } From 6d9c79754859df8ba6e2ab05a2c2d75672bed56e Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Fri, 15 Dec 2023 12:09:03 +0100 Subject: [PATCH 5/6] Cleanup --- core/native/src/Instant.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/native/src/Instant.kt b/core/native/src/Instant.kt index 99404b34..b1df3c57 100644 --- a/core/native/src/Instant.kt +++ b/core/native/src/Instant.kt @@ -219,9 +219,9 @@ public actual fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateT val initialLdt = toLocalDateTimeFailing(initialOffset) val otherLdt = other.toLocalDateTimeFailing(other.offsetIn(timeZone)) - val months = initialLdt.until(otherLdt, DateTimeUnit.MONTH).toLong().toInt() // `until` on dates never fails + val months = initialLdt.until(otherLdt, DateTimeUnit.MONTH) // `until` on dates never fails val ldtWithMonths = initialLdt.plus(months, DateTimeUnit.MONTH) // won't throw: thisLdt + months <= otherLdt, which is known to be valid - val days = ldtWithMonths.until(otherLdt, DateTimeUnit.DAY).toLong().toInt() // `until` on dates never fails + val days = ldtWithMonths.until(otherLdt, DateTimeUnit.DAY) // `until` on dates never fails val newInstant = timeZone.localDateTimeToInstant(ldtWithMonths.plus(days, DateTimeUnit.DAY), preferred = initialOffset) // won't throw: thisLdt + days <= otherLdt val nanoseconds = newInstant.until(other, DateTimeUnit.NANOSECOND) // |otherLdt - thisLdt| < 24h From 24dfa69e33f251f7409eb5ae2d8f7e01761c0777 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Mon, 18 Dec 2023 12:25:51 +0100 Subject: [PATCH 6/6] Simplify --- core/native/src/Instant.kt | 48 ++++++++++++++------------------ core/native/src/LocalDate.kt | 24 +++++----------- core/native/src/LocalDateTime.kt | 13 ++------- core/native/src/LocalTime.kt | 15 ++-------- core/native/src/TimeZone.kt | 22 ++++----------- core/native/src/internal/util.kt | 8 ++++++ 6 files changed, 48 insertions(+), 82 deletions(-) create mode 100644 core/native/src/internal/util.kt diff --git a/core/native/src/Instant.kt b/core/native/src/Instant.kt index b1df3c57..7fa5202b 100644 --- a/core/native/src/Instant.kt +++ b/core/native/src/Instant.kt @@ -36,7 +36,7 @@ private const val MIN_SECOND = -31619119219200L // -1000000-01-01T00:00:00Z */ private const val MAX_SECOND = 31494816403199L // +1000000-12-31T23:59:59 -private fun isValidInstantSecond(second: Long) = second >= MIN_SECOND && second <= MAX_SECOND +private fun isValidInstantSecond(second: Long) = second in MIN_SECOND..MAX_SECOND @Serializable(with = InstantIso8601Serializer::class) public actual class Instant internal constructor(public actual val epochSeconds: Long, public actual val nanosecondsOfSecond: Int) : Comparable { @@ -54,14 +54,11 @@ public actual class Instant internal constructor(public actual val epochSeconds: * @throws ArithmeticException if arithmetic overflow occurs * @throws IllegalArgumentException if the boundaries of Instant are overflown */ - internal fun plus(secondsToAdd: Long, nanosToAdd: Long): Instant { - if ((secondsToAdd or nanosToAdd) == 0L) { - return this - } + internal fun plus(secondsToAdd: Long, nanosToAdd: Long): Instant = onNonZero(secondsToAdd or nanosToAdd) { val newEpochSeconds: Long = safeAdd(safeAdd(epochSeconds, secondsToAdd), (nanosToAdd / NANOS_PER_ONE)) val newNanosToAdd = nanosToAdd % NANOS_PER_ONE val nanoAdjustment = (nanosecondsOfSecond + newNanosToAdd) // safe int+NANOS_PER_ONE - return fromEpochSecondsThrowing(newEpochSeconds, nanoAdjustment) + fromEpochSecondsThrowing(newEpochSeconds, nanoAdjustment) } public actual operator fun plus(duration: Duration): Instant = duration.toComponents { secondsToAdd, nanosecondsToAdd -> @@ -81,10 +78,7 @@ public actual class Instant internal constructor(public actual val epochSeconds: (this.nanosecondsOfSecond - other.nanosecondsOfSecond).nanoseconds actual override fun compareTo(other: Instant): Int { - val s = epochSeconds.compareTo(other.epochSeconds) - if (s != 0) { - return s - } + onNonZero(epochSeconds.compareTo(other.epochSeconds)) { return it } return nanosecondsOfSecond.compareTo(other.nanosecondsOfSecond) } @@ -150,27 +144,21 @@ public actual class Instant internal constructor(public actual val epochSeconds: } -private fun Instant.toLocalDateTimeFailing(offset: UtcOffset): LocalDateTime = try { - toLocalDateTimeImpl(offset) -} catch (e: IllegalArgumentException) { - throw DateTimeArithmeticException("Can not convert instant $this to LocalDateTime to perform computations", e) -} - /** Check that [Instant] fits in [LocalDateTime]. * This is done on the results of computations for consistency with other platforms. */ private fun Instant.check(zone: TimeZone): Instant = this@check.also { - toLocalDateTimeFailing(offsetIn(zone)) + toLocalDateTime(offsetIn(zone)) } public actual fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Instant = try { with(period) { val initialOffset = offsetIn(timeZone) - val newLdt = toLocalDateTimeFailing(initialOffset) - .run { if (totalMonths != 0) { plus(totalMonths, DateTimeUnit.MONTH) } else { this } } - .run { if (days != 0) { plus(days, DateTimeUnit.DAY) } else { this } } + val newLdt = toLocalDateTime(initialOffset) + .run { onNonZero(totalMonths) { plus(it, DateTimeUnit.MONTH) } } + .run { onNonZero(days) { plus(it, DateTimeUnit.DAY) } } timeZone.localDateTimeToInstant(newLdt, preferred = initialOffset) - .run { if (totalNanoseconds != 0L) plus(0, totalNanoseconds).check(timeZone) else this } + .run { onNonZero(totalNanoseconds) { plus(totalNanoseconds, DateTimeUnit.NANOSECOND).check(timeZone) } } }.check(timeZone) } catch (e: ArithmeticException) { throw DateTimeArithmeticException("Arithmetic overflow when adding CalendarPeriod to an Instant", e) @@ -191,7 +179,7 @@ public actual fun Instant.plus(value: Long, unit: DateTimeUnit, timeZone: TimeZo if (value < Int.MIN_VALUE || value > Int.MAX_VALUE) throw ArithmeticException("Can't add a Long date-based value, as it would cause an overflow") val initialOffset = offsetIn(timeZone) - val initialLdt = toLocalDateTimeFailing(initialOffset) + val initialLdt = toLocalDateTime(initialOffset) timeZone.localDateTimeToInstant(initialLdt.plus(value.toInt(), unit), preferred = initialOffset) } is DateTimeUnit.TimeBased -> @@ -216,13 +204,19 @@ public actual fun Instant.plus(value: Long, unit: DateTimeUnit.TimeBased): Insta public actual fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateTimePeriod { val initialOffset = offsetIn(timeZone) - val initialLdt = toLocalDateTimeFailing(initialOffset) - val otherLdt = other.toLocalDateTimeFailing(other.offsetIn(timeZone)) + val initialLdt = toLocalDateTime(initialOffset) + val otherLdt = other.toLocalDateTime(other.offsetIn(timeZone)) val months = initialLdt.until(otherLdt, DateTimeUnit.MONTH) // `until` on dates never fails - val ldtWithMonths = initialLdt.plus(months, DateTimeUnit.MONTH) // won't throw: thisLdt + months <= otherLdt, which is known to be valid + val ldtWithMonths = initialLdt.plus( + months, + DateTimeUnit.MONTH + ) // won't throw: thisLdt + months <= otherLdt, which is known to be valid val days = ldtWithMonths.until(otherLdt, DateTimeUnit.DAY) // `until` on dates never fails - val newInstant = timeZone.localDateTimeToInstant(ldtWithMonths.plus(days, DateTimeUnit.DAY), preferred = initialOffset) // won't throw: thisLdt + days <= otherLdt + val newInstant = timeZone.localDateTimeToInstant( + ldtWithMonths.plus(days, DateTimeUnit.DAY), + preferred = initialOffset + ) // won't throw: thisLdt + days <= otherLdt val nanoseconds = newInstant.until(other, DateTimeUnit.NANOSECOND) // |otherLdt - thisLdt| < 24h return buildDateTimePeriod(months, days, nanoseconds) @@ -231,7 +225,7 @@ public actual fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateT public actual fun Instant.until(other: Instant, unit: DateTimeUnit, timeZone: TimeZone): Long = when (unit) { is DateTimeUnit.DateBased -> - toLocalDateTimeFailing(offsetIn(timeZone)).until(other.toLocalDateTimeFailing(other.offsetIn(timeZone)), unit) + toLocalDateTime(offsetIn(timeZone)).until(other.toLocalDateTime(other.offsetIn(timeZone)), unit) .toLong() is DateTimeUnit.TimeBased -> { check(timeZone); other.check(timeZone) diff --git a/core/native/src/LocalDate.kt b/core/native/src/LocalDate.kt index 3c53b949..acd41c43 100644 --- a/core/native/src/LocalDate.kt +++ b/core/native/src/LocalDate.kt @@ -134,14 +134,8 @@ public actual class LocalDate actual constructor(public actual val year: Int, pu // Several times faster than using `compareBy` actual override fun compareTo(other: LocalDate): Int { - val y = year.compareTo(other.year) - if (y != 0) { - return y - } - val m = monthNumber.compareTo(other.monthNumber) - if (m != 0) { - return m - } + onNonZero(year.compareTo(other.year)) { return it } + onNonZero(monthNumber.compareTo(other.monthNumber)) { return it } return dayOfMonth.compareTo(other.dayOfMonth) } @@ -159,15 +153,12 @@ public actual class LocalDate actual constructor(public actual val year: Int, pu * @throws IllegalArgumentException if the result exceeds the boundaries * @throws ArithmeticException if arithmetic overflow occurs */ - internal fun plusMonths(monthsToAdd: Int): LocalDate { - if (monthsToAdd == 0) { - return this - } + internal fun plusMonths(monthsToAdd: Int): LocalDate = onNonZero(monthsToAdd) { val monthCount = year * 12 + (monthNumber - 1) val calcMonths = safeAdd(monthCount, monthsToAdd) val newYear = calcMonths.floorDiv(12) val newMonth = calcMonths.mod(12) + 1 - return resolvePreviousValid(newYear, newMonth, dayOfMonth) + resolvePreviousValid(newYear, newMonth, dayOfMonth) } // org.threeten.bp.LocalDate#plusDays @@ -176,8 +167,7 @@ public actual class LocalDate actual constructor(public actual val year: Int, pu * @throws ArithmeticException if arithmetic overflow occurs */ internal fun plusDays(daysToAdd: Int): LocalDate = - if (daysToAdd == 0) this - else fromEpochDays(safeAdd(toEpochDays(), daysToAdd)) + onNonZero(daysToAdd) { fromEpochDays(safeAdd(toEpochDays(), daysToAdd)) } override fun equals(other: Any?): Boolean = this === other || (other is LocalDate && compareTo(other) == 0) @@ -221,8 +211,8 @@ public actual operator fun LocalDate.plus(period: DatePeriod): LocalDate = with(period) { try { this@plus - .run { if (totalMonths != 0) plusMonths(totalMonths) else this } - .run { if (days != 0) plusDays(days) else this } + .run { onNonZero(totalMonths) { plusMonths(it) } } + .run { onNonZero(days) { plusDays(days) } } } catch (e: ArithmeticException) { throw DateTimeArithmeticException("Arithmetic overflow when adding a period to a date", e) } catch (e: IllegalArgumentException) { diff --git a/core/native/src/LocalDateTime.kt b/core/native/src/LocalDateTime.kt index c62ef115..20f7941a 100644 --- a/core/native/src/LocalDateTime.kt +++ b/core/native/src/LocalDateTime.kt @@ -51,10 +51,7 @@ public actual constructor(public actual val date: LocalDate, public actual val t // Several times faster than using `compareBy` actual override fun compareTo(other: LocalDateTime): Int { - val d = date.compareTo(other.date) - if (d != 0) { - return d - } + onNonZero(date.compareTo(other.date)) { return it } return time.compareTo(other.time) } @@ -112,11 +109,7 @@ internal fun LocalDateTime.until(other: LocalDateTime, unit: DateTimeUnit.TimeBa * @throws IllegalArgumentException if the result exceeds the boundaries * @throws ArithmeticException if arithmetic overflow occurs */ -internal fun LocalDateTime.plusSeconds(seconds: Int): LocalDateTime -{ - if (seconds == 0) { - return this - } +internal fun LocalDateTime.plusSeconds(seconds: Int): LocalDateTime = onNonZero(seconds) { val currentNanoOfDay = time.toNanosecondOfDay() // at most a day val totalNanos: Long = seconds % SECONDS_PER_DAY * NANOS_PER_ONE.toLong() + // at most a day currentNanoOfDay @@ -124,7 +117,7 @@ internal fun LocalDateTime.plusSeconds(seconds: Int): LocalDateTime totalNanos.floorDiv(NANOS_PER_DAY) // max 2 days val newNanoOfDay: Long = totalNanos.mod(NANOS_PER_DAY) val newTime: LocalTime = if (newNanoOfDay == currentNanoOfDay) time else LocalTime.ofNanoOfDay(newNanoOfDay) - return LocalDateTime(date.plusDays(totalDays.toInt()), newTime) + LocalDateTime(date.plusDays(totalDays.toInt()), newTime) } private val ISO_DATETIME_OPTIONAL_SECONDS_TRAILING_ZEROS by lazy { diff --git a/core/native/src/LocalTime.kt b/core/native/src/LocalTime.kt index b22b34c7..94aa6d37 100644 --- a/core/native/src/LocalTime.kt +++ b/core/native/src/LocalTime.kt @@ -86,18 +86,9 @@ public actual class LocalTime actual constructor( // Several times faster than using `compareBy` actual override fun compareTo(other: LocalTime): Int { - val h = hour.compareTo(other.hour) - if (h != 0) { - return h - } - val m = minute.compareTo(other.minute) - if (m != 0) { - return m - } - val s = second.compareTo(other.second) - if (s != 0) { - return s - } + onNonZero(hour.compareTo(other.hour)) { return it } + onNonZero(minute.compareTo(other.minute)) { return it } + onNonZero(second.compareTo(other.second)) { return it } return nanosecond.compareTo(other.nanosecond) } diff --git a/core/native/src/TimeZone.kt b/core/native/src/TimeZone.kt index 8b1ec08c..827c9acb 100644 --- a/core/native/src/TimeZone.kt +++ b/core/native/src/TimeZone.kt @@ -51,17 +51,12 @@ public actual open class TimeZone internal constructor() { ) { val prefix = zoneId.take(3) val offset = lenientOffsetFormat.parse(zoneId.substring(3)) - return when (offset.totalSeconds) { - 0 -> FixedOffsetTimeZone(offset, prefix) - else -> FixedOffsetTimeZone(offset, "$prefix$offset") - } + return FixedOffsetTimeZone(offset, prefix.onNonZero(offset.totalSeconds) { "$prefix$offset" }) } if (zoneId.startsWith("UT+") || zoneId.startsWith("UT-")) { val offset = lenientOffsetFormat.parse(zoneId.substring(2)) - return when (offset.totalSeconds) { - 0 -> FixedOffsetTimeZone(offset, "UT") - else -> FixedOffsetTimeZone(offset, "UT$offset") - } + val prefix = "UT" + return FixedOffsetTimeZone(offset, prefix.onNonZero(offset.totalSeconds) { "$prefix$offset"}) } } catch (e: DateTimeFormatException) { throw IllegalTimeZoneException(e) @@ -83,14 +78,11 @@ public actual open class TimeZone internal constructor() { public actual fun Instant.toLocalDateTime(): LocalDateTime = instantToLocalDateTime(this) public actual fun LocalDateTime.toInstant(): Instant = localDateTimeToInstant(this) - internal open fun atStartOfDay(date: LocalDate): Instant = error("Should be overridden") //value.atStartOfDay(date) + internal open fun atStartOfDay(date: LocalDate): Instant = error("Should be overridden") internal open fun offsetAtImpl(instant: Instant): UtcOffset = error("Should be overridden") - internal open fun instantToLocalDateTime(instant: Instant): LocalDateTime = try { - instant.toLocalDateTimeImpl(offsetAtImpl(instant)) - } catch (e: IllegalArgumentException) { - throw DateTimeArithmeticException("Instant $instant is not representable as LocalDateTime.", e) - } + internal open fun instantToLocalDateTime(instant: Instant): LocalDateTime = + instant.toLocalDateTime(offsetAtImpl(instant)) internal open fun localDateTimeToInstant(dateTime: LocalDateTime, preferred: UtcOffset? = null): Instant = error("Should be overridden") @@ -118,8 +110,6 @@ public actual class FixedOffsetTimeZone internal constructor(public actual val o override fun localDateTimeToInstant(dateTime: LocalDateTime, preferred: UtcOffset?): Instant = dateTime.toInstant(offset) - - override fun instantToLocalDateTime(instant: Instant): LocalDateTime = instant.toLocalDateTime(offset) } diff --git a/core/native/src/internal/util.kt b/core/native/src/internal/util.kt new file mode 100644 index 00000000..92bfb5c6 --- /dev/null +++ b/core/native/src/internal/util.kt @@ -0,0 +1,8 @@ +/* + * Copyright 2019-2023 JetBrains s.r.o. and contributors. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.internal + +internal inline fun A.onNonZero(value: T, block: (T) -> A): A = if (value != 0) block(value) else this