Skip to content

Commit 8567384

Browse files
committed
Move the Instant arithmetics to common code
1 parent 488c1f3 commit 8567384

File tree

8 files changed

+159
-282
lines changed

8 files changed

+159
-282
lines changed

core/common/src/Instant.kt

+131-8
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,37 @@ public fun String.toInstant(): Instant = Instant.parse(this)
447447
* [LocalDateTime].
448448
* @sample kotlinx.datetime.test.samples.InstantSamples.plusPeriod
449449
*/
450-
public expect fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Instant
450+
public fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Instant = try {
451+
with(period) {
452+
val initialOffset = offsetIn(timeZone)
453+
val initialLdt = toLocalDateTimeFailing(initialOffset)
454+
val instantAfterMonths: Instant
455+
val offsetAfterMonths: UtcOffset
456+
val ldtAfterMonths: LocalDateTime
457+
if (totalMonths != 0L) {
458+
val unresolvedLdtWithMonths = initialLdt.plus(totalMonths, DateTimeUnit.MONTH)
459+
instantAfterMonths = localDateTimeToInstant(unresolvedLdtWithMonths, timeZone, preferred = initialOffset)
460+
offsetAfterMonths = instantAfterMonths.offsetIn(timeZone)
461+
ldtAfterMonths = instantAfterMonths.toLocalDateTime(offsetAfterMonths)
462+
} else {
463+
instantAfterMonths = this@plus
464+
offsetAfterMonths = initialOffset
465+
ldtAfterMonths = initialLdt
466+
}
467+
val instantAfterMonthsAndDays = if (days != 0) {
468+
val unresolvedLdtWithDays = ldtAfterMonths.plus(days, DateTimeUnit.DAY)
469+
localDateTimeToInstant(unresolvedLdtWithDays, timeZone, preferred = offsetAfterMonths)
470+
} else {
471+
instantAfterMonths
472+
}
473+
instantAfterMonthsAndDays
474+
.run { if (totalNanoseconds != 0L) plus(0, totalNanoseconds).check(timeZone) else this }
475+
}.check(timeZone)
476+
} catch (e: ArithmeticException) {
477+
throw DateTimeArithmeticException("Arithmetic overflow when adding CalendarPeriod to an Instant", e)
478+
} catch (e: IllegalArgumentException) {
479+
throw DateTimeArithmeticException("Boundaries of Instant exceeded when adding CalendarPeriod", e)
480+
}
451481

452482
/**
453483
* 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 =
489519
* @throws DateTimeArithmeticException if `this` or [other] instant is too large to fit in [LocalDateTime].
490520
* @sample kotlinx.datetime.test.samples.InstantSamples.periodUntil
491521
*/
492-
public expect fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateTimePeriod
522+
public fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateTimePeriod {
523+
val initialOffset = offsetIn(timeZone)
524+
val initialLdt = toLocalDateTimeFailing(initialOffset)
525+
val otherLdt = other.toLocalDateTimeFailing(other.offsetIn(timeZone))
526+
527+
val months = initialLdt.until(otherLdt, DateTimeUnit.MONTH) // `until` on dates never fails
528+
val unresolvedLdtWithMonths = initialLdt.plus(months, DateTimeUnit.MONTH)
529+
// won't throw: thisLdt + months <= otherLdt, which is known to be valid
530+
val instantWithMonths = localDateTimeToInstant(unresolvedLdtWithMonths, timeZone, preferred = initialOffset)
531+
val offsetWithMonths = instantWithMonths.offsetIn(timeZone)
532+
val ldtWithMonths = instantWithMonths.toLocalDateTime(offsetWithMonths)
533+
val days = ldtWithMonths.until(otherLdt, DateTimeUnit.DAY) // `until` on dates never fails
534+
val unresolvedLdtWithDays = ldtWithMonths.plus(days, DateTimeUnit.DAY)
535+
val newInstant = localDateTimeToInstant(unresolvedLdtWithDays, timeZone, preferred = initialOffset)
536+
// won't throw: thisLdt + days <= otherLdt
537+
val nanoseconds = newInstant.until(other, DateTimeUnit.NANOSECOND) // |otherLdt - thisLdt| < 24h
538+
539+
return buildDateTimePeriod(months, days.toInt(), nanoseconds)
540+
}
493541

