Skip to content

Match 'plus' with the corresponding 'minus' functions #49

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Sep 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions core/commonMain/src/Instant.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
*
Expand All @@ -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].
Expand All @@ -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.
*
Expand All @@ -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].
Expand All @@ -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.
*
Expand All @@ -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].
Expand Down
46 changes: 46 additions & 0 deletions core/commonMain/src/LocalDate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
*
Expand All @@ -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.
*
Expand All @@ -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)
22 changes: 21 additions & 1 deletion core/commonTest/src/InstantTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}

Expand All @@ -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)
Expand All @@ -124,19 +129,21 @@ 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)
checkComponents(instant5.toLocalDateTime(zone), 2019, 10, 28, 3, 59)
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
Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions core/commonTest/src/LocalDateTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
6 changes: 6 additions & 0 deletions core/jsMain/src/Instant.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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) ->
Expand Down
1 change: 1 addition & 0 deletions core/jsMain/src/LocalDate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
3 changes: 3 additions & 0 deletions core/jvmMain/src/Instant.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions core/jvmMain/src/LocalDate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions core/nativeMain/src/Instant.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 -> {
Expand Down
2 changes: 2 additions & 0 deletions core/nativeMain/src/LocalDate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion core/nativeMain/src/ZonedDateTime.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 &&
Expand Down