diff --git a/core/commonMain/src/Instant.kt b/core/commonMain/src/Instant.kt index b5e6c9eb0..fe7bce21d 100644 --- a/core/commonMain/src/Instant.kt +++ b/core/commonMain/src/Instant.kt @@ -182,6 +182,29 @@ public fun String.toInstant(): Instant = Instant.parse(this) public expect fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Instant /** + * Returns an instant that is the result of subtracting components of [DateTimePeriod] from this instant. The components + * are subtracted in the order from the largest units to the smallest, i.e. from years to nanoseconds. + * + * @throws DateTimeArithmeticException if this value or the results of intermediate computations are too large to fit in + * [LocalDateTime]. + */ +public fun Instant.minus(period: DateTimePeriod, timeZone: TimeZone): Instant = + /* An overflow can happen for any component, but we are only worried about nanoseconds, as having an overflow in + any other component means that `plus` will throw due to the minimum value of the numeric type overflowing the + platform-specific limits. */ + if (period.nanoseconds != Long.MIN_VALUE) { + val negatedPeriod = with(period) { + DateTimePeriod(-years, -months, -days, -hours, -minutes, -seconds, -nanoseconds) + } + plus(negatedPeriod, timeZone) + } else { + val negatedPeriod = with(period) { + DateTimePeriod(-years, -months, -days, -hours, -minutes, -seconds, -(nanoseconds+1)) + } + plus(negatedPeriod, timeZone).plus(DateTimeUnit.NANOSECOND) + } + + /** * Returns a [DateTimePeriod] representing the difference between `this` and [other] instants. * * The components of [DateTimePeriod] are calculated so that adding it to `this` instant results in the [other] instant. @@ -290,6 +313,17 @@ public fun Instant.minus(other: Instant, timeZone: TimeZone): DateTimePeriod = */ public expect fun Instant.plus(unit: DateTimeUnit, timeZone: TimeZone): Instant +/** + * Returns an instant that is the result of subtracting one [unit] from this instant + * in the specified [timeZone]. + * + * The returned instant is earlier than this instant. + * + * @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. + */ +public fun Instant.minus(unit: DateTimeUnit, timeZone: TimeZone): Instant = + plus(-1, unit, timeZone) + /** * Returns an instant that is the result of adding one [unit] to this instant. * @@ -300,6 +334,16 @@ public expect fun Instant.plus(unit: DateTimeUnit, timeZone: TimeZone): Instant public fun Instant.plus(unit: DateTimeUnit.TimeBased): Instant = plus(1L, unit) +/** + * Returns an instant that is the result of subtracting one [unit] from this instant. + * + * The returned instant is earlier than this instant. + * + * The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them. + */ +public fun Instant.minus(unit: DateTimeUnit.TimeBased): Instant = + plus(-1L, unit) + /** * Returns an instant that is the result of adding the [value] number of the specified [unit] to this instant * in the specified [timeZone]. @@ -311,6 +355,17 @@ public fun Instant.plus(unit: DateTimeUnit.TimeBased): Instant = */ public expect fun Instant.plus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant +/** + * Returns an instant that is the result of subtracting the [value] number of the specified [unit] from this instant + * in the specified [timeZone]. + * + * If the [value] is positive, the returned instant is earlier than this instant. + * If the [value] is negative, the returned instant is later than this instant. + * + * @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. + */ +public expect fun Instant.minus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant + /** * Returns an instant that is the result of adding the [value] number of the specified [unit] to this instant. * @@ -322,6 +377,17 @@ public expect fun Instant.plus(value: Int, unit: DateTimeUnit, timeZone: TimeZon public fun Instant.plus(value: Int, unit: DateTimeUnit.TimeBased): Instant = plus(value.toLong(), unit) +/** + * Returns an instant that is the result of subtracting the [value] number of the specified [unit] from this instant. + * + * If the [value] is positive, the returned instant is earlier than this instant. + * If the [value] is negative, the returned instant is later than this instant. + * + * The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them. + */ +public fun Instant.minus(value: Int, unit: DateTimeUnit.TimeBased): Instant = + minus(value.toLong(), unit) + /** * Returns an instant that is the result of adding the [value] number of the specified [unit] to this instant * in the specified [timeZone]. @@ -333,6 +399,22 @@ public fun Instant.plus(value: Int, unit: DateTimeUnit.TimeBased): Instant = */ public expect fun Instant.plus(value: Long, unit: DateTimeUnit, timeZone: TimeZone): Instant +/** + * Returns an instant that is the result of subtracting the [value] number of the specified [unit] from this instant + * in the specified [timeZone]. + * + * If the [value] is positive, the returned instant is earlier than this instant. + * If the [value] is negative, the returned instant is later than this instant. + * + * @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. + */ +public fun Instant.minus(value: Long, unit: DateTimeUnit, timeZone: TimeZone) = + if (value != Long.MIN_VALUE) { + plus(-value, unit, timeZone) + } else { + plus(-(value + 1), unit, timeZone).plus(unit, timeZone) + } + /** * Returns an instant that is the result of adding the [value] number of the specified [unit] to this instant. * @@ -343,6 +425,21 @@ public expect fun Instant.plus(value: Long, unit: DateTimeUnit, timeZone: TimeZo */ public expect fun Instant.plus(value: Long, unit: DateTimeUnit.TimeBased): Instant +/** + * Returns an instant that is the result of subtracting the [value] number of the specified [unit] from this instant. + * + * If the [value] is positive, the returned instant is earlier than this instant. + * If the [value] is negative, the returned instant is later than this instant. + * + * The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them. + */ +public fun Instant.minus(value: Long, unit: DateTimeUnit.TimeBased): Instant = + if (value != Long.MIN_VALUE) { + plus(-value, unit) + } else { + plus(-(value + 1), unit).plus(unit) + } + /** * Returns the whole number of the specified date or time [units][unit] between [other] and `this` instants * in the specified [timeZone]. diff --git a/core/commonMain/src/LocalDate.kt b/core/commonMain/src/LocalDate.kt index 907e0d8f2..df9ec4f20 100644 --- a/core/commonMain/src/LocalDate.kt +++ b/core/commonMain/src/LocalDate.kt @@ -110,6 +110,23 @@ public fun LocalDate.atTime(hour: Int, minute: Int, second: Int = 0, nanosecond: */ expect operator fun LocalDate.plus(period: DatePeriod): LocalDate +/** + * Returns a date that is the result of subtracting components of [DatePeriod] from this date. The components are + * subtracted in the order from the largest units to the smallest, i.e. from years to days. + * + * @see LocalDate.periodUntil + * @throws DateTimeArithmeticException if this value or the results of intermediate computations are too large to fit in + * [LocalDate]. + */ +public operator fun LocalDate.minus(period: DatePeriod): LocalDate = + if (period.days != Int.MIN_VALUE && period.months != Int.MIN_VALUE) { + plus(with(period) { DatePeriod(-years, -months, -days) }) + } else { + minus(period.years, DateTimeUnit.YEAR). + minus(period.months, DateTimeUnit.MONTH). + minus(period.days, DateTimeUnit.DAY) + } + /** * Returns a [DatePeriod] representing the difference between `this` and [other] dates. * @@ -186,6 +203,15 @@ public expect fun LocalDate.yearsUntil(other: LocalDate): Int */ public expect fun LocalDate.plus(unit: DateTimeUnit.DateBased): LocalDate +/** + * Returns a [LocalDate] that is the result of subtracting one [unit] from this date. + * + * The returned date is earlier than this date. + * + * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. + */ +public fun LocalDate.minus(unit: DateTimeUnit.DateBased): LocalDate = plus(-1, unit) + /** * Returns a [LocalDate] that is the result of adding the [value] number of the specified [unit] to this date. * @@ -196,6 +222,16 @@ public expect fun LocalDate.plus(unit: DateTimeUnit.DateBased): LocalDate */ public expect fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): LocalDate +/** + * Returns a [LocalDate] that is the result of subtracting the [value] number of the specified [unit] from this date. + * + * If the [value] is positive, the returned date is earlier than this date. + * If the [value] is negative, the returned date is later than this date. + * + * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. + */ +public expect fun LocalDate.minus(value: Int, unit: DateTimeUnit.DateBased): LocalDate + /** * Returns a [LocalDate] that is the result of adding the [value] number of the specified [unit] to this date. * @@ -205,3 +241,13 @@ public expect fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): Loca * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. */ public expect fun LocalDate.plus(value: Long, unit: DateTimeUnit.DateBased): LocalDate + +/** + * Returns a [LocalDate] that is the result of subtracting the [value] number of the specified [unit] from this date. + * + * If the [value] is positive, the returned date is earlier than this date. + * If the [value] is negative, the returned date is later than this date. + * + * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. + */ +public fun LocalDate.minus(value: Long, unit: DateTimeUnit.DateBased): LocalDate = plus(-value, unit) diff --git a/core/commonTest/src/InstantTest.kt b/core/commonTest/src/InstantTest.kt index 3c069aed7..2f5cba334 100644 --- a/core/commonTest/src/InstantTest.kt +++ b/core/commonTest/src/InstantTest.kt @@ -100,6 +100,9 @@ class InstantTest { val diff = instant2.minus(instant1, timeUnit, zone) assertEquals(instant2 - instant1, timeUnit.duration * diff.toDouble()) assertEquals(instant2, instant1.plus(diff, timeUnit, zone)) + assertEquals(instant1, instant2.minus(diff, timeUnit, zone)) + assertEquals(instant2, instant1.plus(diff, timeUnit)) + assertEquals(instant1, instant2.minus(diff, timeUnit)) } } @@ -109,12 +112,14 @@ class InstantTest { val instant2 = instant1.plus(DateTimePeriod(hours = 24), zone) checkComponents(instant2.toLocalDateTime(zone), 2019, 10, 28, 1, 59) expectBetween(instant1, instant2, 24, DateTimeUnit.HOUR) + assertEquals(instant1, instant2.minus(DateTimePeriod(hours = 24), zone)) val instant3 = instant1.plus(DateTimeUnit.DAY, zone) checkComponents(instant3.toLocalDateTime(zone), 2019, 10, 28, 2, 59) expectBetween(instant1, instant3, 25, DateTimeUnit.HOUR) expectBetween(instant1, instant3, 1, DateTimeUnit.DAY) assertEquals(1, instant1.daysUntil(instant3, zone)) + assertEquals(instant1.minus(DateTimeUnit.HOUR), instant2.minus(DateTimeUnit.DAY, zone)) val instant4 = instant1.plus(14, DateTimeUnit.MONTH, zone) checkComponents(instant4.toLocalDateTime(zone), 2020, 12, 27, 2, 59) @@ -124,7 +129,7 @@ class InstantTest { expectBetween(instant1, instant4, 61, DateTimeUnit.WEEK) expectBetween(instant1, instant4, 366 + 31 + 30, DateTimeUnit.DAY) expectBetween(instant1, instant4, (366 + 31 + 30) * 24 + 1, DateTimeUnit.HOUR) - + assertEquals(instant1.plus(DateTimeUnit.HOUR), instant4.minus(14, DateTimeUnit.MONTH, zone)) val period = DateTimePeriod(days = 1, hours = 1) val instant5 = instant1.plus(period, zone) @@ -132,11 +137,13 @@ class InstantTest { assertEquals(period, instant1.periodUntil(instant5, zone)) assertEquals(period, instant5.minus(instant1, zone)) assertEquals(26.hours, instant5.minus(instant1)) + assertEquals(instant1.plus(DateTimeUnit.HOUR), instant5.minus(period, zone)) val instant6 = instant1.plus(23, DateTimeUnit.HOUR, zone) checkComponents(instant6.toLocalDateTime(zone), 2019, 10, 28, 0, 59) expectBetween(instant1, instant6, 23, DateTimeUnit.HOUR) expectBetween(instant1, instant6, 0, DateTimeUnit.DAY) + assertEquals(instant1, instant6.minus(23, DateTimeUnit.HOUR, zone)) } @Test @@ -465,6 +472,19 @@ class InstantRangeTest { assertArithmeticFails("$instant") { instant.plus(DateTimePeriod(seconds = Long.MAX_VALUE), UTC) } assertArithmeticFails("$instant") { instant.plus(DateTimePeriod(seconds = Long.MIN_VALUE), UTC) } } + // Arithmetic overflow in an Int + for (instant in smallInstants + listOf(maxValidInstant)) { + assertEquals(instant.epochSeconds + Int.MIN_VALUE, + instant.plus(Int.MIN_VALUE, DateTimeUnit.SECOND, UTC).epochSeconds) + assertEquals(instant.epochSeconds - Int.MAX_VALUE, + instant.minus(Int.MAX_VALUE, DateTimeUnit.SECOND, UTC).epochSeconds) + } + for (instant in smallInstants + listOf(minValidInstant)) { + assertEquals(instant.epochSeconds + Int.MAX_VALUE, + instant.plus(Int.MAX_VALUE, DateTimeUnit.SECOND, UTC).epochSeconds) + assertEquals(instant.epochSeconds - Int.MIN_VALUE, + instant.minus(Int.MIN_VALUE, DateTimeUnit.SECOND, UTC).epochSeconds) + } // Overflowing a LocalDateTime in input maxValidInstant.plus(DateTimePeriod(nanoseconds = -1), UTC) minValidInstant.plus(DateTimePeriod(nanoseconds = 1), UTC) diff --git a/core/commonTest/src/LocalDateTest.kt b/core/commonTest/src/LocalDateTest.kt index eca91529e..38092ba3b 100644 --- a/core/commonTest/src/LocalDateTest.kt +++ b/core/commonTest/src/LocalDateTest.kt @@ -91,6 +91,9 @@ class LocalDateTest { checkComponents(startDate.plus(1, DateTimeUnit.DAY), 2016, 3, 1) checkComponents(startDate.plus(DateTimeUnit.YEAR), 2017, 2, 28) checkComponents(startDate + DatePeriod(years = 4), 2020, 2, 29) + assertEquals(startDate, startDate.plus(DateTimeUnit.DAY).minus(DateTimeUnit.DAY)) + assertEquals(startDate, startDate.plus(3, DateTimeUnit.DAY).minus(3, DateTimeUnit.DAY)) + assertEquals(startDate, startDate + DatePeriod(years = 4) - DatePeriod(years = 4)) checkComponents(LocalDate.parse("2016-01-31") + DatePeriod(months = 1), 2016, 2, 29) diff --git a/core/jsMain/src/Instant.kt b/core/jsMain/src/Instant.kt index b45669349..a4f2e5073 100644 --- a/core/jsMain/src/Instant.kt +++ b/core/jsMain/src/Instant.kt @@ -163,6 +163,12 @@ public actual fun Instant.plus(value: Int, unit: DateTimeUnit, timeZone: TimeZon throw e } +public actual fun Instant.minus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant = + if (value == Int.MIN_VALUE) + plus(-value.toLong(), unit, timeZone) + else + plus(-value, unit, timeZone) + actual fun Instant.plus(value: Long, unit: DateTimeUnit.TimeBased): Instant = try { multiplyAndDivide(value, unit.nanoseconds, NANOS_PER_ONE.toLong()).let { (d, r) -> diff --git a/core/jsMain/src/LocalDate.kt b/core/jsMain/src/LocalDate.kt index 2c71900ab..2132b7cd2 100644 --- a/core/jsMain/src/LocalDate.kt +++ b/core/jsMain/src/LocalDate.kt @@ -50,6 +50,7 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa public actual fun LocalDate.plus(unit: DateTimeUnit.DateBased): LocalDate = plusNumber(1, unit) public actual fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): LocalDate = plusNumber(value, unit) +public actual fun LocalDate.minus(value: Int, unit: DateTimeUnit.DateBased): LocalDate = plusNumber(-value, unit) public actual fun LocalDate.plus(value: Long, unit: DateTimeUnit.DateBased): LocalDate = plusNumber(value, unit) private fun LocalDate.plusNumber(value: Number, unit: DateTimeUnit.DateBased): LocalDate = diff --git a/core/jvmMain/src/Instant.kt b/core/jvmMain/src/Instant.kt index 286d6b01b..dfcace595 100644 --- a/core/jvmMain/src/Instant.kt +++ b/core/jvmMain/src/Instant.kt @@ -113,6 +113,9 @@ public actual fun Instant.plus(unit: DateTimeUnit, timeZone: TimeZone): Instant 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) diff --git a/core/jvmMain/src/LocalDate.kt b/core/jvmMain/src/LocalDate.kt index c128832cc..8c3625845 100644 --- a/core/jvmMain/src/LocalDate.kt +++ b/core/jvmMain/src/LocalDate.kt @@ -55,6 +55,9 @@ public actual fun LocalDate.plus(unit: DateTimeUnit.DateBased): LocalDate = public actual fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): LocalDate = plus(value.toLong(), unit) +public actual fun LocalDate.minus(value: Int, unit: DateTimeUnit.DateBased): LocalDate = + plus(-value.toLong(), unit) + public actual fun LocalDate.plus(value: Long, unit: DateTimeUnit.DateBased): LocalDate = try { when (unit) { diff --git a/core/nativeMain/src/Instant.kt b/core/nativeMain/src/Instant.kt index f6f698a63..78b9c78ee 100644 --- a/core/nativeMain/src/Instant.kt +++ b/core/nativeMain/src/Instant.kt @@ -300,6 +300,8 @@ 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 -> { diff --git a/core/nativeMain/src/LocalDate.kt b/core/nativeMain/src/LocalDate.kt index f72bcd1ad..dc575a1d1 100644 --- a/core/nativeMain/src/LocalDate.kt +++ b/core/nativeMain/src/LocalDate.kt @@ -259,6 +259,8 @@ public actual fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): Loca throw DateTimeArithmeticException("Boundaries of LocalDate exceeded when adding a value", e) } +public actual fun LocalDate.minus(value: Int, unit: DateTimeUnit.DateBased): LocalDate = plus(-value, unit) + public actual fun LocalDate.plus(value: Long, unit: DateTimeUnit.DateBased): LocalDate = if (value > Int.MAX_VALUE || value < Int.MIN_VALUE) throw DateTimeArithmeticException("Can't add a Long to a LocalDate") // TODO: less specific message diff --git a/core/nativeMain/src/ZonedDateTime.kt b/core/nativeMain/src/ZonedDateTime.kt index f896792c3..a0aef9ae6 100644 --- a/core/nativeMain/src/ZonedDateTime.kt +++ b/core/nativeMain/src/ZonedDateTime.kt @@ -16,7 +16,16 @@ internal class ZonedDateTime(val dateTime: LocalDateTime, private val zone: Time internal fun plus(value: Int, unit: DateTimeUnit.DateBased): ZonedDateTime = dateTime.plus(value, unit).resolve() // Never throws in practice - private fun LocalDateTime.resolve(): ZonedDateTime = with(zone) { atZone(offset) } + private fun LocalDateTime.resolve(): ZonedDateTime = + // workaround for https://github.com/Kotlin/kotlinx-datetime/issues/51 + if (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. + with(zone) { atZone(offset) } + } override fun equals(other: Any?): Boolean = this === other || other is ZonedDateTime &&