494542
/**
495543
* 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
505553
* @throws DateTimeArithmeticException if `this` or [other] instant is too large to fit in [LocalDateTime].
506554
* @sample kotlinx.datetime.test.samples.InstantSamples.untilAsDateTimeUnit
507555
*/
508-
public expect fun Instant.until(other: Instant, unit: DateTimeUnit, timeZone: TimeZone): Long
556+
public fun Instant.until(other: Instant, unit: DateTimeUnit, timeZone: TimeZone): Long =
557+
when (unit) {
558+
is DateTimeUnit.DateBased ->
559+
toLocalDateTimeFailing(offsetIn(timeZone)).until(other.toLocalDateTimeFailing(other.offsetIn(timeZone)), unit)
560+
is DateTimeUnit.TimeBased -> {
561+
check(timeZone); other.check(timeZone)
562+
until(other, unit)
563+
}
564+
}
509565

510566
/**
511567
* 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 =
592648
* @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime].
593649
*/
594650
@Deprecated("Use the plus overload with an explicit number of units", ReplaceWith("this.plus(1, unit, timeZone)"))
595-
public expect fun Instant.plus(unit: DateTimeUnit, timeZone: TimeZone): Instant
651+
public fun Instant.plus(unit: DateTimeUnit, timeZone: TimeZone): Instant =
652+
plus(1L, unit, timeZone)
596653

597654
/**
598655
* 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 =
641698
* @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime].
642699
* @sample kotlinx.datetime.test.samples.InstantSamples.plusDateTimeUnit
643700
*/
644-
public expect fun Instant.plus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant
701+
public fun Instant.plus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant =
702+
plus(value.toLong(), unit, timeZone)
645703

646704
/**
647705
* 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
659717
* @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime].
660718
* @sample kotlinx.datetime.test.samples.InstantSamples.minusDateTimeUnit
661719
*/
662-
public expect fun Instant.minus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant
720+
public fun Instant.minus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant =
721+
plus(-value.toLong(), unit, timeZone)
663722

664723
/**
665724
* 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 =
700759
* @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime].
701760
* @sample kotlinx.datetime.test.samples.InstantSamples.plusDateTimeUnit
702761
*/
703-
public expect fun Instant.plus(value: Long, unit: DateTimeUnit, timeZone: TimeZone): Instant
762+
public fun Instant.plus(value: Long, unit: DateTimeUnit, timeZone: TimeZone): Instant = try {
763+
when (unit) {
764+
is DateTimeUnit.DateBased -> {
765+
val initialOffset = offsetIn(timeZone)
766+
val initialLdt = toLocalDateTimeFailing(initialOffset)
767+
localDateTimeToInstant(initialLdt.plus(value, unit), timeZone, preferred = initialOffset)
768+
}
769+
is DateTimeUnit.TimeBased ->
770+
check(timeZone).plus(value, unit).check(timeZone)
771+
}
772+
} catch (e: ArithmeticException) {
773+
throw DateTimeArithmeticException("Arithmetic overflow when adding to an Instant", e)
774+
} catch (e: IllegalArgumentException) {
775+
throw DateTimeArithmeticException("Boundaries of Instant exceeded when adding a value", e)
776+
}
704777

705778
/**
706779
* 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
732805
*
733806
* @sample kotlinx.datetime.test.samples.InstantSamples.plusTimeBasedUnit
734807
*/
735-
public expect fun Instant.plus(value: Long, unit: DateTimeUnit.TimeBased): Instant
808+
public fun Instant.plus(value: Long, unit: DateTimeUnit.TimeBased): Instant =
809+
try {
810+
multiplyAndDivide(value, unit.nanoseconds, NANOS_PER_ONE.toLong()).let { (seconds, nanoseconds) ->
811+
plus(seconds, nanoseconds)
812+
}
813+
} catch (_: ArithmeticException) {
814+
if (value > 0) Instant.MAX else Instant.MIN
815+
} catch (_: IllegalArgumentException) {
816+
if (value > 0) Instant.MAX else Instant.MIN
817+
}
818+
736819

