Skip to content

Commit f9f804f

Browse files
authored
Match 'plus' with the corresponding 'minus' functions (#49)
1 parent ad8551b commit f9f804f

File tree

11 files changed

+194
-2
lines changed

11 files changed

+194
-2
lines changed

core/commonMain/src/Instant.kt

+97
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,29 @@ public fun String.toInstant(): Instant = Instant.parse(this)
182182
public expect fun Instant.plus(period: DateTimePeriod, timeZone: TimeZone): Instant
183183

184184
/**
185+
* Returns an instant that is the result of subtracting components of [DateTimePeriod] from this instant. The components
186+
* are subtracted in the order from the largest units to the smallest, i.e. from years to nanoseconds.
187+
*
188+
* @throws DateTimeArithmeticException if this value or the results of intermediate computations are too large to fit in
189+
* [LocalDateTime].
190+
*/
191+
public fun Instant.minus(period: DateTimePeriod, timeZone: TimeZone): Instant =
192+
/* An overflow can happen for any component, but we are only worried about nanoseconds, as having an overflow in
193+
any other component means that `plus` will throw due to the minimum value of the numeric type overflowing the
194+
platform-specific limits. */
195+
if (period.nanoseconds != Long.MIN_VALUE) {
196+
val negatedPeriod = with(period) {
197+
DateTimePeriod(-years, -months, -days, -hours, -minutes, -seconds, -nanoseconds)
198+
}
199+
plus(negatedPeriod, timeZone)
200+
} else {
201+
val negatedPeriod = with(period) {
202+
DateTimePeriod(-years, -months, -days, -hours, -minutes, -seconds, -(nanoseconds+1))
203+
}
204+
plus(negatedPeriod, timeZone).plus(DateTimeUnit.NANOSECOND)
205+
}
206+
207+
/**
185208
* Returns a [DateTimePeriod] representing the difference between `this` and [other] instants.
186209
*
187210
* 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 =
290313
*/
291314
public expect fun Instant.plus(unit: DateTimeUnit, timeZone: TimeZone): Instant
292315

316+
/**
317+
* Returns an instant that is the result of subtracting one [unit] from this instant
318+
* in the specified [timeZone].
319+
*
320+
* The returned instant is earlier than this instant.
321+
*
322+
* @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime].
323+
*/
324+
public fun Instant.minus(unit: DateTimeUnit, timeZone: TimeZone): Instant =
325+
plus(-1, unit, timeZone)
326+
293327
/**
294328
* Returns an instant that is the result of adding one [unit] to this instant.
295329
*
@@ -300,6 +334,16 @@ public expect fun Instant.plus(unit: DateTimeUnit, timeZone: TimeZone): Instant
300334
public fun Instant.plus(unit: DateTimeUnit.TimeBased): Instant =
301335
plus(1L, unit)
302336

337+
/**
338+
* Returns an instant that is the result of subtracting one [unit] from this instant.
339+
*
340+
* The returned instant is earlier than this instant.
341+
*
342+
* The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them.
343+
*/
344+
public fun Instant.minus(unit: DateTimeUnit.TimeBased): Instant =
345+
plus(-1L, unit)
346+
303347
/**
304348
* Returns an instant that is the result of adding the [value] number of the specified [unit] to this instant
305349
* in the specified [timeZone].
@@ -311,6 +355,17 @@ public fun Instant.plus(unit: DateTimeUnit.TimeBased): Instant =
311355
*/
312356
public expect fun Instant.plus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant
313357

358+
/**
359+
* Returns an instant that is the result of subtracting the [value] number of the specified [unit] from this instant
360+
* in the specified [timeZone].
361+
*
362+
* If the [value] is positive, the returned instant is earlier than this instant.
363+
* If the [value] is negative, the returned instant is later than this instant.
364+
*
365+
* @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime].
366+
*/
367+
public expect fun Instant.minus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant
368+
314369
/**
315370
* Returns an instant that is the result of adding the [value] number of the specified [unit] to this instant.
316371
*
@@ -322,6 +377,17 @@ public expect fun Instant.plus(value: Int, unit: DateTimeUnit, timeZone: TimeZon
322377
public fun Instant.plus(value: Int, unit: DateTimeUnit.TimeBased): Instant =
323378
plus(value.toLong(), unit)
324379

380+
/**
381+
* Returns an instant that is the result of subtracting the [value] number of the specified [unit] from this instant.
382+
*
383+
* If the [value] is positive, the returned instant is earlier than this instant.
384+
* If the [value] is negative, the returned instant is later than this instant.
385+
*
386+
* The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them.
387+
*/
388+
public fun Instant.minus(value: Int, unit: DateTimeUnit.TimeBased): Instant =
389+
minus(value.toLong(), unit)
390+
325391
/**
326392
* Returns an instant that is the result of adding the [value] number of the specified [unit] to this instant
327393
* in the specified [timeZone].
@@ -333,6 +399,22 @@ public fun Instant.plus(value: Int, unit: DateTimeUnit.TimeBased): Instant =
333399
*/
334400
public expect fun Instant.plus(value: Long, unit: DateTimeUnit, timeZone: TimeZone): Instant
335401

402+
/**
403+
* Returns an instant that is the result of subtracting the [value] number of the specified [unit] from this instant
404+
* in the specified [timeZone].
405+
*
406+
* If the [value] is positive, the returned instant is earlier than this instant.
407+
* If the [value] is negative, the returned instant is later than this instant.
408+
*
409+
* @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime].
410+
*/
411+
public fun Instant.minus(value: Long, unit: DateTimeUnit, timeZone: TimeZone) =
412+
if (value != Long.MIN_VALUE) {
413+
plus(-value, unit, timeZone)
414+
} else {
415+
plus(-(value + 1), unit, timeZone).plus(unit, timeZone)
416+
}
417+
336418
/**
337419
* Returns an instant that is the result of adding the [value] number of the specified [unit] to this instant.
338420
*
@@ -343,6 +425,21 @@ public expect fun Instant.plus(value: Long, unit: DateTimeUnit, timeZone: TimeZo
343425
*/
344426
public expect fun Instant.plus(value: Long, unit: DateTimeUnit.TimeBased): Instant
345427

428+
/**
429+
* Returns an instant that is the result of subtracting the [value] number of the specified [unit] from this instant.
430+
*
431+
* If the [value] is positive, the returned instant is earlier than this instant.
432+
* If the [value] is negative, the returned instant is later than this instant.
433+
*
434+
* The return value is clamped to the platform-specific boundaries for [Instant] if the result exceeds them.
435+
*/
436+
public fun Instant.minus(value: Long, unit: DateTimeUnit.TimeBased): Instant =
437+
if (value != Long.MIN_VALUE) {
438+
plus(-value, unit)
439+
} else {
440+
plus(-(value + 1), unit).plus(unit)
441+
}
442+
346443
/**
347444
* Returns the whole number of the specified date or time [units][unit] between [other] and `this` instants
348445
* in the specified [timeZone].

core/commonMain/src/LocalDate.kt

+46
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,23 @@ public fun LocalDate.atTime(hour: Int, minute: Int, second: Int = 0, nanosecond:
110110
*/
111111
expect operator fun LocalDate.plus(period: DatePeriod): LocalDate
112112

113+
/**
114+
* Returns a date that is the result of subtracting components of [DatePeriod] from this date. The components are
115+
* subtracted in the order from the largest units to the smallest, i.e. from years to days.
116+
*
117+
* @see LocalDate.periodUntil
118+
* @throws DateTimeArithmeticException if this value or the results of intermediate computations are too large to fit in
119+
* [LocalDate].
120+
*/
121+
public operator fun LocalDate.minus(period: DatePeriod): LocalDate =
122+
if (period.days != Int.MIN_VALUE && period.months != Int.MIN_VALUE) {
123+
plus(with(period) { DatePeriod(-years, -months, -days) })
124+
} else {
125+
minus(period.years, DateTimeUnit.YEAR).
126+
minus(period.months, DateTimeUnit.MONTH).
127+
minus(period.days, DateTimeUnit.DAY)
128+
}
129+
113130
/**
114131
* Returns a [DatePeriod] representing the difference between `this` and [other] dates.
115132
*
@@ -186,6 +203,15 @@ public expect fun LocalDate.yearsUntil(other: LocalDate): Int
186203
*/
187204
public expect fun LocalDate.plus(unit: DateTimeUnit.DateBased): LocalDate
188205

206+
/**
207+
* Returns a [LocalDate] that is the result of subtracting one [unit] from this date.
208+
*
209+
* The returned date is earlier than this date.
210+
*
211+
* @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate].
212+
*/
213+
public fun LocalDate.minus(unit: DateTimeUnit.DateBased): LocalDate = plus(-1, unit)
214+
189215
/**
190216
* Returns a [LocalDate] that is the result of adding the [value] number of the specified [unit] to this date.
191217
*
@@ -196,6 +222,16 @@ public expect fun LocalDate.plus(unit: DateTimeUnit.DateBased): LocalDate
196222
*/
197223
public expect fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): LocalDate
198224

