diff --git a/core/native/src/Instant.kt b/core/native/src/Instant.kt index 32474be45..7fa5202b5 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<Instant> { @@ -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,34 +144,21 @@ public actual class Instant internal constructor(public actual val epochSeconds: } -private fun Instant.toZonedDateTimeFailing(zone: TimeZone): ZonedDateTime = try { - toZonedDateTime(zone) -} 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), zone, 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) + toLocalDateTime(offsetIn(zone)) } 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 } - withDate.toInstant() - .run { if (totalNanoseconds != 0L) plus(0, totalNanoseconds).check(timeZone) else this } + val initialOffset = offsetIn(timeZone) + val newLdt = toLocalDateTime(initialOffset) + .run { onNonZero(totalMonths) { plus(it, DateTimeUnit.MONTH) } } + .run { onNonZero(days) { plus(it, DateTimeUnit.DAY) } } + timeZone.localDateTimeToInstant(newLdt, preferred = initialOffset) + .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) @@ -197,7 +178,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") - toZonedDateTimeFailing(timeZone).plus(value.toInt(), unit).toInstant() + val initialOffset = offsetIn(timeZone) + val initialLdt = toLocalDateTime(initialOffset) + timeZone.localDateTimeToInstant(initialLdt.plus(value.toInt(), unit), preferred = initialOffset) } is DateTimeUnit.TimeBased -> check(timeZone).plus(value, unit).check(timeZone) @@ -220,14 +203,21 @@ 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.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 initialOffset = 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 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 return buildDateTimePeriod(months, days, nanoseconds) } @@ -235,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 -> - toZonedDateTimeFailing(timeZone).dateTime.until(other.toZonedDateTimeFailing(timeZone).dateTime, 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 3c53b949e..acd41c433 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 c62ef1154..20f7941a3 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 b22b34c77..94aa6d37a 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 2a8fc3e2c..827c9acbe 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,19 +78,13 @@ 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 localDateTimeToInstant(dateTime: LocalDateTime): Instant = - atZone(dateTime).toInstant() + internal open fun instantToLocalDateTime(instant: Instant): LocalDateTime = + instant.toLocalDateTime(offsetAtImpl(instant)) - internal open fun atZone(dateTime: LocalDateTime, preferred: UtcOffset? = null): ZonedDateTime = + internal open fun localDateTimeToInstant(dateTime: LocalDateTime, preferred: UtcOffset? = null): Instant = error("Should be overridden") override fun equals(other: Any?): Boolean = @@ -119,11 +108,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, this, offset) - - override fun instantToLocalDateTime(instant: Instant): LocalDateTime = instant.toLocalDateTime(offset) - override fun localDateTimeToInstant(dateTime: LocalDateTime): Instant = dateTime.toInstant(offset) + override fun localDateTimeToInstant(dateTime: LocalDateTime, preferred: UtcOffset?): Instant = + dateTime.toInstant(offset) } diff --git a/core/native/src/ZonedDateTime.kt b/core/native/src/ZonedDateTime.kt deleted file mode 100644 index 655da4408..000000000 --- a/core/native/src/ZonedDateTime.kt +++ /dev/null @@ -1,74 +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, 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) - } - - override fun equals(other: Any?): Boolean = - this === other || other is ZonedDateTime && - dateTime == other.dateTime && offset == other.offset && zone == other.zone - - override fun hashCode(): Int { - return dateTime.hashCode() xor offset.hashCode() xor zone.hashCode().rotateLeft(3) - } - - override fun toString(): String { - var str = dateTime.toString() + offset.toString() - if (zone !is FixedOffsetTimeZone || offset !== zone.offset) { - str += "[$zone]" - } - 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 d4e7bc6b6..a42ee8fab 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 localDateTimeToInstant(dateTime: LocalDateTime, preferred: UtcOffset?): Instant = when (val info = tzid.infoAtDatetime(dateTime)) { - is OffsetInfo.Regular -> ZonedDateTime(dateTime, this, info.offset) + is OffsetInfo.Regular -> dateTime.toInstant(info.offset) is OffsetInfo.Gap -> { try { - ZonedDateTime(dateTime.plusSeconds(info.transitionDurationSeconds), this, 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,9 @@ 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 -> dateTime.toInstant( + if (info.offsetAfter == preferred) info.offsetAfter else info.offsetBefore + ) } override fun offsetAtImpl(instant: Instant): UtcOffset = tzid.infoAtInstant(instant) diff --git a/core/native/src/internal/util.kt b/core/native/src/internal/util.kt new file mode 100644 index 000000000..92bfb5c60 --- /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, T: Number> A.onNonZero(value: T, block: (T) -> A): A = if (value != 0) block(value) else this diff --git a/core/native/test/ThreeTenBpTimeZoneTest.kt b/core/native/test/ThreeTenBpTimeZoneTest.kt index 95ee1cf81..07b08d649 100644 --- a/core/native/test/ThreeTenBpTimeZoneTest.kt +++ b/core/native/test/ThreeTenBpTimeZoneTest.kt @@ -33,15 +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(ZonedDateTime(LocalDateTime(2007, 10, 28, 2, 30, 0, 0), - zone, UtcOffset(seconds = 2 * 3600)), zone.atZone(t)) + assertEquals(t.toInstant(UtcOffset(hours = 2)), zone.localDateTimeToInstant(t)) } }