737820
/**
738821
* 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<DateTimeComponents>, offset: Ut
800883

801884
internal const val DISTANT_PAST_SECONDS = -3217862419201
802885
internal const val DISTANT_FUTURE_SECONDS = 3093527980800
886+
887+
private fun Instant.toLocalDateTimeFailing(offset: UtcOffset): LocalDateTime = try {
888+
toLocalDateTime(offset)
889+
} catch (e: IllegalArgumentException) {
890+
throw DateTimeArithmeticException("Can not convert instant $this to LocalDateTime to perform computations", e)
891+
}
892+
893+
/** Check that [Instant] fits in [LocalDateTime].
894+
* This is done on the results of computations for consistency with other platforms.
895+
*/
896+
private fun Instant.check(zone: TimeZone): Instant = this@check.also {
897+
toLocalDateTimeFailing(offsetIn(zone))
898+
}
899+
900+
private fun LocalDateTime.plus(value: Long, unit: DateTimeUnit.DateBased) =
901+
date.plus(value, unit).atTime(time)
902+
903+
private fun LocalDateTime.plus(value: Int, unit: DateTimeUnit.DateBased) =
904+
date.plus(value, unit).atTime(time)
905+
906+
/**
907+
* @throws ArithmeticException if arithmetic overflow occurs
908+
* @throws IllegalArgumentException if the boundaries of Instant are overflown
909+
*/
910+
internal expect fun Instant.plus(secondsToAdd: Long, nanosToAdd: Long): Instant
911+
912+
// org.threeten.bp.LocalDateTime#until
913+
internal fun LocalDateTime.until(other: LocalDateTime, unit: DateTimeUnit.DateBased): Long {
914+
val otherDate = other.date
915+
val delta = when {
916+
otherDate > date && other.time < time -> -1 // addition won't throw: endDate - date >= 1
917+
otherDate < date && other.time > time -> 1 // addition won't throw: date - endDate >= 1
918+
else -> 0
919+
}
920+
val endDate = otherDate.plus(delta, DateTimeUnit.DAY)
921+
return when (unit) {
922+
is DateTimeUnit.MonthBased -> date.until(endDate, DateTimeUnit.MONTH) / unit.months
923+
is DateTimeUnit.DayBased -> date.until(endDate, DateTimeUnit.DAY) / unit.days
924+
}
925+
}

core/common/src/TimeZone.kt

+4
Original file line numberDiff line numberDiff line change
@@ -274,3 +274,7 @@ public expect fun LocalDateTime.toInstant(offset: UtcOffset): Instant
274274
* @sample kotlinx.datetime.test.samples.TimeZoneSamples.atStartOfDayIn
275275
*/
276276
public expect fun LocalDate.atStartOfDayIn(timeZone: TimeZone): Instant
277+
278+
internal expect fun localDateTimeToInstant(
279+
dateTime: LocalDateTime, timeZone: TimeZone, preferred: UtcOffset? = null
280+
): Instant

core/commonKotlin/src/Instant.kt

+11-131
Original file line numberDiff line numberDiff line change
@@ -62,21 +62,6 @@ public actual class Instant internal constructor(public actual val epochSeconds:
6262
if (epochSeconds > 0) Long.MAX_VALUE else Long.MIN_VALUE
6363
}
6464