225+
/**
226+
* Returns a [LocalDate] that is the result of subtracting the [value] number of the specified [unit] from this date.
227+
*
228+
* If the [value] is positive, the returned date is earlier than this date.
229+
* If the [value] is negative, the returned date is later than this date.
230+
*
231+
* @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate].
232+
*/
233+
public expect fun LocalDate.minus(value: Int, unit: DateTimeUnit.DateBased): LocalDate
234+
199235
/**
200236
* Returns a [LocalDate] that is the result of adding the [value] number of the specified [unit] to this date.
201237
*
@@ -205,3 +241,13 @@ public expect fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): Loca
205241
* @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate].
206242
*/
207243
public expect fun LocalDate.plus(value: Long, unit: DateTimeUnit.DateBased): LocalDate
244+
245+
/**
246+
* Returns a [LocalDate] that is the result of subtracting the [value] number of the specified [unit] from this date.
247+
*
248+
* If the [value] is positive, the returned date is earlier than this date.
249+
* If the [value] is negative, the returned date is later than this date.
250+
*
251+
* @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate].
252+
*/
253+
public fun LocalDate.minus(value: Long, unit: DateTimeUnit.DateBased): LocalDate = plus(-value, unit)

core/commonTest/src/InstantTest.kt

+21-1
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ class InstantTest {
100100
val diff = instant2.minus(instant1, timeUnit, zone)
101101
assertEquals(instant2 - instant1, timeUnit.duration * diff.toDouble())
102102
assertEquals(instant2, instant1.plus(diff, timeUnit, zone))
103+
assertEquals(instant1, instant2.minus(diff, timeUnit, zone))
104+
assertEquals(instant2, instant1.plus(diff, timeUnit))
105+
assertEquals(instant1, instant2.minus(diff, timeUnit))
103106
}
104107
}
105108

