Skip to content

Commit e45e73f

Browse files
committed
Specialize plus(... DateTimeUnit) in each platform
1 parent bb3c91c commit e45e73f

File tree

10 files changed

+145
-167
lines changed

10 files changed

+145
-167
lines changed

core/commonMain/src/DateTimeUnit.kt

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ sealed class DateTimeUnit {
6464
}
6565

6666
sealed class DateBased : DateTimeUnit() {
67-
// TODO: investigate how to move subclasses to ChronoUnit scope
67+
// TODO: investigate how to move subclasses up to DateTimeUnit scope
6868
class DayBased(val days: Int) : DateBased() {
6969
init {
7070
require(days > 0) { "Unit duration must be positive, but was $days days." }
@@ -141,15 +141,3 @@ internal enum class CalendarUnit {
141141
NANOSECOND
142142
}
143143

144-
internal val CalendarUnit.dateTimeUnit: DateTimeUnit
145-
get() = when (this) {
146-
CalendarUnit.YEAR -> DateTimeUnit.YEAR
147-
CalendarUnit.MONTH -> DateTimeUnit.MONTH
148-
CalendarUnit.DAY -> DateTimeUnit.DAY
149-
CalendarUnit.HOUR -> DateTimeUnit.HOUR
150-
CalendarUnit.MINUTE -> DateTimeUnit.MINUTE
151-
CalendarUnit.SECOND -> DateTimeUnit.SECOND
152-
CalendarUnit.MILLISECOND -> DateTimeUnit.MILLISECOND
153-
CalendarUnit.MICROSECOND -> DateTimeUnit.MICROSECOND
154-
CalendarUnit.NANOSECOND -> DateTimeUnit.NANOSECOND
155-
}

core/commonMain/src/Instant.kt

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,6 @@ public fun String.toInstant(): Instant = Instant.parse(this)
9090
*/
9191
public expect fun Instant.plus(period: DateTimePeriod, zone: TimeZone): Instant
9292

93-
/**
94-
* @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime].
95-
*/
96-
internal expect fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone): Instant
97-
9893
/**
9994
* @throws DateTimeArithmeticException if this [Instant] or [other] is too large to fit in [LocalDateTime].
10095
*/
@@ -141,28 +136,17 @@ public fun Instant.minus(other: Instant, zone: TimeZone): DateTimePeriod = other
141136
/**
142137
* @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime].
143138
*/
144-
public fun Instant.plus(unit: DateTimeUnit, zone: TimeZone): Instant =
145-
plus(unit.calendarScale, unit.calendarUnit, zone)
139+
public expect fun Instant.plus(unit: DateTimeUnit, zone: TimeZone): Instant
146140

147141
/**
148142
* @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime].
149143
*/
150-
public fun Instant.plus(value: Int, unit: DateTimeUnit, zone: TimeZone): Instant =
151-
try {
152-
plus(safeMultiply(value.toLong(), unit.calendarScale), unit.calendarUnit, zone)
153-
} catch (e: ArithmeticException) {
154-
throw DateTimeArithmeticException(e)
155-
}
144+
public expect fun Instant.plus(value: Int, unit: DateTimeUnit, zone: TimeZone): Instant
156145

157146
/**
158147
* @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime].
159148
*/
160-
public fun Instant.plus(value: Long, unit: DateTimeUnit, zone: TimeZone): Instant =
161-
try {
162-
plus(safeMultiply(value, unit.calendarScale), unit.calendarUnit, zone)
163-
} catch (e: ArithmeticException) {
164-
throw DateTimeArithmeticException(e)
165-
}
149+
public expect fun Instant.plus(value: Long, unit: DateTimeUnit, zone: TimeZone): Instant
166150

167151

168152
public fun Instant.minus(other: Instant, unit: DateTimeUnit, zone: TimeZone): Long = other.until(this, unit, zone)

core/commonMain/src/LocalDate.kt

Lines changed: 16 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -37,20 +37,8 @@ public expect class LocalDate : Comparable<LocalDate> {
3737
*/
3838
public fun String.toLocalDate(): LocalDate = LocalDate.parse(this)
3939

40-
/**
41-
* @throws IllegalArgumentException if the calendar unit is not date-based.
42-
* @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate].
43-
*/
44-
internal expect fun LocalDate.plus(value: Long, unit: CalendarUnit): LocalDate
4540

4641
/**
47-
* @throws IllegalArgumentException if the calendar unit is not date-based.
48-
* @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate].
49-
*/
50-
internal expect fun LocalDate.plus(value: Int, unit: CalendarUnit): LocalDate
51-
52-
/**
53-
* @throws IllegalArgumentException if [period] has non-zero time (as opposed to date) components.
5442
* @throws DateTimeArithmeticException if arithmetic overflow occurs or the boundaries of [LocalDate] are exceeded at
5543
* any point in intermediate computations.
5644
*/
@@ -66,24 +54,22 @@ public expect fun LocalDate.daysUntil(other: LocalDate): Int
6654
public expect fun LocalDate.monthsUntil(other: LocalDate): Int
6755
public expect fun LocalDate.yearsUntil(other: LocalDate): Int
6856

69-
public fun LocalDate.plus(unit: DateTimeUnit.DateBased): LocalDate =
70-
plus(unit.calendarScale, unit.calendarUnit)
71-
public fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): LocalDate =
72-
try {
73-
plus(safeMultiply(value.toLong(), unit.calendarScale), unit.calendarUnit)
74-
} catch (e: Exception) {
75-
if (e !is ArithmeticException) throw e
76-
throw DateTimeArithmeticException(e)
77-
}
78-
public fun LocalDate.plus(value: Long, unit: DateTimeUnit.DateBased): LocalDate =
79-
try {
80-
plus(safeMultiply(value, unit.calendarScale), unit.calendarUnit)
81-
} catch (e: Exception) {
82-
if (e !is ArithmeticException) throw e
83-
throw DateTimeArithmeticException(e)
84-
}
57+
/**
58+
* @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate].
59+
*/
60+
public expect fun LocalDate.plus(unit: DateTimeUnit.DateBased): LocalDate
61+
62+
/**
63+
* @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate].
64+
*/
65+
public expect fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): LocalDate
66+
67+
/**
68+
* @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate].
69+
*/
70+
public expect fun LocalDate.plus(value: Long, unit: DateTimeUnit.DateBased): LocalDate
8571

8672
public fun LocalDate.until(other: LocalDate, unit: DateTimeUnit.DateBased): Int = when(unit) {
87-
is DateTimeUnit.DateBased.MonthBased -> (monthsUntil(other) / unit.months).toInt()
88-
is DateTimeUnit.DateBased.DayBased -> (daysUntil(other) / unit.days).toInt()
73+
is DateTimeUnit.DateBased.MonthBased -> monthsUntil(other) / unit.months
74+
is DateTimeUnit.DateBased.DayBased -> daysUntil(other) / unit.days
8975
}

core/jsMain/src/Instant.kt

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,20 @@ import kotlinx.datetime.internal.JSJoda.LocalTime
1818
import kotlin.math.nextTowards
1919
import kotlin.math.truncate
2020

21-
@OptIn(kotlin.time.ExperimentalTime::class)
21+
@OptIn(ExperimentalTime::class)
2222
public actual class Instant internal constructor(internal val value: jtInstant) : Comparable<Instant> {
2323

2424
actual val epochSeconds: Long
2525
get() = value.epochSecond().toLong()
2626
actual val nanosecondsOfSecond: Int
2727
get() = value.nano().toInt()
2828

29-
public actual fun toEpochMilliseconds(): Long = epochSeconds * 1000 + nanosecondsOfSecond / 1_000_000
29+
public actual fun toEpochMilliseconds(): Long =
30+
epochSeconds * MILLIS_PER_ONE + nanosecondsOfSecond / NANOS_PER_MILLI
3031

3132
actual operator fun plus(duration: Duration): Instant {
3233
val addSeconds = truncate(duration.inSeconds)
33-
val addNanos = (duration.inNanoseconds % 1e9).toInt()
34+
val addNanos = (duration.inNanoseconds % NANOS_PER_ONE).toInt()
3435
return try {
3536
Instant(plusFix(addSeconds, addNanos))
3637
} catch (e: Throwable) {
@@ -67,7 +68,7 @@ public actual class Instant internal constructor(internal val value: jtInstant)
6768
Instant(jtClock.systemUTC().instant())
6869

6970
actual fun fromEpochMilliseconds(epochMilliseconds: Long): Instant = try {
70-
fromEpochSeconds(epochMilliseconds / 1000, epochMilliseconds % 1000 * 1000_000)
71+
fromEpochSeconds(epochMilliseconds / MILLIS_PER_ONE, epochMilliseconds % MILLIS_PER_ONE * NANOS_PER_MILLI)
7172
} catch (e: Throwable) {
7273
if (!e.isJodaDateTimeException()) throw e
7374
if (epochMilliseconds > 0) MAX else MIN
@@ -141,23 +142,46 @@ private fun ZonedDateTime.plusNanosFix(nanoseconds: Long): ZonedDateTime {
141142

142143
private fun jtInstant.atZone(zone: TimeZone): ZonedDateTime = atZone(zone.zoneId)
143144

144-
internal actual fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone): Instant = try {
145-
val thisZdt = this.value.atZone(zone)
146-
when (unit) {
147-
CalendarUnit.YEAR -> thisZdt.plusYears(value).toInstant()
148-
CalendarUnit.MONTH -> thisZdt.plusMonths(value).toInstant()
149-
CalendarUnit.DAY -> thisZdt.plusDays(value).let { it as ZonedDateTime }.toInstant()
150-
CalendarUnit.HOUR -> thisZdt.plusHours(value).toInstant()
151-
CalendarUnit.MINUTE -> thisZdt.plusMinutes(value).toInstant()
152-
CalendarUnit.SECOND -> this.plusFix(value.toDouble(), 0)
153-
CalendarUnit.MILLISECOND -> this.plusFix((value / 1_000).toDouble(), (value % 1_000).toInt() * 1_000_000).also { it.atZone(zone) }
154-
CalendarUnit.MICROSECOND -> this.plusFix((value / 1_000_000).toDouble(), (value % 1_000_000).toInt() * 1000).also { it.atZone(zone) }
155-
CalendarUnit.NANOSECOND -> this.plusFix((value / 1_000_000_000).toDouble(), (value % 1_000_000_000).toInt()).also { it.atZone(zone) }
156-
}.let(::Instant)
157-
} catch (e: Throwable) {
158-
if (e.isJodaDateTimeException()) throw DateTimeArithmeticException(e)
159-
throw e
160-
}
145+
public actual fun Instant.plus(unit: DateTimeUnit, zone: TimeZone): Instant =
146+
plus(1, unit, zone)
147+
148+
public actual fun Instant.plus(value: Long, unit: DateTimeUnit, zone: TimeZone): Instant =
149+
try {
150+
val thisZdt = this.value.atZone(zone)
151+
when (unit) {
152+
is DateTimeUnit.TimeBased -> {
153+
multiplyAndDivide(value, unit.nanoseconds, NANOS_PER_ONE.toLong()).let {
154+
(d, r) -> this.plusFix(d.toDouble(), r.toInt()).also { it.atZone(zone.zoneId) }
155+
}
156+
}
157+
is DateTimeUnit.DateBased.DayBased ->
158+
(thisZdt.plusDays(value.toDouble() * unit.days) as ZonedDateTime).toInstant()
159+
is DateTimeUnit.DateBased.MonthBased ->
160+
thisZdt.plusMonths(value.toDouble() * unit.months).toInstant()
161+
}.let(::Instant)
162+
} catch (e: Throwable) {
163+
if (e.isJodaDateTimeException()) throw DateTimeArithmeticException(e)
164+
throw e
165+
}
166+
167+
public actual fun Instant.plus(value: Int, unit: DateTimeUnit, zone: TimeZone): Instant =
168+
try {
169+
val thisZdt = this.value.atZone(zone)
170+
when (unit) {
171+
is DateTimeUnit.TimeBased -> {
172+
multiplyAndDivide(value.toLong(), unit.nanoseconds, NANOS_PER_ONE.toLong()).let {
173+
(d, r) -> this.plusFix(d.toDouble(), r.toInt()).also { it.atZone(zone.zoneId) }
174+
}
175+
}
176+
is DateTimeUnit.DateBased.DayBased ->
177+
(thisZdt.plusDays(value.toDouble() * unit.days) as ZonedDateTime).toInstant()
178+
is DateTimeUnit.DateBased.MonthBased ->
179+
thisZdt.plusMonths(value.toDouble() * unit.months).toInstant()
180+
}.let(::Instant)
181+
} catch (e: Throwable) {
182+
if (e.isJodaDateTimeException()) throw DateTimeArithmeticException(e)
183+
throw e
184+
}
161185

162186
@OptIn(ExperimentalTime::class)
163187
public actual fun Instant.periodUntil(other: Instant, zone: TimeZone): DateTimePeriod = try {
@@ -183,11 +207,11 @@ public actual fun Instant.until(other: Instant, unit: DateTimeUnit, zone: TimeZo
183207
this.value.atZone(zone)
184208
other.value.atZone(zone)
185209
try {
186-
// TODO: use fused multiplyAddDivide
187-
safeAdd(
188-
safeMultiply(other.epochSeconds - this.epochSeconds, LocalTime.NANOS_PER_SECOND.toLong()),
189-
(other.nanosecondsOfSecond - this.nanosecondsOfSecond).toLong()
190-
) / unit.nanoseconds
210+
multiplyAddAndDivide(
211+
(other.value.epochSecond().toDouble() - this.value.epochSecond().toDouble()).toLong(),
212+
NANOS_PER_ONE.toLong(),
213+
(other.nanosecondsOfSecond - this.nanosecondsOfSecond).toLong(),
214+
unit.nanoseconds)
191215
} catch (e: ArithmeticException) {
192216
if (this < other) Long.MAX_VALUE else Long.MIN_VALUE
193217
}

core/jsMain/src/LocalDate.kt

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -46,31 +46,20 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa
4646
actual override fun compareTo(other: LocalDate): Int = this.value.compareTo(other.value).toInt()
4747
}
4848

49-
50-
private fun LocalDate.plusNumber(value: Number, unit: CalendarUnit): LocalDate = try {
51-
52-
when (unit) {
53-
CalendarUnit.YEAR -> this.value.plusYears(value)
54-
CalendarUnit.MONTH -> this.value.plusMonths(value)
55-
CalendarUnit.DAY -> this.value.plusDays(value)
56-
CalendarUnit.HOUR,
57-
CalendarUnit.MINUTE,
58-
CalendarUnit.SECOND,
59-
CalendarUnit.MILLISECOND,
60-
CalendarUnit.MICROSECOND,
61-
CalendarUnit.NANOSECOND -> throw IllegalArgumentException("Only date based units can be added to LocalDate")
62-
}.let(::LocalDate)
63-
} catch (e: Throwable) {
64-
if (e.isJodaDateTimeException() || e.isJodaArithmeticException()) throw DateTimeArithmeticException(e)
65-
throw e
66-
}
67-
68-
internal actual fun LocalDate.plus(value: Long, unit: CalendarUnit): LocalDate =
69-
plusNumber(value.toDouble(), unit)
70-
71-
internal actual fun LocalDate.plus(value: Int, unit: CalendarUnit): LocalDate =
72-
plusNumber(value, unit)
73-
49+
public actual fun LocalDate.plus(unit: DateTimeUnit.DateBased): LocalDate = plusNumber(1, unit)
50+
public actual fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): LocalDate = plusNumber(value, unit)
51+
public actual fun LocalDate.plus(value: Long, unit: DateTimeUnit.DateBased): LocalDate = plusNumber(value, unit)
52+
53+
private fun LocalDate.plusNumber(value: Number, unit: DateTimeUnit.DateBased): LocalDate =
54+
try {
55+
when (unit) {
56+
is DateTimeUnit.DateBased.DayBased -> this.value.plusDays(value.toDouble() * unit.days)
57+
is DateTimeUnit.DateBased.MonthBased -> this.value.plusMonths(value.toDouble() * unit.months)
58+
}.let(::LocalDate)
59+
} catch (e: Throwable) {
60+
if (!e.isJodaDateTimeException() && !e.isJodaArithmeticException()) throw e
61+
throw DateTimeArithmeticException("The result of adding $value of $unit to $this is out of LocalDate range.", e)
62+
}
7463

7564

7665
public actual operator fun LocalDate.plus(period: DatePeriod): LocalDate = try {

core/jvmMain/src/Instant.kt

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import kotlin.time.*
1414
import java.time.Instant as jtInstant
1515
import java.time.Clock as jtClock
1616

17-
@OptIn(kotlin.time.ExperimentalTime::class)
17+
@OptIn(ExperimentalTime::class)
1818
public actual class Instant internal constructor(internal val value: jtInstant) : Comparable<Instant> {
1919

2020
actual val epochSeconds: Long
@@ -105,23 +105,31 @@ public actual fun Instant.plus(period: DateTimePeriod, zone: TimeZone): Instant
105105
}
106106
}
107107

108-
internal actual fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone): Instant = try {
109-
val thisZdt = atZone(zone)
110-
when (unit) {
111-
CalendarUnit.YEAR -> thisZdt.plusYears(value).toInstant()
112-
CalendarUnit.MONTH -> thisZdt.plusMonths(value).toInstant()
113-
CalendarUnit.DAY -> thisZdt.plusDays(value).toInstant()
114-
CalendarUnit.HOUR -> thisZdt.plusHours(value).toInstant()
115-
CalendarUnit.MINUTE -> thisZdt.plusMinutes(value).toInstant()
116-
CalendarUnit.SECOND -> this.value.plusSeconds(value).also { it.atZone(zone.zoneId) }
117-
CalendarUnit.MILLISECOND -> this.value.plusMillis(value).also { it.atZone(zone.zoneId) }
118-
CalendarUnit.MICROSECOND -> this.value.plusSeconds(value / 1_000_000).plusNanos((value % 1_000_000) * 1000).also { it.atZone(zone.zoneId) }
119-
CalendarUnit.NANOSECOND -> this.value.plusNanos(value).also { it.atZone(zone.zoneId) }
120-
}.let(::Instant)
121-
} catch (e: Throwable) {
122-
if (e !is DateTimeException && e !is ArithmeticException) throw e
123-
throw DateTimeArithmeticException("Instant $this cannot be represented as local date when adding $value $unit to it", e)
124-
}
108+
public actual fun Instant.plus(unit: DateTimeUnit, zone: TimeZone): Instant =
109+
plus(1L, unit, zone)
110+
111+
public actual fun Instant.plus(value: Int, unit: DateTimeUnit, zone: TimeZone): Instant =
112+
plus(value.toLong(), unit, zone)
113+
114+
public actual fun Instant.plus(value: Long, unit: DateTimeUnit, zone: TimeZone): Instant =
115+
try {
116+
val thisZdt = atZone(zone)
117+
when (unit) {
118+
is DateTimeUnit.TimeBased -> {
119+
multiplyAndDivide(value, unit.nanoseconds, NANOS_PER_ONE.toLong()).let {
120+
(d, r) -> this.value.plusSeconds(d).plusNanos(r).also { it.atZone(zone.zoneId) }
121+
}
122+
}
123+
is DateTimeUnit.DateBased.DayBased ->
124+
thisZdt.plusDays(safeMultiply(value, unit.days.toLong())).toInstant()
125+
is DateTimeUnit.DateBased.MonthBased ->
126+
thisZdt.plusMonths(safeMultiply(value, unit.months.toLong())).toInstant()
127+
}.let(::Instant)
128+
} catch (e: Exception) {
129+
if (e !is DateTimeException && e !is ArithmeticException) throw e
130+
throw DateTimeArithmeticException("Instant $this cannot be represented as local date when adding $value $unit to it", e)
131+
}
132+
125133

126134
@OptIn(ExperimentalTime::class)
127135
public actual fun Instant.periodUntil(other: Instant, zone: TimeZone): DateTimePeriod {

0 commit comments

Comments
 (0)