diff --git a/core/common/src/Instant.kt b/core/common/src/Instant.kt index 3deedd94..d6f4ea6b 100644 --- a/core/common/src/Instant.kt +++ b/core/common/src/Instant.kt @@ -447,7 +447,37 @@ public fun String.toInstant(): Instant = Instant.parse(this) * [LocalDateTime]. * @sample kotlinx.datetime.test.samples.InstantSamples.plusPeriod */ -public expect fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Instant +public fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Instant = try { + with(period) { + val initialOffset = offsetIn(timeZone) + val initialLdt = toLocalDateTimeFailing(initialOffset) + val instantAfterMonths: Instant + val offsetAfterMonths: UtcOffset + val ldtAfterMonths: LocalDateTime + if (totalMonths != 0L) { + val unresolvedLdtWithMonths = initialLdt.plus(totalMonths, DateTimeUnit.MONTH) + instantAfterMonths = localDateTimeToInstant(unresolvedLdtWithMonths, timeZone, preferred = initialOffset) + offsetAfterMonths = instantAfterMonths.offsetIn(timeZone) + ldtAfterMonths = instantAfterMonths.toLocalDateTime(offsetAfterMonths) + } else { + instantAfterMonths = this@plus + offsetAfterMonths = initialOffset + ldtAfterMonths = initialLdt + } + val instantAfterMonthsAndDays = if (days != 0) { + val unresolvedLdtWithDays = ldtAfterMonths.plus(days, DateTimeUnit.DAY) + localDateTimeToInstant(unresolvedLdtWithDays, timeZone, preferred = offsetAfterMonths) + } else { + instantAfterMonths + } + instantAfterMonthsAndDays + .run { if (totalNanoseconds != 0L) plus(0, totalNanoseconds).check(timeZone) else this } + }.check(timeZone) +} catch (e: ArithmeticException) { + throw DateTimeArithmeticException("Arithmetic overflow when adding CalendarPeriod to an Instant", e) +} catch (e: IllegalArgumentException) { + throw DateTimeArithmeticException("Boundaries of Instant exceeded when adding CalendarPeriod", e) +} /** * Returns an instant that is the result of subtracting components of [DateTimePeriod] from this instant. The components @@ -489,7 +519,25 @@ public fun Instant.minus(period: DateTimePeriod, timeZone: TimeZone): Instant = * @throws DateTimeArithmeticException if `this` or [other] instant is too large to fit in [LocalDateTime]. * @sample kotlinx.datetime.test.samples.InstantSamples.periodUntil */ -public expect fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateTimePeriod +public fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateTimePeriod { + val initialOffset = offsetIn(timeZone) + val initialLdt = toLocalDateTimeFailing(initialOffset) + val otherLdt = other.toLocalDateTimeFailing(other.offsetIn(timeZone)) + + val months = initialLdt.until(otherLdt, DateTimeUnit.MONTH) // `until` on dates never fails + val unresolvedLdtWithMonths = initialLdt.plus(months, DateTimeUnit.MONTH) + // won't throw: thisLdt + months <= otherLdt, which is known to be valid + val instantWithMonths = localDateTimeToInstant(unresolvedLdtWithMonths, timeZone, preferred = initialOffset) + val offsetWithMonths = instantWithMonths.offsetIn(timeZone) + val ldtWithMonths = instantWithMonths.toLocalDateTime(offsetWithMonths) + val days = ldtWithMonths.until(otherLdt, DateTimeUnit.DAY) // `until` on dates never fails + val unresolvedLdtWithDays = ldtWithMonths.plus(days, DateTimeUnit.DAY) + val newInstant = localDateTimeToInstant(unresolvedLdtWithDays, timeZone, preferred = initialOffset) + // won't throw: thisLdt + days <= otherLdt + val nanoseconds = newInstant.until(other, DateTimeUnit.NANOSECOND) // |otherLdt - thisLdt| < 24h + + return buildDateTimePeriod(months, days.toInt(), nanoseconds) +} /** * Returns the whole number of the specified date or time [units][unit] between `this` and [other] instants @@ -505,7 +553,15 @@ public expect fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateT * @throws DateTimeArithmeticException if `this` or [other] instant is too large to fit in [LocalDateTime]. * @sample kotlinx.datetime.test.samples.InstantSamples.untilAsDateTimeUnit */ -public expect fun Instant.until(other: Instant, unit: DateTimeUnit, timeZone: TimeZone): Long +public 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) + is DateTimeUnit.TimeBased -> { + check(timeZone); other.check(timeZone) + until(other, unit) + } + } /** * Returns the whole number of the specified time [units][unit] between `this` and [other] instants. @@ -592,7 +648,8 @@ public fun Instant.minus(other: Instant, timeZone: TimeZone): DateTimePeriod = * @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. */ @Deprecated("Use the plus overload with an explicit number of units", ReplaceWith("this.plus(1, unit, timeZone)")) -public expect fun Instant.plus(unit: DateTimeUnit, timeZone: TimeZone): Instant +public fun Instant.plus(unit: DateTimeUnit, timeZone: TimeZone): Instant = + plus(1L, unit, timeZone) /** * Returns an instant that is the result of subtracting one [unit] from this instant @@ -641,7 +698,8 @@ public fun Instant.minus(unit: DateTimeUnit.TimeBased): Instant = * @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. * @sample kotlinx.datetime.test.samples.InstantSamples.plusDateTimeUnit */ -public expect fun Instant.plus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant +public fun Instant.plus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant = + plus(value.toLong(), unit, timeZone) /** * Returns an instant that is the result of subtracting the [value] number of the specified [unit] from this instant @@ -659,7 +717,8 @@ public expect fun Instant.plus(value: Int, unit: DateTimeUnit, timeZone: TimeZon * @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. * @sample kotlinx.datetime.test.samples.InstantSamples.minusDateTimeUnit */ -public expect fun Instant.minus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant +public fun Instant.minus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant = + plus(-value.toLong(), unit, timeZone) /** * Returns an instant that is the result of adding the [value] number of the specified [unit] to this instant. @@ -700,7 +759,21 @@ public fun Instant.minus(value: Int, unit: DateTimeUnit.TimeBased): Instant = * @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. * @sample kotlinx.datetime.test.samples.InstantSamples.plusDateTimeUnit */ -public expect fun Instant.plus(value: Long, unit: DateTimeUnit, timeZone: TimeZone): Instant +public fun Instant.plus(value: Long, unit: DateTimeUnit, timeZone: TimeZone): Instant = try { + when (unit) { + is DateTimeUnit.DateBased -> { + val initialOffset = offsetIn(timeZone) + val initialLdt = toLocalDateTimeFailing(initialOffset) + localDateTimeToInstant(initialLdt.plus(value, unit), timeZone, preferred = initialOffset) + } + is DateTimeUnit.TimeBased -> + check(timeZone).plus(value, unit).check(timeZone) + } +} catch (e: ArithmeticException) { + throw DateTimeArithmeticException("Arithmetic overflow when adding to an Instant", e) +} catch (e: IllegalArgumentException) { + throw DateTimeArithmeticException("Boundaries of Instant exceeded when adding a value", e) +} /** * Returns an instant that is the result of subtracting the [value] number of the specified [unit] from this instant @@ -732,7 +805,17 @@ public fun Instant.minus(value: Long, unit: DateTimeUnit, timeZone: TimeZone): I * * @sample kotlinx.datetime.test.samples.InstantSamples.plusTimeBasedUnit */ -public expect fun Instant.plus(value: Long, unit: DateTimeUnit.TimeBased): Instant +public fun Instant.plus(value: Long, unit: DateTimeUnit.TimeBased): Instant = + try { + multiplyAndDivide(value, unit.nanoseconds, NANOS_PER_ONE.toLong()).let { (seconds, nanoseconds) -> + plus(seconds, nanoseconds) + } + } catch (_: ArithmeticException) { + if (value > 0) Instant.MAX else Instant.MIN + } catch (_: IllegalArgumentException) { + if (value > 0) Instant.MAX else Instant.MIN + } + /** * Returns an instant that is the result of subtracting the [value] number of the specified [unit] from this instant. @@ -800,3 +883,43 @@ public fun Instant.format(format: DateTimeFormat, offset: Ut internal const val DISTANT_PAST_SECONDS = -3217862419201 internal const val DISTANT_FUTURE_SECONDS = 3093527980800 + +private fun Instant.toLocalDateTimeFailing(offset: UtcOffset): LocalDateTime = try { + toLocalDateTime(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)) +} + +private fun LocalDateTime.plus(value: Long, unit: DateTimeUnit.DateBased) = + date.plus(value, unit).atTime(time) + +private fun LocalDateTime.plus(value: Int, unit: DateTimeUnit.DateBased) = + date.plus(value, unit).atTime(time) + +/** + * @throws ArithmeticException if arithmetic overflow occurs + * @throws IllegalArgumentException if the boundaries of Instant are overflown + */ +internal expect fun Instant.plus(secondsToAdd: Long, nanosToAdd: Long): Instant + +// org.threeten.bp.LocalDateTime#until +internal fun LocalDateTime.until(other: LocalDateTime, unit: DateTimeUnit.DateBased): Long { + val otherDate = other.date + val delta = when { + otherDate > date && other.time < time -> -1 // addition won't throw: endDate - date >= 1 + otherDate < date && other.time > time -> 1 // addition won't throw: date - endDate >= 1 + else -> 0 + } + val endDate = otherDate.plus(delta, DateTimeUnit.DAY) + return when (unit) { + is DateTimeUnit.MonthBased -> date.until(endDate, DateTimeUnit.MONTH) / unit.months + is DateTimeUnit.DayBased -> date.until(endDate, DateTimeUnit.DAY) / unit.days + } +} diff --git a/core/common/src/TimeZone.kt b/core/common/src/TimeZone.kt index 808a8ad6..41985747 100644 --- a/core/common/src/TimeZone.kt +++ b/core/common/src/TimeZone.kt @@ -274,3 +274,7 @@ public expect fun LocalDateTime.toInstant(offset: UtcOffset): Instant * @sample kotlinx.datetime.test.samples.TimeZoneSamples.atStartOfDayIn */ public expect fun LocalDate.atStartOfDayIn(timeZone: TimeZone): Instant + +internal expect fun localDateTimeToInstant( + dateTime: LocalDateTime, timeZone: TimeZone, preferred: UtcOffset? = null +): Instant diff --git a/core/commonJs/src/internal/Platform.kt b/core/commonJs/src/internal/Platform.kt index c5ba347b..c00263e6 100644 --- a/core/commonJs/src/internal/Platform.kt +++ b/core/commonJs/src/internal/Platform.kt @@ -96,21 +96,21 @@ private object SystemTimeZone: TimeZone() { /* https://github.com/js-joda/js-joda/blob/8c1a7448db92ca014417346049fb64b55f7b1ac1/packages/core/src/LocalDate.js#L1404-L1416 + * https://github.com/js-joda/js-joda/blob/8c1a7448db92ca014417346049fb64b55f7b1ac1/packages/core/src/zone/SystemDefaultZoneRules.js#L69-L71 */ - override fun atStartOfDay(date: LocalDate): Instant = atZone(date.atTime(LocalTime.MIN)).toInstant() + override fun atStartOfDay(date: LocalDate): Instant = localDateTimeToInstant(date.atTime(LocalTime.MIN)) /* https://github.com/js-joda/js-joda/blob/8c1a7448db92ca014417346049fb64b55f7b1ac1/packages/core/src/zone/SystemDefaultZoneRules.js#L21-L24 */ override fun offsetAtImpl(instant: Instant): UtcOffset = UtcOffset(minutes = -Date(instant.toEpochMilliseconds().toDouble()).getTimezoneOffset().toInt()) /* https://github.com/js-joda/js-joda/blob/8c1a7448db92ca014417346049fb64b55f7b1ac1/packages/core/src/zone/SystemDefaultZoneRules.js#L49-L55 */ - override fun atZone(dateTime: LocalDateTime, preferred: UtcOffset?): ZonedDateTime { + override fun localDateTimeToInstant(dateTime: LocalDateTime, preferred: UtcOffset?): Instant { val epochMilli = dateTime.toInstant(UTC).toEpochMilliseconds() val offsetInMinutesBeforePossibleTransition = Date(epochMilli.toDouble()).getTimezoneOffset().toInt() val epochMilliSystemZone = epochMilli + offsetInMinutesBeforePossibleTransition * SECONDS_PER_MINUTE * MILLIS_PER_ONE val offsetInMinutesAfterPossibleTransition = Date(epochMilliSystemZone.toDouble()).getTimezoneOffset().toInt() val offset = UtcOffset(minutes = -offsetInMinutesAfterPossibleTransition) - return ZonedDateTime(dateTime, this, offset) + return dateTime.toInstant(offset) } override fun equals(other: Any?): Boolean = other === this diff --git a/core/commonKotlin/src/Instant.kt b/core/commonKotlin/src/Instant.kt index 439aa705..d6475d53 100644 --- a/core/commonKotlin/src/Instant.kt +++ b/core/commonKotlin/src/Instant.kt @@ -62,21 +62,6 @@ public actual class Instant internal constructor(public actual val epochSeconds: if (epochSeconds > 0) Long.MAX_VALUE else Long.MIN_VALUE } - // org.threeten.bp.Instant#plus(long, long) - /** - * @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 - } - 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) - } - public actual operator fun plus(duration: Duration): Instant = duration.toComponents { secondsToAdd, nanosecondsToAdd -> try { plus(secondsToAdd, nanosecondsToAdd.toLong()) @@ -129,7 +114,7 @@ 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 */ - private fun fromEpochSecondsThrowing(epochSeconds: Long, nanosecondAdjustment: Long): Instant { + internal fun fromEpochSecondsThrowing(epochSeconds: Long, nanosecondAdjustment: Long): Instant { val secs = safeAdd(epochSeconds, nanosecondAdjustment.floorDiv(NANOS_PER_ONE.toLong())) val nos = nanosecondAdjustment.mod(NANOS_PER_ONE.toLong()).toInt() return Instant(secs, nos) @@ -164,96 +149,6 @@ 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]. - * 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) -} - -public actual fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Instant = try { - with(period) { - val withDate = toZonedDateTimeFailing(timeZone) - .run { if (totalMonths != 0L) plus(totalMonths, DateTimeUnit.MONTH) else this } - .run { if (days != 0) plus(days.toLong(), DateTimeUnit.DAY) else this } - withDate.toInstant() - .run { if (totalNanoseconds != 0L) plus(0, totalNanoseconds).check(timeZone) else this } - }.check(timeZone) -} catch (e: ArithmeticException) { - throw DateTimeArithmeticException("Arithmetic overflow when adding CalendarPeriod to an Instant", e) -} catch (e: IllegalArgumentException) { - throw DateTimeArithmeticException("Boundaries of Instant exceeded when adding CalendarPeriod", e) -} - -@Deprecated("Use the plus overload with an explicit number of units", ReplaceWith("this.plus(1, unit, timeZone)")) -public actual fun Instant.plus(unit: DateTimeUnit, timeZone: TimeZone): Instant = - plus(1L, unit, timeZone) -public actual fun Instant.plus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant = - plus(value.toLong(), unit, timeZone) -public actual fun Instant.minus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant = - plus(-value.toLong(), unit, timeZone) -public actual fun Instant.plus(value: Long, unit: DateTimeUnit, timeZone: TimeZone): Instant = try { - when (unit) { - is DateTimeUnit.DateBased -> - toZonedDateTimeFailing(timeZone).plus(value, unit).toInstant() - is DateTimeUnit.TimeBased -> - check(timeZone).plus(value, unit).check(timeZone) - } -} catch (e: ArithmeticException) { - throw DateTimeArithmeticException("Arithmetic overflow when adding to an Instant", e) -} catch (e: IllegalArgumentException) { - throw DateTimeArithmeticException("Boundaries of Instant exceeded when adding a value", e) -} - -public actual fun Instant.plus(value: Long, unit: DateTimeUnit.TimeBased): Instant = - try { - multiplyAndDivide(value, unit.nanoseconds, NANOS_PER_ONE.toLong()).let { (seconds, nanoseconds) -> - plus(seconds, nanoseconds) - } - } catch (_: ArithmeticException) { - if (value > 0) Instant.MAX else Instant.MIN - } catch (_: IllegalArgumentException) { - if (value > 0) Instant.MAX else Instant.MIN - } - -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) // `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) // `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 - - return buildDateTimePeriod(months, days.toInt(), nanoseconds) -} - -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) - .toLong() - is DateTimeUnit.TimeBased -> { - check(timeZone); other.check(timeZone) - until(other, unit) - } - } - private val ISO_DATE_TIME_OFFSET_WITH_TRAILING_ZEROS = DateTimeComponents.Format { date(ISO_DATE) alternativeParsing({ @@ -277,3 +172,14 @@ private val ISO_DATE_TIME_OFFSET_WITH_TRAILING_ZEROS = DateTimeComponents.Format outputSecond = WhenToOutput.IF_NONZERO ) } + +// org.threeten.bp.Instant#plus(long, long) +internal actual fun Instant.plus(secondsToAdd: Long, nanosToAdd: Long): Instant { + if ((secondsToAdd or nanosToAdd) == 0L) { + return this + } + 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 Instant.fromEpochSecondsThrowing(newEpochSeconds, nanoAdjustment) +} diff --git a/core/commonKotlin/src/LocalDateTime.kt b/core/commonKotlin/src/LocalDateTime.kt index e6b7730b..2d6fb392 100644 --- a/core/commonKotlin/src/LocalDateTime.kt +++ b/core/commonKotlin/src/LocalDateTime.kt @@ -81,28 +81,6 @@ public actual constructor(public actual val date: LocalDate, public actual val t } } -// org.threeten.bp.LocalDateTime#until -internal fun LocalDateTime.until(other: LocalDateTime, unit: DateTimeUnit.DateBased): Long { - var endDate: LocalDate = other.date - if (endDate > date && other.time < time) { - endDate = endDate.plusDays(-1) // won't throw: endDate - date >= 1 - } else if (endDate < date && other.time > time) { - endDate = endDate.plusDays(1) // won't throw: date - endDate >= 1 - } - return when (unit) { - is DateTimeUnit.MonthBased -> date.until(endDate, DateTimeUnit.MONTH) / unit.months - is DateTimeUnit.DayBased -> date.until(endDate, DateTimeUnit.DAY) / unit.days - } -} - -// org.threeten.bp.LocalDateTime#until -/** @throws ArithmeticException on arithmetic overflow. */ -internal fun LocalDateTime.until(other: LocalDateTime, unit: DateTimeUnit.TimeBased): Long { - val daysUntil = date.daysUntil(other.date) - val timeUntil: Long = other.time.toNanosecondOfDay() - time.toNanosecondOfDay() - return multiplyAddAndDivide(daysUntil.toLong(), NANOS_PER_DAY, timeUntil, unit.nanoseconds) -} - // org.threeten.bp.LocalDateTime#plusWithOverflow /** * @throws IllegalArgumentException if the result exceeds the boundaries diff --git a/core/commonKotlin/src/TimeZone.kt b/core/commonKotlin/src/TimeZone.kt index 4aa6aa5e..418d7df9 100644 --- a/core/commonKotlin/src/TimeZone.kt +++ b/core/commonKotlin/src/TimeZone.kt @@ -91,10 +91,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): ZonedDateTime = + internal open fun localDateTimeToInstant(dateTime: LocalDateTime, preferred: UtcOffset? = null): Instant = error("Should be overridden") actual override fun equals(other: Any?): Boolean = @@ -118,11 +115,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?): ZonedDateTime = - ZonedDateTime(dateTime, this, 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) } @@ -178,3 +174,7 @@ private val lenientOffsetFormat = UtcOffsetFormat.build { ) } } + +internal actual fun localDateTimeToInstant( + dateTime: LocalDateTime, timeZone: TimeZone, preferred: UtcOffset? +): Instant = timeZone.localDateTimeToInstant(dateTime, preferred) diff --git a/core/commonKotlin/src/ZonedDateTime.kt b/core/commonKotlin/src/ZonedDateTime.kt deleted file mode 100644 index effd5acd..00000000 --- a/core/commonKotlin/src/ZonedDateTime.kt +++ /dev/null @@ -1,75 +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: Long, unit: DateTimeUnit.DateBased): ZonedDateTime = - dateTime.date.plus(value, unit).atTime(dateTime.time).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) - // 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/commonKotlin/src/internal/RegionTimeZone.kt b/core/commonKotlin/src/internal/RegionTimeZone.kt index d4e7bc6b..a42ee8fa 100644 --- a/core/commonKotlin/src/internal/RegionTimeZone.kt +++ b/core/commonKotlin/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/commonKotlin/test/ThreeTenBpLocalDateTimeTest.kt b/core/commonKotlin/test/ThreeTenBpLocalDateTimeTest.kt index d1b7e097..4b5a6d08 100644 --- a/core/commonKotlin/test/ThreeTenBpLocalDateTimeTest.kt +++ b/core/commonKotlin/test/ThreeTenBpLocalDateTimeTest.kt @@ -55,47 +55,4 @@ class ThreeTenBpLocalDateTimeTest { assertEquals(sec, t.second) } } - - @Test - fun until() { - val data = arrayOf( - Pair(Pair("2012-06-15T00:00", "2012-06-15T00:00"), Pair(DateTimeUnit.NANOSECOND, 0L)), - Pair(Pair("2012-06-15T00:00", "2012-06-15T00:00"), Pair(DateTimeUnit.SECOND, 0L)), - Pair(Pair("2012-06-15T00:00", "2012-06-15T00:00"), Pair(DateTimeUnit.MINUTE, 0L)), - Pair(Pair("2012-06-15T00:00", "2012-06-15T00:00"), Pair(DateTimeUnit.HOUR, 0L)), - Pair(Pair("2012-06-15T00:00", "2012-06-15T00:00:01"), Pair(DateTimeUnit.NANOSECOND, 1000000000L)), - Pair(Pair("2012-06-15T00:00", "2012-06-15T00:00:01"), Pair(DateTimeUnit.SECOND, 1L)), - Pair(Pair("2012-06-15T00:00", "2012-06-15T00:00:01"), Pair(DateTimeUnit.MINUTE, 0L)), - Pair(Pair("2012-06-15T00:00", "2012-06-15T00:00:01"), Pair(DateTimeUnit.HOUR, 0L)), - Pair(Pair("2012-06-15T00:00", "2012-06-15T00:01"), Pair(DateTimeUnit.NANOSECOND, 60000000000L)), - Pair(Pair("2012-06-15T00:00", "2012-06-15T00:01"), Pair(DateTimeUnit.SECOND, 60L)), - Pair(Pair("2012-06-15T00:00", "2012-06-15T00:01"), Pair(DateTimeUnit.MINUTE, 1L)), - Pair(Pair("2012-06-15T00:00", "2012-06-15T00:01"), Pair(DateTimeUnit.HOUR, 0L)), - Pair(Pair("2012-06-15T12:30:40.500", "2012-06-15T12:30:39.499"), Pair(DateTimeUnit.SECOND, -1L)), - Pair(Pair("2012-06-15T12:30:40.500", "2012-06-15T12:30:39.500"), Pair(DateTimeUnit.SECOND, -1L)), - Pair(Pair("2012-06-15T12:30:40.500", "2012-06-15T12:30:39.501"), Pair(DateTimeUnit.SECOND, 0L)), - Pair(Pair("2012-06-15T12:30:40.500", "2012-06-15T12:30:40.499"), Pair(DateTimeUnit.SECOND, 0L)), - Pair(Pair("2012-06-15T12:30:40.500", "2012-06-15T12:30:40.500"), Pair(DateTimeUnit.SECOND, 0L)), - Pair(Pair("2012-06-15T12:30:40.500", "2012-06-15T12:30:40.501"), Pair(DateTimeUnit.SECOND, 0L)), - Pair(Pair("2012-06-15T12:30:40.500", "2012-06-15T12:30:41.499"), Pair(DateTimeUnit.SECOND, 0L)), - Pair(Pair("2012-06-15T12:30:40.500", "2012-06-15T12:30:41.500"), Pair(DateTimeUnit.SECOND, 1L)), - Pair(Pair("2012-06-15T12:30:40.500", "2012-06-15T12:30:41.501"), Pair(DateTimeUnit.SECOND, 1L)), - Pair(Pair("2012-06-15T12:30:40.500", "2012-06-16T12:30:39.499"), Pair(DateTimeUnit.SECOND, 86400L - 2)), - Pair(Pair("2012-06-15T12:30:40.500", "2012-06-16T12:30:39.500"), Pair(DateTimeUnit.SECOND, 86400L - 1)), - Pair(Pair("2012-06-15T12:30:40.500", "2012-06-16T12:30:39.501"), Pair(DateTimeUnit.SECOND, 86400L - 1)), - Pair(Pair("2012-06-15T12:30:40.500", "2012-06-16T12:30:40.499"), Pair(DateTimeUnit.SECOND, 86400L - 1)), - Pair(Pair("2012-06-15T12:30:40.500", "2012-06-16T12:30:40.500"), Pair(DateTimeUnit.SECOND, 86400L + 0)), - Pair(Pair("2012-06-15T12:30:40.500", "2012-06-16T12:30:40.501"), Pair(DateTimeUnit.SECOND, 86400L + 0)), - Pair(Pair("2012-06-15T12:30:40.500", "2012-06-16T12:30:41.499"), Pair(DateTimeUnit.SECOND, 86400L + 0)), - Pair(Pair("2012-06-15T12:30:40.500", "2012-06-16T12:30:41.500"), Pair(DateTimeUnit.SECOND, 86400L + 1)), - Pair(Pair("2012-06-15T12:30:40.500", "2012-06-16T12:30:41.501"), Pair(DateTimeUnit.SECOND, 86400L + 1))) - for ((values, interval) in data) { - val (v1, v2) = values - val dt1 = v1.toLocalDateTime() - val dt2 = v2.toLocalDateTime() - val (unit, length) = interval - assertEquals(length, dt1.until(dt2, unit)) - assertEquals(-length, dt2.until(dt1, unit)) - } - } } diff --git a/core/commonKotlin/test/ThreeTenBpTimeZoneTest.kt b/core/commonKotlin/test/ThreeTenBpTimeZoneTest.kt index 95ee1cf8..07b08d64 100644 --- a/core/commonKotlin/test/ThreeTenBpTimeZoneTest.kt +++ b/core/commonKotlin/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)) } } diff --git a/core/jvm/src/Instant.kt b/core/jvm/src/Instant.kt index 53c67044..a34c0043 100644 --- a/core/jvm/src/Instant.kt +++ b/core/jvm/src/Instant.kt @@ -100,83 +100,8 @@ public actual class Instant internal constructor(internal val value: jtInstant) } } -private fun Instant.atZone(zone: TimeZone): java.time.ZonedDateTime = try { - value.atZone(zone.zoneId) +internal actual fun Instant.plus(secondsToAdd: Long, nanosToAdd: Long): Instant = try { + Instant(this.value.plusSeconds(secondsToAdd).plusNanos(nanosToAdd)) } catch (e: DateTimeException) { - throw DateTimeArithmeticException(e) -} - -public actual fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Instant { - try { - val thisZdt = atZone(timeZone) - return with(period) { - thisZdt - .run { if (totalMonths != 0L) plusMonths(totalMonths) else this } - .run { if (days != 0) plusDays(days.toLong()) else this } - .run { if (totalNanoseconds != 0L) plusNanos(totalNanoseconds) else this } - }.toInstant().let(::Instant) - } catch (e: DateTimeException) { - throw DateTimeArithmeticException(e) - } -} - -@Deprecated("Use the plus overload with an explicit number of units", ReplaceWith("this.plus(1, unit, timeZone)")) -public actual fun Instant.plus(unit: DateTimeUnit, timeZone: TimeZone): Instant = - plus(1L, unit, timeZone) - -public actual fun Instant.plus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant = - plus(value.toLong(), unit, timeZone) - -public actual fun Instant.minus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant = - plus(-value.toLong(), unit, timeZone) - -public actual fun Instant.plus(value: Long, unit: DateTimeUnit, timeZone: TimeZone): Instant = - try { - val thisZdt = atZone(timeZone) - when (unit) { - is DateTimeUnit.TimeBased -> - plus(value, unit).value.also { it.atZone(timeZone.zoneId) } - is DateTimeUnit.DayBased -> - thisZdt.plusDays(safeMultiply(value, unit.days.toLong())).toInstant() - is DateTimeUnit.MonthBased -> - thisZdt.plusMonths(safeMultiply(value, unit.months.toLong())).toInstant() - }.let(::Instant) - } catch (e: Exception) { - if (e !is DateTimeException && e !is ArithmeticException) throw e - throw DateTimeArithmeticException("Instant $this cannot be represented as local date when adding $value $unit to it", e) - } - -public actual fun Instant.plus(value: Long, unit: DateTimeUnit.TimeBased): Instant = - try { - multiplyAndDivide(value, unit.nanoseconds, NANOS_PER_ONE.toLong()).let { (d, r) -> - Instant(this.value.plusSeconds(d).plusNanos(r)) - } - } catch (e: Exception) { - if (e !is DateTimeException && e !is ArithmeticException) throw e - if (value > 0) Instant.MAX else Instant.MIN - } - -public actual fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateTimePeriod { - var thisZdt = this.atZone(timeZone) - val otherZdt = other.atZone(timeZone) - - val months = thisZdt.until(otherZdt, ChronoUnit.MONTHS); thisZdt = thisZdt.plusMonths(months) - val days = thisZdt.until(otherZdt, ChronoUnit.DAYS); thisZdt = thisZdt.plusDays(days) - val nanoseconds = thisZdt.until(otherZdt, ChronoUnit.NANOS) - - return buildDateTimePeriod(months, days.toInt(), nanoseconds) -} - -public actual fun Instant.until(other: Instant, unit: DateTimeUnit, timeZone: TimeZone): Long = try { - val thisZdt = this.atZone(timeZone) - val otherZdt = other.atZone(timeZone) - when(unit) { - is DateTimeUnit.TimeBased -> until(other, unit) - is DateTimeUnit.DayBased -> thisZdt.until(otherZdt, ChronoUnit.DAYS) / unit.days - is DateTimeUnit.MonthBased -> thisZdt.until(otherZdt, ChronoUnit.MONTHS) / unit.months - } -} catch (e: DateTimeException) { - throw DateTimeArithmeticException(e) -} catch (e: ArithmeticException) { - if (this.value < other.value) Long.MAX_VALUE else Long.MIN_VALUE + throw IllegalArgumentException(e) } diff --git a/core/jvm/src/TimeZoneJvm.kt b/core/jvm/src/TimeZoneJvm.kt index cd90993e..1ea3b2c4 100644 --- a/core/jvm/src/TimeZoneJvm.kt +++ b/core/jvm/src/TimeZoneJvm.kt @@ -97,3 +97,9 @@ public actual fun LocalDateTime.toInstant(offset: UtcOffset): Instant = public actual fun LocalDate.atStartOfDayIn(timeZone: TimeZone): Instant = this.value.atStartOfDay(timeZone.zoneId).toInstant().let(::Instant) + +internal actual fun localDateTimeToInstant( + dateTime: LocalDateTime, timeZone: TimeZone, preferred: UtcOffset? +): Instant = java.time.ZonedDateTime.ofLocal( + dateTime.value, timeZone.zoneId, preferred?.zoneOffset +).toInstant().let(::Instant)