Skip to content

Commit 5bc86cb

Browse files
authored
Native: use DateTimeUnit instead of CalendarUnit (#21)
1 parent 7768a95 commit 5bc86cb

File tree

8 files changed

+276
-178
lines changed

8 files changed

+276
-178
lines changed

core/commonMain/src/DateTimeUnit.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,16 @@ internal enum class CalendarUnit {
140140
MICROSECOND,
141141
NANOSECOND
142142
}
143+
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/nativeMain/src/Instant.kt

Lines changed: 31 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ private val instantParser: Parser<Instant>
5656
}
5757

5858
// never fails: 9_999 years are always supported
59-
val localDate = dateVal.withYear(dateVal.year % 10000).plus(days, CalendarUnit.DAY)
59+
val localDate = dateVal.withYear(dateVal.year % 10000).plus(days, DateTimeUnit.DAY)
6060
val localTime = LocalTime.of(hours, min, seconds, 0)
6161
val secDelta: Long = try {
6262
safeMultiply((dateVal.year / 10000).toLong(), SECONDS_PER_10000_YEARS)
@@ -285,10 +285,9 @@ private fun Instant.check(zone: TimeZone): Instant = [email protected] {
285285
actual fun Instant.plus(period: DateTimePeriod, zone: TimeZone): Instant = try {
286286
with(period) {
287287
val withDate = toZonedLocalDateTimeFailing(zone)
288-
.run { if (years != 0 && months == 0) plusYears(years.toLong()) else this }
289-
.run { if (months != 0) plusMonths(years * 12L + months.toLong()) else this }
290-
.run { if (days != 0) plusDays(days.toLong()) else this }
291-
// See [Instant.plus(Instant, long, CalendarUnit, TimeZone)] for an explanation of time inside day being special
288+
.run { if (years != 0 && months == 0) plus(years.toLong(), DateTimeUnit.YEAR) else this }
289+
.run { if (months != 0) plus(years * 12L + months.toLong(), DateTimeUnit.MONTH) else this }
290+
.run { if (days != 0) plus(days.toLong(), DateTimeUnit.DAY) else this }
292291
val secondsToAdd = safeAdd(seconds,
293292
safeAdd(minutes.toLong() * SECONDS_PER_MINUTE, hours.toLong() * SECONDS_PER_HOUR))
294293
withDate.toInstant().plus(secondsToAdd, period.nanoseconds)
@@ -299,33 +298,15 @@ actual fun Instant.plus(period: DateTimePeriod, zone: TimeZone): Instant = try {
299298
throw DateTimeArithmeticException("Boundaries of Instant exceeded when adding CalendarPeriod", e)
300299
}
301300

302-
internal actual fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone): Instant = try {
301+
internal actual fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone): Instant =
302+
plusDateTimeUnit(value, unit.dateTimeUnit, zone)
303+
304+
internal fun Instant.plusDateTimeUnit(value: Long, unit: DateTimeUnit, zone: TimeZone): Instant = try {
303305
when (unit) {
304-
CalendarUnit.YEAR -> toZonedLocalDateTimeFailing(zone).plusYears(value).toInstant()
305-
CalendarUnit.MONTH -> toZonedLocalDateTimeFailing(zone).plusMonths(value).toInstant()
306-
CalendarUnit.DAY -> toZonedLocalDateTimeFailing(zone).plusDays(value).toInstant()
307-
/* From org.threeten.bp.ZonedDateTime#plusHours: the time is added to the raw LocalDateTime,
308-
then org.threeten.bp.ZonedDateTime#create is called on the absolute instant
309-
(gotten from org.threeten.bp.chrono.ChronoLocalDateTime#toEpochSecond). This, in turn,
310-
finds an applicable offset and builds the local representation of the zoned datetime.
311-
If we then feed the result to org.threeten.bp.chrono.ChronoZonedDateTime#toInstant, it simply
312-
builds the instant with org.threeten.bp.chrono.ChronoZonedDateTime#toEpochSecond, which, once
313-
more, finds the absolute instant. Thus, we can summarize the composition of `atZone`, `plusHours`
314-
and `toInstant` like this:
315-
1. An absolute instant is converted to a zoned datetime by adding an offset;
316-
2. Time is added to a datetime, ignoring the offset;
317-
3. Using the know offset, it is converted to an absolute instant;
318-
4. The instant is adapted to a zoned datetime representation;
319-
5. The zoned datetime is converted to an absolute instant.
320-
4 and 5 are invertible by their form: their composition adds and subtracts the offset to and from
321-
the unix epoch. 1-3 can then be simplified to just adding the time to the instant directly.
322-
*/
323-
CalendarUnit.HOUR -> plus(safeMultiply(value, SECONDS_PER_HOUR.toLong()), 0).check(zone)
324-
CalendarUnit.MINUTE -> plus(safeMultiply(value, SECONDS_PER_MINUTE.toLong()), 0).check(zone)
325-
CalendarUnit.SECOND -> plus(value, 0).check(zone)
326-
CalendarUnit.MILLISECOND -> plus(value / MILLIS_PER_ONE, (value % MILLIS_PER_ONE) * NANOS_PER_MILLI).check(zone)
327-
CalendarUnit.MICROSECOND -> plus(value / MICROS_PER_ONE, (value % MICROS_PER_ONE) * NANOS_PER_MICRO).check(zone)
328-
CalendarUnit.NANOSECOND -> plus(0, value).check(zone)
306+
is DateTimeUnit.DateBased -> toZonedLocalDateTimeFailing(zone).plus(value, unit).toInstant()
307+
is DateTimeUnit.TimeBased -> multiplyAndDivide(value, unit.nanoseconds, NANOS_PER_ONE.toLong()).let {
308+
(seconds, nanoseconds) -> check(zone).plus(seconds, nanoseconds).check(zone)
309+
}
329310
}
330311
} catch (e: ArithmeticException) {
331312
throw DateTimeArithmeticException("Arithmetic overflow when adding to an Instant", e)
@@ -338,21 +319,30 @@ actual fun Instant.periodUntil(other: Instant, zone: TimeZone): DateTimePeriod {
338319
var thisLdt = toZonedLocalDateTimeFailing(zone)
339320
val otherLdt = other.toZonedLocalDateTimeFailing(zone)
340321

341-
val months = thisLdt.until(otherLdt, CalendarUnit.MONTH) // `until` on dates never fails
342-
thisLdt = thisLdt.plusMonths(months) // won't throw: thisLdt + months <= otherLdt, which is known to be valid
343-
val days = thisLdt.until(otherLdt, CalendarUnit.DAY) // `until` on dates never fails
344-
thisLdt = thisLdt.plusDays(days) // won't throw: thisLdt + days <= otherLdt
345-
val time = thisLdt.until(otherLdt, CalendarUnit.NANOSECOND).nanoseconds // |otherLdt - thisLdt| < 24h
322+
val months = thisLdt.until(otherLdt, DateTimeUnit.MONTH) // `until` on dates never fails
323+
thisLdt = thisLdt.plus(months, DateTimeUnit.MONTH) // won't throw: thisLdt + months <= otherLdt, which is known to be valid
324+
val days = thisLdt.until(otherLdt, DateTimeUnit.DAY) // `until` on dates never fails
325+
thisLdt = thisLdt.plus(days, DateTimeUnit.DAY) // won't throw: thisLdt + days <= otherLdt
326+
val time = thisLdt.until(otherLdt, DateTimeUnit.NANOSECOND).nanoseconds // |otherLdt - thisLdt| < 24h
346327

347328
time.toComponents { hours, minutes, seconds, nanoseconds ->
348329
return DateTimePeriod((months / 12).toInt(), (months % 12).toInt(), days.toInt(), hours, minutes, seconds.toLong(), nanoseconds.toLong())
349330
}
350331
}
351332

352333
public actual fun Instant.until(other: Instant, unit: DateTimeUnit, zone: TimeZone): Long =
353-
try {
354-
// TODO: inline 'until' here and simplify operation for time-based units
355-
toZonedLocalDateTimeFailing(zone).until(other.toZonedLocalDateTimeFailing(zone), unit.calendarUnit) / unit.calendarScale
356-
} catch (e: ArithmeticException) {
357-
if (this < other) Long.MAX_VALUE else Long.MIN_VALUE
334+
when (unit) {
335+
is DateTimeUnit.DateBased ->
336+
toZonedLocalDateTimeFailing(zone).dateTime.until(other.toZonedLocalDateTimeFailing(zone).dateTime, unit)
337+
is DateTimeUnit.TimeBased -> {
338+
check(zone); other.check(zone)
339+
try {
340+
multiplyAddAndDivide(other.epochSeconds - epochSeconds,
341+
NANOS_PER_ONE.toLong(),
342+
(other.nanosecondsOfSecond - nanosecondsOfSecond).toLong(),
343+
unit.nanoseconds)
344+
} catch (e: ArithmeticException) {
345+
if (this < other) Long.MAX_VALUE else Long.MIN_VALUE
346+
}
347+
}
358348
}

core/nativeMain/src/LocalDate.kt

Lines changed: 22 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,12 @@ public actual class LocalDate actual constructor(actual val year: Int, actual va
186186
}
187187
val monthCount: Long = year * 12L + (monthNumber - 1)
188188
val calcMonths = safeAdd(monthCount, monthsToAdd)
189-
val newYear: Int = floorDiv(calcMonths, 12).toInt()
189+
val newYear = floorDiv(calcMonths, 12)
190+
if (newYear < Int.MIN_VALUE || newYear > Int.MAX_VALUE) {
191+
throw ArithmeticException("Addition overflows an Int")
192+
}
190193
val newMonth = floorMod(calcMonths, 12).toInt() + 1
191-
return resolvePreviousValid(newYear, newMonth, dayOfMonth)
194+
return resolvePreviousValid(newYear.toInt(), newMonth, dayOfMonth)
192195
}
193196

194197
// org.threeten.bp.LocalDate#plusWeeks
@@ -250,25 +253,20 @@ public actual class LocalDate actual constructor(actual val year: Int, actual va
250253
}
251254

252255
internal actual fun LocalDate.plus(value: Long, unit: CalendarUnit): LocalDate =
253-
when (unit) {
254-
CalendarUnit.YEAR, CalendarUnit.MONTH, CalendarUnit.DAY -> try {
255-
when (unit) {
256-
CalendarUnit.YEAR -> plusYears(value)
257-
CalendarUnit.MONTH -> plusMonths(value)
258-
CalendarUnit.DAY -> plusDays(value)
259-
else -> throw RuntimeException("impossible")
260-
}
261-
} catch (e: ArithmeticException) {
262-
throw DateTimeArithmeticException("Arithmetic overflow when adding a value to a date", e)
263-
} catch (e: IllegalArgumentException) {
264-
throw DateTimeArithmeticException("Boundaries of LocalDate exceeded when adding a value", e)
256+
if (unit.dateTimeUnit is DateTimeUnit.DateBased)
257+
plusDateTimeUnit(value, unit.dateTimeUnit as DateTimeUnit.DateBased)
258+
else throw IllegalArgumentException("Only date based units can be added to LocalDate")
259+
260+
internal fun LocalDate.plusDateTimeUnit(value: Long, unit: DateTimeUnit.DateBased): LocalDate =
261+
try {
262+
when (unit) {
263+
is DateTimeUnit.DateBased.DayBased -> plusDays(safeMultiply(value, unit.days.toLong()))
264+
is DateTimeUnit.DateBased.MonthBased -> plusMonths(safeMultiply(value, unit.months.toLong()))
265265
}
266-
CalendarUnit.HOUR,
267-
CalendarUnit.MINUTE,
268-
CalendarUnit.SECOND,
269-
CalendarUnit.MILLISECOND,
270-
CalendarUnit.MICROSECOND,
271-
CalendarUnit.NANOSECOND -> throw IllegalArgumentException("Only date based units can be added to LocalDate")
266+
} catch (e: ArithmeticException) {
267+
throw DateTimeArithmeticException("Arithmetic overflow when adding a value to a date", e)
268+
} catch (e: IllegalArgumentException) {
269+
throw DateTimeArithmeticException("Boundaries of LocalDate exceeded when adding a value", e)
272270
}
273271

274272
internal actual fun LocalDate.plus(value: Int, unit: CalendarUnit): LocalDate =
@@ -279,8 +277,8 @@ actual operator fun LocalDate.plus(period: DatePeriod): LocalDate =
279277
try {
280278
this@plus
281279
.run { if (years != 0 && months == 0) plusYears(years.toLong()) else this }
282-
.run { if (months != 0) this.plusMonths(years * 12L + months.toLong()) else this }
283-
.run { if (days != 0) this.plusDays(days.toLong()) else this }
280+
.run { if (months != 0) plusMonths(years * 12L + months.toLong()) else this }
281+
.run { if (days != 0) plusDays(days.toLong()) else this }
284282
} catch (e: ArithmeticException) {
285283
throw DateTimeArithmeticException("Arithmetic overflow when adding a period to a date", e)
286284
} catch (e: IllegalArgumentException) {
@@ -308,22 +306,8 @@ internal fun LocalDate.longMonthsUntil(other: LocalDate): Long {
308306
return (packed2 - packed1) / 32
309307
}
310308

311-
// org.threeten.bp.LocalDate#until(org.threeten.bp.temporal.Temporal, org.threeten.bp.temporal.TemporalUnit)
312-
internal fun LocalDate.longUntil(end: LocalDate, unit: CalendarUnit): Long =
313-
when (unit) {
314-
CalendarUnit.DAY -> longDaysUntil(end)
315-
CalendarUnit.MONTH -> longMonthsUntil(end)
316-
CalendarUnit.YEAR -> longMonthsUntil(end) / 12
317-
CalendarUnit.HOUR,
318-
CalendarUnit.MINUTE,
319-
CalendarUnit.SECOND,
320-
CalendarUnit.MILLISECOND,
321-
CalendarUnit.MICROSECOND,
322-
CalendarUnit.NANOSECOND -> throw IllegalArgumentException("Unsupported unit: $unit")
323-
}
324-
325309
actual fun LocalDate.periodUntil(other: LocalDate): DatePeriod {
326-
val months = longUntil(other, CalendarUnit.MONTH)
327-
val days = plusMonths(months).longUntil(other, CalendarUnit.DAY)
310+
val months = longMonthsUntil(other)
311+
val days = plusMonths(months).longDaysUntil(other)
328312
return DatePeriod((months / 12).toInt(), (months % 12).toInt(), days.toInt())
329313
}

core/nativeMain/src/LocalDateTime.kt

Lines changed: 21 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -71,18 +71,8 @@ public actual class LocalDateTime internal constructor(
7171
* @throws IllegalArgumentException if the result exceeds the boundaries
7272
* @throws ArithmeticException if arithmetic overflow occurs
7373
*/
74-
internal fun plusYears(years: Long): LocalDateTime = LocalDateTime(date.plusYears(years), time)
75-
/**
76-
* @throws IllegalArgumentException if the result exceeds the boundaries
77-
* @throws ArithmeticException if arithmetic overflow occurs
78-
*/
79-
internal fun plusMonths(months: Long): LocalDateTime = LocalDateTime(date.plusMonths(months), time)
80-
/**
81-
* @throws IllegalArgumentException if the result exceeds the boundaries
82-
* @throws ArithmeticException if arithmetic overflow occurs
83-
*/
84-
internal fun plusDays(days: Long): LocalDateTime = LocalDateTime(date.plusDays(days), time)
85-
74+
internal fun plus(value: Long, unit: DateTimeUnit.DateBased): LocalDateTime =
75+
LocalDateTime(date.plusDateTimeUnit(value, unit), time)
8676
}
8777

8878
actual fun Instant.toLocalDateTime(timeZone: TimeZone): LocalDateTime =
@@ -95,44 +85,26 @@ actual fun Instant.offsetAt(timeZone: TimeZone): ZoneOffset =
9585
with(timeZone) { offset }
9686

9787
// org.threeten.bp.LocalDateTime#until
98-
/** @throws ArithmeticException on arithmetic overflow. Only possible for time-based units. */
99-
internal fun LocalDateTime.until(other: LocalDateTime, unit: CalendarUnit): Long =
100-
when (unit) {
101-
CalendarUnit.YEAR, CalendarUnit.MONTH, CalendarUnit.DAY -> {
102-
var endDate: LocalDate = other.date
103-
if (endDate > date && other.time < time) {
104-
endDate = endDate.plusDays(-1) // won't throw: endDate - date >= 1
105-
} else if (endDate < date && other.time > time) {
106-
endDate = endDate.plusDays(1) // won't throw: date - endDate >= 1
107-
}
108-
date.longUntil(endDate, unit)
109-
}
110-
CalendarUnit.HOUR, CalendarUnit.MINUTE, CalendarUnit.SECOND, CalendarUnit.MILLISECOND, CalendarUnit.MICROSECOND, CalendarUnit.NANOSECOND -> {
111-
var daysUntil = date.longDaysUntil(other.date)
112-
var timeUntil: Long = other.time.toNanoOfDay() - time.toNanoOfDay()
113-
if (daysUntil > 0 && timeUntil < 0) {
114-
daysUntil--
115-
timeUntil += NANOS_PER_DAY
116-
} else if (daysUntil < 0 && timeUntil > 0) {
117-
daysUntil++
118-
timeUntil -= NANOS_PER_DAY
119-
}
120-
val nanos = timeUntil
121-
when (unit) {
122-
CalendarUnit.HOUR -> safeAdd(nanos / NANOS_PER_HOUR, safeMultiply(daysUntil, HOURS_PER_DAY.toLong()))
123-
CalendarUnit.MINUTE -> safeAdd(nanos / NANOS_PER_MINUTE,
124-
safeMultiply(daysUntil, MINUTES_PER_DAY.toLong()))
125-
CalendarUnit.SECOND -> safeAdd(nanos / NANOS_PER_ONE,
126-
safeMultiply(daysUntil, SECONDS_PER_DAY.toLong()))
127-
CalendarUnit.MILLISECOND -> safeAdd(nanos / NANOS_PER_MILLI,
128-
safeMultiply(daysUntil, SECONDS_PER_DAY.toLong() * MILLIS_PER_ONE))
129-
CalendarUnit.MICROSECOND -> safeAdd(nanos / NANOS_PER_MICRO,
130-
safeMultiply(daysUntil, SECONDS_PER_DAY.toLong() * MICROS_PER_ONE))
131-
CalendarUnit.NANOSECOND -> safeAdd(nanos, safeMultiply(daysUntil, NANOS_PER_DAY))
132-
else -> throw RuntimeException("impossible")
133-
}
134-
}
88+
internal fun LocalDateTime.until(other: LocalDateTime, unit: DateTimeUnit.DateBased): Long {
89+
var endDate: LocalDate = other.date
90+
if (endDate > date && other.time < time) {
91+
endDate = endDate.plusDays(-1) // won't throw: endDate - date >= 1
92+
} else if (endDate < date && other.time > time) {
93+
endDate = endDate.plusDays(1) // won't throw: date - endDate >= 1
94+
}
95+
return when (unit) {
96+
is DateTimeUnit.DateBased.MonthBased -> date.longMonthsUntil(endDate) / unit.months
97+
is DateTimeUnit.DateBased.DayBased -> date.longDaysUntil(endDate) / unit.days
13598
}
99+
}
100+
101+
// org.threeten.bp.LocalDateTime#until
102+
/** @throws ArithmeticException on arithmetic overflow. */
103+
internal fun LocalDateTime.until(other: LocalDateTime, unit: DateTimeUnit.TimeBased): Long {
104+
val daysUntil = date.longDaysUntil(other.date)
105+
val timeUntil: Long = other.time.toNanoOfDay() - time.toNanoOfDay()
106+
return multiplyAddAndDivide(daysUntil, NANOS_PER_DAY, timeUntil, unit.nanoseconds)
107+
}
136108

137109
// org.threeten.bp.LocalDateTime#plusSeconds
138110
/**

0 commit comments

Comments
 (0)