@@ -109,12 +112,14 @@ class InstantTest {
109112
val instant2 = instant1.plus(DateTimePeriod(hours = 24), zone)
110113
checkComponents(instant2.toLocalDateTime(zone), 2019, 10, 28, 1, 59)
111114
expectBetween(instant1, instant2, 24, DateTimeUnit.HOUR)
115+
assertEquals(instant1, instant2.minus(DateTimePeriod(hours = 24), zone))
112116

113117
val instant3 = instant1.plus(DateTimeUnit.DAY, zone)
114118
checkComponents(instant3.toLocalDateTime(zone), 2019, 10, 28, 2, 59)
115119
expectBetween(instant1, instant3, 25, DateTimeUnit.HOUR)
116120
expectBetween(instant1, instant3, 1, DateTimeUnit.DAY)
117121
assertEquals(1, instant1.daysUntil(instant3, zone))
122+
assertEquals(instant1.minus(DateTimeUnit.HOUR), instant2.minus(DateTimeUnit.DAY, zone))
118123

119124
val instant4 = instant1.plus(14, DateTimeUnit.MONTH, zone)
120125
checkComponents(instant4.toLocalDateTime(zone), 2020, 12, 27, 2, 59)
@@ -124,19 +129,21 @@ class InstantTest {
124129
expectBetween(instant1, instant4, 61, DateTimeUnit.WEEK)
125130
expectBetween(instant1, instant4, 366 + 31 + 30, DateTimeUnit.DAY)
126131
expectBetween(instant1, instant4, (366 + 31 + 30) * 24 + 1, DateTimeUnit.HOUR)
127-
132+
assertEquals(instant1.plus(DateTimeUnit.HOUR), instant4.minus(14, DateTimeUnit.MONTH, zone))
128133

129134
val period = DateTimePeriod(days = 1, hours = 1)
130135
val instant5 = instant1.plus(period, zone)
131136
checkComponents(instant5.toLocalDateTime(zone), 2019, 10, 28, 3, 59)
132137
assertEquals(period, instant1.periodUntil(instant5, zone))
133138
assertEquals(period, instant5.minus(instant1, zone))
134139
assertEquals(26.hours, instant5.minus(instant1))
140+
assertEquals(instant1.plus(DateTimeUnit.HOUR), instant5.minus(period, zone))
135141

136142
val instant6 = instant1.plus(23, DateTimeUnit.HOUR, zone)
137143
checkComponents(instant6.toLocalDateTime(zone), 2019, 10, 28, 0, 59)
138144
expectBetween(instant1, instant6, 23, DateTimeUnit.HOUR)
139145
expectBetween(instant1, instant6, 0, DateTimeUnit.DAY)
146+
assertEquals(instant1, instant6.minus(23, DateTimeUnit.HOUR, zone))
140147
}
141148

142149
@Test
@@ -465,6 +472,19 @@ class InstantRangeTest {
465472
assertArithmeticFails("$instant") { instant.plus(DateTimePeriod(seconds = Long.MAX_VALUE), UTC) }
466473
assertArithmeticFails("$instant") { instant.plus(DateTimePeriod(seconds = Long.MIN_VALUE), UTC) }
467474
}
475+
// Arithmetic overflow in an Int
476+
for (instant in smallInstants + listOf(maxValidInstant)) {
477+
assertEquals(instant.epochSeconds + Int.MIN_VALUE,
478+
instant.plus(Int.MIN_VALUE, DateTimeUnit.SECOND, UTC).epochSeconds)
479+
assertEquals(instant.epochSeconds - Int.MAX_VALUE,
480+
instant.minus(Int.MAX_VALUE, DateTimeUnit.SECOND, UTC).epochSeconds)
481+
}
482+
for (instant in smallInstants + listOf(minValidInstant)) {
483+
assertEquals(instant.epochSeconds + Int.MAX_VALUE,
484+
instant.plus(Int.MAX_VALUE, DateTimeUnit.SECOND, UTC).epochSeconds)
485+
assertEquals(instant.epochSeconds - Int.MIN_VALUE,
486+
instant.minus(Int.MIN_VALUE, DateTimeUnit.SECOND, UTC).epochSeconds)
487+
}
468488
// Overflowing a LocalDateTime in input
469489
maxValidInstant.plus(DateTimePeriod(nanoseconds = -1), UTC)
470490
minValidInstant.plus(DateTimePeriod(nanoseconds = 1), UTC)

core/commonTest/src/LocalDateTest.kt

+3
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ class LocalDateTest {
9191
checkComponents(startDate.plus(1, DateTimeUnit.DAY), 2016, 3, 1)
9292
checkComponents(startDate.plus(DateTimeUnit.YEAR), 2017, 2, 28)
9393
checkComponents(startDate + DatePeriod(years = 4), 2020, 2, 29)
94+
assertEquals(startDate, startDate.plus(DateTimeUnit.DAY).minus(DateTimeUnit.DAY))
95+
assertEquals(startDate, startDate.plus(3, DateTimeUnit.DAY).minus(3, DateTimeUnit.DAY))
96+
assertEquals(startDate, startDate + DatePeriod(years = 4) - DatePeriod(years = 4))
9497

9598
checkComponents(LocalDate.parse("2016-01-31") + DatePeriod(months = 1), 2016, 2, 29)
9699

core/jsMain/src/Instant.kt

+6
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,12 @@ public actual fun Instant.plus(value: Int, unit: DateTimeUnit, timeZone: TimeZon
163163
throw e
164164
}
165165

166+
public actual fun Instant.minus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant =
167+
if (value == Int.MIN_VALUE)
168+
plus(-value.toLong(), unit, timeZone)
169+
else
170+
plus(-value, unit, timeZone)
171+
166172
actual fun Instant.plus(value: Long, unit: DateTimeUnit.TimeBased): Instant =
167173
try {
168174
multiplyAndDivide(value, unit.nanoseconds, NANOS_PER_ONE.toLong()).let { (d, r) ->

core/jsMain/src/LocalDate.kt

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa
5050

5151
public actual fun LocalDate.plus(unit: DateTimeUnit.DateBased): LocalDate = plusNumber(1, unit)
5252
public actual fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): LocalDate = plusNumber(value, unit)
53+
public actual fun LocalDate.minus(value: Int, unit: DateTimeUnit.DateBased): LocalDate = plusNumber(-value, unit)
5354
public actual fun LocalDate.plus(value: Long, unit: DateTimeUnit.DateBased): LocalDate = plusNumber(value, unit)
5455

5556
private fun LocalDate.plusNumber(value: Number, unit: DateTimeUnit.DateBased): LocalDate =

core/jvmMain/src/Instant.kt

+3
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ public actual fun Instant.plus(unit: DateTimeUnit, timeZone: TimeZone): Instant
113113
public actual fun Instant.plus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant =
114114
plus(value.toLong(), unit, timeZone)
115115

116+
public actual fun Instant.minus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant =
117+
plus(-value.toLong(), unit, timeZone)
118+
116119
public actual fun Instant.plus(value: Long, unit: DateTimeUnit, timeZone: TimeZone): Instant =
117120
try {
118121
val thisZdt = atZone(timeZone)

core/jvmMain/src/LocalDate.kt

+3
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ public actual fun LocalDate.plus(unit: DateTimeUnit.DateBased): LocalDate =
5555
public actual fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): LocalDate =
5656
plus(value.toLong(), unit)
5757

58+
public actual fun LocalDate.minus(value: Int, unit: DateTimeUnit.DateBased): LocalDate =
59+
plus(-value.toLong(), unit)
60+
5861
public actual fun LocalDate.plus(value: Long, unit: DateTimeUnit.DateBased): LocalDate =
5962
try {
6063
when (unit) {

core/nativeMain/src/Instant.kt

+2
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,8 @@ public actual fun Instant.plus(unit: DateTimeUnit, timeZone: TimeZone): Instant
300300
plus(1L, unit, timeZone)
301301
public actual fun Instant.plus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant =
302302
plus(value.toLong(), unit, timeZone)
303+
public actual fun Instant.minus(value: Int, unit: DateTimeUnit, timeZone: TimeZone): Instant =
304+
plus(-value.toLong(), unit, timeZone)
303305
public actual fun Instant.plus(value: Long, unit: DateTimeUnit, timeZone: TimeZone): Instant = try {
304306
when (unit) {
305307
is DateTimeUnit.DateBased -> {

core/nativeMain/src/LocalDate.kt

+2
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,8 @@ public actual fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): Loca
259259
throw DateTimeArithmeticException("Boundaries of LocalDate exceeded when adding a value", e)
260260
}
261261

262+
public actual fun LocalDate.minus(value: Int, unit: DateTimeUnit.DateBased): LocalDate = plus(-value, unit)
263+
262264
public actual fun LocalDate.plus(value: Long, unit: DateTimeUnit.DateBased): LocalDate =
263265
if (value > Int.MAX_VALUE || value < Int.MIN_VALUE)
264266
throw DateTimeArithmeticException("Can't add a Long to a LocalDate") // TODO: less specific message

core/nativeMain/src/ZonedDateTime.kt

+10-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,16 @@ internal class ZonedDateTime(val dateTime: LocalDateTime, private val zone: Time
1616
internal fun plus(value: Int, unit: DateTimeUnit.DateBased): ZonedDateTime = dateTime.plus(value, unit).resolve()
1717

1818
// Never throws in practice
19-
private fun LocalDateTime.resolve(): ZonedDateTime = with(zone) { atZone(offset) }
19+
private fun LocalDateTime.resolve(): ZonedDateTime =
20+
// workaround for https://github.com/Kotlin/kotlinx-datetime/issues/51
21+
if (toInstant(offset).toLocalDateTime(zone) == this@resolve) {
22+
// this LocalDateTime is valid in these timezone and offset.
23+
ZonedDateTime(this, zone, offset)
24+
} else {
25+
// this LDT does need proper resolving, as the instant that it would map to given the preferred offset
26+
// is is mapped to another LDT.
27+
with(zone) { atZone(offset) }
28+
}
2029

2130
override fun equals(other: Any?): Boolean =
2231
this === other || other is ZonedDateTime &&

0 commit comments

Comments
 (0)