65-
// org.threeten.bp.Instant#plus(long, long)
66-
/**
67-
* @throws ArithmeticException if arithmetic overflow occurs
68-
* @throws IllegalArgumentException if the boundaries of Instant are overflown
69-
*/
70-
internal fun plus(secondsToAdd: Long, nanosToAdd: Long): Instant {
71-
if ((secondsToAdd or nanosToAdd) == 0L) {
72-
return this
73-
}
74-
val newEpochSeconds: Long = safeAdd(safeAdd(epochSeconds, secondsToAdd), (nanosToAdd / NANOS_PER_ONE))
75-
val newNanosToAdd = nanosToAdd % NANOS_PER_ONE
76-
val nanoAdjustment = (nanosecondsOfSecond + newNanosToAdd) // safe int+NANOS_PER_ONE
77-
return fromEpochSecondsThrowing(newEpochSeconds, nanoAdjustment)
78-
}
79-
8065
public actual operator fun plus(duration: Duration): Instant = duration.toComponents { secondsToAdd, nanosecondsToAdd ->
8166
try {
8267
plus(secondsToAdd, nanosecondsToAdd.toLong())
@@ -133,7 +118,7 @@ public actual class Instant internal constructor(public actual val epochSeconds:
133118
* @throws ArithmeticException if arithmetic overflow occurs
134119
* @throws IllegalArgumentException if the boundaries of Instant are overflown
135120
*/
136-
private fun fromEpochSecondsThrowing(epochSeconds: Long, nanosecondAdjustment: Long): Instant {
121+
internal fun fromEpochSecondsThrowing(epochSeconds: Long, nanosecondAdjustment: Long): Instant {
137122
val secs = safeAdd(epochSeconds, nanosecondAdjustment.floorDiv(NANOS_PER_ONE.toLong()))
138123
val nos = nanosecondAdjustment.mod(NANOS_PER_ONE.toLong()).toInt()
139124
return Instant(secs, nos)
@@ -168,116 +153,6 @@ public actual class Instant internal constructor(public actual val epochSeconds:
168153

169154
}
170155

171-
private fun Instant.toLocalDateTimeFailing(offset: UtcOffset): LocalDateTime = try {
172-
toLocalDateTimeImpl(offset)
173-
} catch (e: IllegalArgumentException) {
174-
throw DateTimeArithmeticException("Can not convert instant $this to LocalDateTime to perform computations", e)
175-
}
176-
177-
/** Check that [Instant] fits in [LocalDateTime].
178-
* This is done on the results of computations for consistency with other platforms.
179-
*/
180-
private fun Instant.check(zone: TimeZone): Instant = this@check.also {
181-
toLocalDateTimeFailing(offsetIn(zone))
182-
}
183-
184-
public actual fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Instant = try {
185-
with(period) {
186-
val initialOffset = offsetIn(timeZone)
187-
val initialLdt = toLocalDateTimeFailing(initialOffset)
188-
val instantAfterMonths: Instant
189-
val offsetAfterMonths: UtcOffset
190-
val ldtAfterMonths: LocalDateTime
191-
if (totalMonths != 0L) {
192-
val unresolvedLdtWithMonths = initialLdt.plus(totalMonths, DateTimeUnit.MONTH)
193-
instantAfterMonths = timeZone.localDateTimeToInstant(unresolvedLdtWithMonths, initialOffset)
194-
offsetAfterMonths = instantAfterMonths.offsetIn(timeZone)
195-
ldtAfterMonths = instantAfterMonths.toLocalDateTimeFailing(offsetAfterMonths)
196-
} else {
197-
instantAfterMonths = this@plus
198-
offsetAfterMonths = initialOffset
199-
ldtAfterMonths = initialLdt
200-
}
201-
val instantAfterMonthsAndDays = if (days != 0) {
202-
val unresolvedLdtWithDays = ldtAfterMonths.plus(days, DateTimeUnit.DAY)
203-
timeZone.localDateTimeToInstant(unresolvedLdtWithDays, offsetAfterMonths)
204-
} else {
205-
instantAfterMonths
206-
}
207-
instantAfterMonthsAndDays
208-
.run { if (totalNanoseconds != 0L) plus(0, totalNanoseconds).check(timeZone) else this }
209-
}.check(timeZone)
210-
} catch (e: ArithmeticException) {
211-
throw DateTimeArithmeticException("Arithmetic overflow when adding CalendarPeriod to an Instant", e)
212-
} catch (e: IllegalArgumentException) {
213-
throw DateTimeArithmeticException("Boundaries of Instant exceeded when adding CalendarPeriod", e)
214-
}
215-
216-
@Deprecated("Use the plus overload with an explicit number of units", ReplaceWith("this.plus(1, unit, timeZone)"))
217-
public actual fun Instant.plus(unit: DateTimeUnit, timeZone: TimeZone): Instant =
218-
plus(1L, unit, timeZone)
219-
public actual fun Instant.plus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant =
220-
plus(value.toLong(), unit, timeZone)
221-
public actual fun Instant.minus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant =
222-
plus(-value.toLong(), unit, timeZone)
223-
public actual fun Instant.plus(value: Long, unit: DateTimeUnit, timeZone: TimeZone): Instant = try {
224-
when (unit) {
225-
is DateTimeUnit.DateBased -> {
226-
val initialOffset = offsetIn(timeZone)
227-
val initialLdt = toLocalDateTimeFailing(initialOffset)
228-
timeZone.localDateTimeToInstant(initialLdt.plus(value, unit), preferred = initialOffset)
229-
}
230-
is DateTimeUnit.TimeBased ->
231-
check(timeZone).plus(value, unit).check(timeZone)
232-
}
233-
} catch (e: ArithmeticException) {
234-
throw DateTimeArithmeticException("Arithmetic overflow when adding to an Instant", e)
235-
} catch (e: IllegalArgumentException) {
236-
throw DateTimeArithmeticException("Boundaries of Instant exceeded when adding a value", e)
237-
}
238-
239-
public actual fun Instant.plus(value: Long, unit: DateTimeUnit.TimeBased): Instant =
240-
try {
241-
multiplyAndDivide(value, unit.nanoseconds, NANOS_PER_ONE.toLong()).let { (seconds, nanoseconds) ->
242-
plus(seconds, nanoseconds)
243-
}
244-
} catch (_: ArithmeticException) {
245-
if (value > 0) Instant.MAX else Instant.MIN
246-
} catch (_: IllegalArgumentException) {
247-
if (value > 0) Instant.MAX else Instant.MIN
248-
}
249-
250-
public actual fun Instant.periodUntil(other: Instant, timeZone: TimeZone): DateTimePeriod {
251-
val initialOffset = offsetIn(timeZone)
252-
val initialLdt = toLocalDateTimeFailing(initialOffset)
253-
val otherLdt = other.toLocalDateTimeFailing(other.offsetIn(timeZone))
254-
255-
val months = initialLdt.until(otherLdt, DateTimeUnit.MONTH) // `until` on dates never fails
256-
val unresolvedLdtWithMonths = initialLdt.plus(months, DateTimeUnit.MONTH)
257-
// won't throw: thisLdt + months <= otherLdt, which is known to be valid
258-
val instantWithMonths = timeZone.localDateTimeToInstant(unresolvedLdtWithMonths, preferred = initialOffset)
259-
val offsetWithMonths = instantWithMonths.offsetIn(timeZone)
260-
val ldtWithMonths = instantWithMonths.toLocalDateTimeFailing(offsetWithMonths)
261-
val days = ldtWithMonths.until(otherLdt, DateTimeUnit.DAY) // `until` on dates never fails
262-
val unresolvedLdtWithDays = ldtWithMonths.plus(days, DateTimeUnit.DAY)
263-
val newInstant = timeZone.localDateTimeToInstant(unresolvedLdtWithDays, preferred = initialOffset)
264-
// won't throw: thisLdt + days <= otherLdt
265-
val nanoseconds = newInstant.until(other, DateTimeUnit.NANOSECOND) // |otherLdt - thisLdt| < 24h
266-
267-
return buildDateTimePeriod(months, days.toInt(), nanoseconds)
268-
}
269-
270-
public actual fun Instant.until(other: Instant, unit: DateTimeUnit, timeZone: TimeZone): Long =
271-
when (unit) {
272-
is DateTimeUnit.DateBased ->
273-
toLocalDateTimeFailing(offsetIn(timeZone)).until(other.toLocalDateTimeFailing(other.offsetIn(timeZone)), unit)
274-
.toLong()
275-
is DateTimeUnit.TimeBased -> {
276-
check(timeZone); other.check(timeZone)
277-
until(other, unit)
278-
}
279-
}
280-
281156
private val ISO_DATE_TIME_OFFSET_WITH_TRAILING_ZEROS = DateTimeComponents.Format {
282157
date(ISO_DATE)
283158
alternativeParsing({
@@ -302,8 +177,13 @@ private val ISO_DATE_TIME_OFFSET_WITH_TRAILING_ZEROS = DateTimeComponents.Format
302177
)
303178
}
304179

305-
private fun LocalDateTime.plus(value: Long, unit: DateTimeUnit.DateBased) =
306-
date.plus(value, unit).atTime(time)
307-
308-
private fun LocalDateTime.plus(value: Int, unit: DateTimeUnit.DateBased) =
309-
date.plus(value, unit).atTime(time)
180+
// org.threeten.bp.Instant#plus(long, long)
181+
internal actual fun Instant.plus(secondsToAdd: Long, nanosToAdd: Long): Instant {
182+
if ((secondsToAdd or nanosToAdd) == 0L) {
183+
return this
184+
}
185+
val newEpochSeconds: Long = safeAdd(safeAdd(epochSeconds, secondsToAdd), (nanosToAdd / NANOS_PER_ONE))
186+
val newNanosToAdd = nanosToAdd % NANOS_PER_ONE
187+
val nanoAdjustment = (nanosecondsOfSecond + newNanosToAdd) // safe int+NANOS_PER_ONE
188+
return Instant.fromEpochSecondsThrowing(newEpochSeconds, nanoAdjustment)
189+
}

0 commit comments

Comments
 (0)