From 5927c75991c467a929568b3e775db2fde44ca259 Mon Sep 17 00:00:00 2001 From: Ilya Gorbunov Date: Thu, 30 Apr 2020 06:19:25 +0300 Subject: [PATCH 01/10] Add Instant.minus(Instant, ...) with parameters ... for consistency with LocalDate operations. --- core/commonMain/src/Instant.kt | 3 +++ core/commonTest/src/InstantTest.kt | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/core/commonMain/src/Instant.kt b/core/commonMain/src/Instant.kt index b58069fce..f771d2001 100644 --- a/core/commonMain/src/Instant.kt +++ b/core/commonMain/src/Instant.kt @@ -128,3 +128,6 @@ public fun Instant.yearsUntil(other: Instant, zone: TimeZone): Int = private fun Long.clampToInt(): Int = if (this > Int.MAX_VALUE) Int.MAX_VALUE else if (this < Int.MIN_VALUE) Int.MIN_VALUE else toInt() + +public fun Instant.minus(other: Instant, zone: TimeZone): CalendarPeriod = other.periodUntil(this, zone) +public fun Instant.minus(other: Instant, unit: CalendarUnit, zone: TimeZone): Long = other.until(this, unit, zone) diff --git a/core/commonTest/src/InstantTest.kt b/core/commonTest/src/InstantTest.kt index c26555edb..f63cedbe4 100644 --- a/core/commonTest/src/InstantTest.kt +++ b/core/commonTest/src/InstantTest.kt @@ -92,17 +92,21 @@ class InstantTest { checkComponents(instant2.toLocalDateTime(zone), 2019, 10, 28, 1, 59) assertEquals(24.hours, instant2 - instant1) assertEquals(24, instant1.until(instant2, CalendarUnit.HOUR, zone)) + assertEquals(24, instant2.minus(instant1, CalendarUnit.HOUR, zone)) val instant3 = instant1.plus(1, CalendarUnit.DAY, zone) checkComponents(instant3.toLocalDateTime(zone), 2019, 10, 28, 2, 59) assertEquals(25.hours, instant3 - instant1) assertEquals(1, instant1.until(instant3, CalendarUnit.DAY, zone)) assertEquals(1, instant1.daysUntil(instant3, zone)) + assertEquals(1, instant3.minus(instant1, CalendarUnit.DAY, zone)) val period = CalendarPeriod(days = 1, hours = 1) val instant4 = instant1.plus(period, zone) checkComponents(instant4.toLocalDateTime(zone), 2019, 10, 28, 3, 59) assertEquals(period, instant1.periodUntil(instant4, zone)) + assertEquals(period, instant4.minus(instant1, zone)) + assertEquals(26.hours, instant4.minus(instant1)) } @OptIn(ExperimentalTime::class) From 399d94030cab11624679dc9b71ae727f42876dbc Mon Sep 17 00:00:00 2001 From: Ilya Gorbunov Date: Tue, 16 Jun 2020 04:00:20 +0300 Subject: [PATCH 02/10] Add LocalDate.daysUntil/monthsUntil/yearsUntil/until operations --- core/commonMain/src/Instant.kt | 3 +- core/commonMain/src/LocalDate.kt | 13 ++++++ core/commonTest/src/LocalDateTest.kt | 42 +++++++++++++++++++ core/jsMain/src/LocalDate.kt | 11 ++++- core/jvmMain/src/LocalDate.kt | 11 ++++- core/nativeMain/src/LocalDate.kt | 24 +++++++---- core/nativeMain/src/LocalDateTime.kt | 4 +- .../nativeTest/src/ThreeTenBpLocalDateTest.kt | 28 ------------- 8 files changed, 94 insertions(+), 42 deletions(-) diff --git a/core/commonMain/src/Instant.kt b/core/commonMain/src/Instant.kt index f771d2001..45ce9c53c 100644 --- a/core/commonMain/src/Instant.kt +++ b/core/commonMain/src/Instant.kt @@ -126,7 +126,8 @@ public fun Instant.monthsUntil(other: Instant, zone: TimeZone): Int = public fun Instant.yearsUntil(other: Instant, zone: TimeZone): Int = until(other, CalendarUnit.YEAR, zone).clampToInt() -private fun Long.clampToInt(): Int = +// TODO: move to internal utils +internal fun Long.clampToInt(): Int = if (this > Int.MAX_VALUE) Int.MAX_VALUE else if (this < Int.MIN_VALUE) Int.MIN_VALUE else toInt() public fun Instant.minus(other: Instant, zone: TimeZone): CalendarPeriod = other.periodUntil(this, zone) diff --git a/core/commonMain/src/LocalDate.kt b/core/commonMain/src/LocalDate.kt index 65c4fa948..e37a433a6 100644 --- a/core/commonMain/src/LocalDate.kt +++ b/core/commonMain/src/LocalDate.kt @@ -58,3 +58,16 @@ expect fun LocalDate.periodUntil(other: LocalDate): CalendarPeriod /** */ operator fun LocalDate.minus(other: LocalDate): CalendarPeriod = other.periodUntil(this) + +public expect fun LocalDate.daysUntil(other: LocalDate): Int +public expect fun LocalDate.monthsUntil(other: LocalDate): Int +public expect fun LocalDate.yearsUntil(other: LocalDate): Int + +public fun LocalDate.until(other: LocalDate, unit: CalendarUnit): Int = when(unit) { + CalendarUnit.YEAR -> yearsUntil(other) + CalendarUnit.MONTH -> monthsUntil(other) + CalendarUnit.WEEK -> daysUntil(other) / 7 + CalendarUnit.DAY -> daysUntil(other) + CalendarUnit.HOUR, CalendarUnit.MINUTE, CalendarUnit.SECOND, CalendarUnit.NANOSECOND -> + throw UnsupportedOperationException("Only date based units can be used to express difference between LocalDate values.") +} \ No newline at end of file diff --git a/core/commonTest/src/LocalDateTest.kt b/core/commonTest/src/LocalDateTest.kt index 3bd5bff70..ce9873ae2 100644 --- a/core/commonTest/src/LocalDateTest.kt +++ b/core/commonTest/src/LocalDateTest.kt @@ -96,6 +96,48 @@ class LocalDateTest { } } + // based on threetenbp test for until() + @Test + fun until() { + val data = listOf( + Pair(Pair("2012-06-30", "2012-06-30"), Pair(CalendarUnit.DAY, 0)), + Pair(Pair("2012-06-30", "2012-06-30"), Pair(CalendarUnit.WEEK, 0)), + Pair(Pair("2012-06-30", "2012-06-30"), Pair(CalendarUnit.MONTH, 0)), + Pair(Pair("2012-06-30", "2012-06-30"), Pair(CalendarUnit.YEAR, 0)), + Pair(Pair("2012-06-30", "2012-07-01"), Pair(CalendarUnit.DAY, 1)), + Pair(Pair("2012-06-30", "2012-07-01"), Pair(CalendarUnit.WEEK, 0)), + Pair(Pair("2012-06-30", "2012-07-01"), Pair(CalendarUnit.MONTH, 0)), + Pair(Pair("2012-06-30", "2012-07-01"), Pair(CalendarUnit.YEAR, 0)), + Pair(Pair("2012-06-30", "2012-07-07"), Pair(CalendarUnit.DAY, 7)), + Pair(Pair("2012-06-30", "2012-07-07"), Pair(CalendarUnit.WEEK, 1)), + Pair(Pair("2012-06-30", "2012-07-07"), Pair(CalendarUnit.MONTH, 0)), + Pair(Pair("2012-06-30", "2012-07-07"), Pair(CalendarUnit.YEAR, 0)), + Pair(Pair("2012-06-30", "2012-07-29"), Pair(CalendarUnit.MONTH, 0)), + Pair(Pair("2012-06-30", "2012-07-30"), Pair(CalendarUnit.MONTH, 1)), + Pair(Pair("2012-06-30", "2012-07-31"), Pair(CalendarUnit.MONTH, 1))) + for ((values, interval) in data) { + val (v1, v2) = values + val (unit, length) = interval + val start = LocalDate.parse(v1) + val end = LocalDate.parse(v2) + assertEquals(length, start.until(end, unit), "$v2 - $v1 = $length($unit)") + assertEquals(-length, end.until(start, unit), "$v1 - $v2 = -$length($unit)") + @Suppress("NON_EXHAUSTIVE_WHEN") + when (unit) { + CalendarUnit.YEAR -> assertEquals(length, start.yearsUntil(end)) + CalendarUnit.MONTH -> assertEquals(length, start.monthsUntil(end)) + CalendarUnit.WEEK -> assertEquals(length, start.daysUntil(end) / 7) + CalendarUnit.DAY -> assertEquals(length, start.daysUntil(end)) + } + } + + val d1 = LocalDate(2012, 6, 21) + val d2 = LocalDate(2012, 7, 21) + + for (unit in listOf(CalendarUnit.HOUR, CalendarUnit.MINUTE, CalendarUnit.SECOND, CalendarUnit.NANOSECOND)) + assertFailsWith { d1.until(d2, unit) } + } + @Test fun invalidDate() { assertFailsWith { LocalDate(2007, 2, 29) } diff --git a/core/jsMain/src/LocalDate.kt b/core/jsMain/src/LocalDate.kt index 53376b482..8e8096218 100644 --- a/core/jsMain/src/LocalDate.kt +++ b/core/jsMain/src/LocalDate.kt @@ -78,4 +78,13 @@ public actual fun LocalDate.periodUntil(other: LocalDate): CalendarPeriod { val days = startD.until(endD, ChronoUnit.DAYS).toInt() return CalendarPeriod(months / 12, months % 12, days) -} \ No newline at end of file +} + +public actual fun LocalDate.daysUntil(other: LocalDate): Int = + this.value.until(other.value, ChronoUnit.DAYS).toInt() + +public actual fun LocalDate.monthsUntil(other: LocalDate): Int = + this.value.until(other.value, ChronoUnit.MONTHS).toInt() + +public actual fun LocalDate.yearsUntil(other: LocalDate): Int = + this.value.until(other.value, ChronoUnit.YEARS).toInt() \ No newline at end of file diff --git a/core/jvmMain/src/LocalDate.kt b/core/jvmMain/src/LocalDate.kt index 20c903590..bfc628057 100644 --- a/core/jvmMain/src/LocalDate.kt +++ b/core/jvmMain/src/LocalDate.kt @@ -73,4 +73,13 @@ public actual fun LocalDate.periodUntil(other: LocalDate): CalendarPeriod { val days = startD.until(endD, ChronoUnit.DAYS) return CalendarPeriod((months / 12).toInt(), (months % 12).toInt(), days.toInt()) -} \ No newline at end of file +} + +public actual fun LocalDate.daysUntil(other: LocalDate): Int = + this.value.until(other.value, ChronoUnit.DAYS).clampToInt() + +public actual fun LocalDate.monthsUntil(other: LocalDate): Int = + this.value.until(other.value, ChronoUnit.MONTHS).clampToInt() + +public actual fun LocalDate.yearsUntil(other: LocalDate): Int = + this.value.until(other.value, ChronoUnit.YEARS).clampToInt() \ No newline at end of file diff --git a/core/nativeMain/src/LocalDate.kt b/core/nativeMain/src/LocalDate.kt index d0564099c..50cff3356 100644 --- a/core/nativeMain/src/LocalDate.kt +++ b/core/nativeMain/src/LocalDate.kt @@ -291,27 +291,33 @@ actual operator fun LocalDate.plus(period: CalendarPeriod): LocalDate = } } + +// TODO: ensure range of LocalDate fits in Int number of days +public actual fun LocalDate.daysUntil(other: LocalDate): Int = longDaysUntil(other).toInt() +public actual fun LocalDate.monthsUntil(other: LocalDate): Int = longMonthsUntil(other).toInt() +public actual fun LocalDate.yearsUntil(other: LocalDate): Int = (longMonthsUntil(other) / 12).toInt() + // org.threeten.bp.LocalDate#daysUntil -internal fun LocalDate.daysUntil(other: LocalDate): Long = +internal fun LocalDate.longDaysUntil(other: LocalDate): Long = other.toEpochDay() - this.toEpochDay() // org.threeten.bp.LocalDate#getProlepticMonth internal val LocalDate.prolepticMonth get() = (year * 12L) + (monthNumber - 1) // org.threeten.bp.LocalDate#monthsUntil -internal fun LocalDate.monthsUntil(other: LocalDate): Long { +internal fun LocalDate.longMonthsUntil(other: LocalDate): Long { val packed1: Long = prolepticMonth * 32L + dayOfMonth val packed2: Long = other.prolepticMonth * 32L + other.dayOfMonth return (packed2 - packed1) / 32 } // org.threeten.bp.LocalDate#until(org.threeten.bp.temporal.Temporal, org.threeten.bp.temporal.TemporalUnit) -internal fun LocalDate.until(end: LocalDate, unit: CalendarUnit): Long = +internal fun LocalDate.longUntil(end: LocalDate, unit: CalendarUnit): Long = when (unit) { - CalendarUnit.DAY -> daysUntil(end) - CalendarUnit.WEEK -> daysUntil(end) / 7 - CalendarUnit.MONTH -> monthsUntil(end) - CalendarUnit.YEAR -> monthsUntil(end) / 12 + CalendarUnit.DAY -> longDaysUntil(end) + CalendarUnit.WEEK -> longDaysUntil(end) / 7 + CalendarUnit.MONTH -> longMonthsUntil(end) + CalendarUnit.YEAR -> longMonthsUntil(end) / 12 CalendarUnit.HOUR, CalendarUnit.MINUTE, CalendarUnit.SECOND, @@ -319,7 +325,7 @@ internal fun LocalDate.until(end: LocalDate, unit: CalendarUnit): Long = } actual fun LocalDate.periodUntil(other: LocalDate): CalendarPeriod { - val months = until(other, CalendarUnit.MONTH) - val days = plusMonths(months).until(other, CalendarUnit.DAY) + val months = longUntil(other, CalendarUnit.MONTH) + val days = plusMonths(months).longUntil(other, CalendarUnit.DAY) return CalendarPeriod((months / 12).toInt(), (months % 12).toInt(), days.toInt()) } diff --git a/core/nativeMain/src/LocalDateTime.kt b/core/nativeMain/src/LocalDateTime.kt index 7199a0fba..1acea5797 100644 --- a/core/nativeMain/src/LocalDateTime.kt +++ b/core/nativeMain/src/LocalDateTime.kt @@ -105,10 +105,10 @@ internal fun LocalDateTime.until(other: LocalDateTime, unit: CalendarUnit): Long } else if (endDate < date && other.time > time) { endDate = endDate.plusDays(1) // won't throw: date - endDate >= 1 } - date.until(endDate, unit) + date.longUntil(endDate, unit) } CalendarUnit.HOUR, CalendarUnit.MINUTE, CalendarUnit.SECOND, CalendarUnit.NANOSECOND -> { - var daysUntil = date.daysUntil(other.date) + var daysUntil = date.longDaysUntil(other.date) var timeUntil: Long = other.time.toNanoOfDay() - time.toNanoOfDay() if (daysUntil > 0 && timeUntil < 0) { daysUntil-- diff --git a/core/nativeTest/src/ThreeTenBpLocalDateTest.kt b/core/nativeTest/src/ThreeTenBpLocalDateTest.kt index eda0ec3af..593809053 100644 --- a/core/nativeTest/src/ThreeTenBpLocalDateTest.kt +++ b/core/nativeTest/src/ThreeTenBpLocalDateTest.kt @@ -131,34 +131,6 @@ class ThreeTenBpLocalDateTest { assertEquals(LocalDate(2005, 7, 15), date.plusDays(-730)) } - @Test - fun until() { - val data = arrayOf( - Pair(Pair("2012-06-30", "2012-06-30"), Pair(CalendarUnit.DAY, 0)), - Pair(Pair("2012-06-30", "2012-06-30"), Pair(CalendarUnit.WEEK, 0)), - Pair(Pair("2012-06-30", "2012-06-30"), Pair(CalendarUnit.MONTH, 0)), - Pair(Pair("2012-06-30", "2012-06-30"), Pair(CalendarUnit.YEAR, 0)), - Pair(Pair("2012-06-30", "2012-07-01"), Pair(CalendarUnit.DAY, 1)), - Pair(Pair("2012-06-30", "2012-07-01"), Pair(CalendarUnit.WEEK, 0)), - Pair(Pair("2012-06-30", "2012-07-01"), Pair(CalendarUnit.MONTH, 0)), - Pair(Pair("2012-06-30", "2012-07-01"), Pair(CalendarUnit.YEAR, 0)), - Pair(Pair("2012-06-30", "2012-07-07"), Pair(CalendarUnit.DAY, 7)), - Pair(Pair("2012-06-30", "2012-07-07"), Pair(CalendarUnit.WEEK, 1)), - Pair(Pair("2012-06-30", "2012-07-07"), Pair(CalendarUnit.MONTH, 0)), - Pair(Pair("2012-06-30", "2012-07-07"), Pair(CalendarUnit.YEAR, 0)), - Pair(Pair("2012-06-30", "2012-07-29"), Pair(CalendarUnit.MONTH, 0)), - Pair(Pair("2012-06-30", "2012-07-30"), Pair(CalendarUnit.MONTH, 1)), - Pair(Pair("2012-06-30", "2012-07-31"), Pair(CalendarUnit.MONTH, 1))) - for ((values, interval) in data) { - val (v1, v2) = values - val (unit, length) = interval - val start = LocalDate.parse(v1) - val end = LocalDate.parse(v2) - assertEquals(length, start.until(end, unit).toInt(), "$v2 - $v1 = $length($unit)") - assertEquals(-length, end.until(start, unit).toInt(), "$v1 - $v2 = -$length($unit)") - } - } - @Test fun strings() { val data = arrayOf( From 200a4341585349ade5372a7f78f1b5443c5a50d8 Mon Sep 17 00:00:00 2001 From: Ilya Gorbunov Date: Tue, 23 Jun 2020 17:09:18 +0300 Subject: [PATCH 03/10] Remove questionable ways to construct CalendarPeriod --- core/commonMain/src/CalendarPeriod.kt | 13 +------------ core/commonTest/src/CalendarPeriodTest.kt | 6 +++--- core/commonTest/src/LocalDateTest.kt | 19 +++++++++---------- 3 files changed, 13 insertions(+), 25 deletions(-) diff --git a/core/commonMain/src/CalendarPeriod.kt b/core/commonMain/src/CalendarPeriod.kt index 947e64db5..8f1325b88 100644 --- a/core/commonMain/src/CalendarPeriod.kt +++ b/core/commonMain/src/CalendarPeriod.kt @@ -10,13 +10,9 @@ import kotlin.time.ExperimentalTime typealias Period = CalendarPeriod +// TODO: could be error-prone without explicitly named params class CalendarPeriod(val years: Int = 0, val months: Int = 0, val days: Int = 0, val hours: Int = 0, val minutes: Int = 0, val seconds: Long = 0, val nanoseconds: Long = 0) { - object Builder { - val Int.years get() = CalendarPeriod(years = this) - val Int.months get() = CalendarPeriod(months = this) - val Int.days get() = CalendarPeriod(days = this) - } private fun allNotPositive() = years <= 0 && months <= 0 && days <= 0 && hours <= 0 && minutes <= 0 && seconds <= 0 && nanoseconds <= 0 && @@ -68,13 +64,6 @@ class CalendarPeriod(val years: Int = 0, val months: Int = 0, val days: Int = 0, } } -inline fun period(builder: CalendarPeriod.Builder.() -> CalendarPeriod): CalendarPeriod = CalendarPeriod.Builder.builder() - -val Int.calendarDays: CalendarPeriod get() = CalendarPeriod(days = this) -val Int.calendarMonths: CalendarPeriod get() = CalendarPeriod(months = this) -val Int.calendarYears: CalendarPeriod get() = CalendarPeriod(years = this) - - @OptIn(ExperimentalTime::class) fun Duration.toCalendarPeriod(): CalendarPeriod = toComponents { hours, minutes, seconds, nanoseconds -> CalendarPeriod(hours = hours, minutes = minutes, seconds = seconds.toLong(), nanoseconds = nanoseconds.toLong()) diff --git a/core/commonTest/src/CalendarPeriodTest.kt b/core/commonTest/src/CalendarPeriodTest.kt index f717b25e9..22e9986f8 100644 --- a/core/commonTest/src/CalendarPeriodTest.kt +++ b/core/commonTest/src/CalendarPeriodTest.kt @@ -13,10 +13,10 @@ class CalendarPeriodTest { @Test fun toStringConversion() { - assertEquals("P1Y", 1.calendarYears.toString()) + assertEquals("P1Y", CalendarPeriod(years = 1).toString()) assertEquals("P1Y1M", CalendarPeriod(years = 1, months = 1).toString()) - assertEquals("P11M", 11.calendarMonths.toString()) - assertEquals("P14M", 14.calendarMonths.toString()) // TODO: normalize or not + assertEquals("P11M", CalendarPeriod(months = 11).toString()) + assertEquals("P14M", CalendarPeriod(months = 14).toString()) // TODO: normalize or not assertEquals("P10M5D", CalendarPeriod(months = 10, days = 5).toString()) assertEquals("P1Y40D", CalendarPeriod(years = 1, days = 40).toString()) diff --git a/core/commonTest/src/LocalDateTest.kt b/core/commonTest/src/LocalDateTest.kt index ce9873ae2..c048dba7e 100644 --- a/core/commonTest/src/LocalDateTest.kt +++ b/core/commonTest/src/LocalDateTest.kt @@ -59,11 +59,11 @@ class LocalDateTest { @Test fun addComponents() { val startDate = LocalDate(2016, 2, 29) - checkComponents(startDate + 1.calendarDays, 2016, 3, 1) - checkComponents(startDate + 1.calendarYears, 2017, 2, 28) - checkComponents(startDate + 4.calendarYears, 2020, 2, 29) + checkComponents(startDate.plus(1, CalendarUnit.DAY), 2016, 3, 1) + checkComponents(startDate.plus(1, CalendarUnit.YEAR), 2017, 2, 28) + checkComponents(startDate + CalendarPeriod(years = 4), 2020, 2, 29) - checkComponents(LocalDate.parse("2016-01-31") + 1.calendarMonths, 2016, 2, 29) + checkComponents(LocalDate.parse("2016-01-31") + CalendarPeriod(months = 1), 2016, 2, 29) assertFailsWith { startDate + CalendarPeriod(hours = 7) } assertFailsWith { startDate.plus(7, CalendarUnit.HOUR) } @@ -73,10 +73,9 @@ class LocalDateTest { fun tomorrow() { val today = Clock.System.todayAt(TimeZone.SYSTEM) - val nextMonthPlusDay1 = today + 1.calendarMonths + 1.calendarDays - val nextMonthPlusDay2 = today + (1.calendarMonths + 1.calendarDays) - val nextMonthPlusDay3 = today + 1.calendarDays + 1.calendarMonths - + val nextMonthPlusDay1 = today.plus(1, CalendarUnit.MONTH).plus(1, CalendarUnit.DAY) + val nextMonthPlusDay2 = today + CalendarPeriod(months = 1, days = 1) + val nextMonthPlusDay3 = today.plus(1, CalendarUnit.DAY).plus(1, CalendarUnit.MONTH) } @Test @@ -86,8 +85,8 @@ class LocalDateTest { repeat(1000) { val days1 = Random.nextInt(-3652..3652) val days2 = Random.nextInt(-3652..3652) - val ldtBefore = origin + days1.calendarDays - val ldtNow = origin + days2.calendarDays + val ldtBefore = origin + CalendarPeriod(days = days1) + val ldtNow = origin.plus(days2, CalendarUnit.DAY) val diff = ldtNow - ldtBefore val ldtAfter = ldtBefore + diff From 4de15c3665c5ba8c64a00a24404c87fccdae35a2 Mon Sep 17 00:00:00 2001 From: Ilya Gorbunov Date: Wed, 1 Jul 2020 23:11:38 +0300 Subject: [PATCH 04/10] Experiment with extensible ChronoUnit instead of fixed enum of CalendarUnits --- core/commonMain/src/ChronoUnit.kt | 41 ++++++++++++++++++++++++++++ core/commonMain/src/Instant.kt | 12 ++++++++ core/commonMain/src/LocalDate.kt | 16 ++++++++++- core/commonTest/src/InstantTest.kt | 23 +++++++++++----- core/commonTest/src/LocalDateTest.kt | 5 ++-- 5 files changed, 87 insertions(+), 10 deletions(-) create mode 100644 core/commonMain/src/ChronoUnit.kt diff --git a/core/commonMain/src/ChronoUnit.kt b/core/commonMain/src/ChronoUnit.kt new file mode 100644 index 000000000..9a649f9be --- /dev/null +++ b/core/commonMain/src/ChronoUnit.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2019-2020 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime + + +enum class TimeComponent { + MONTH, + DAY, + NANOSECOND +} + +class ChronoUnit(val scale: Long, val component: TimeComponent) { + init { + require(scale > 0) { "Unit scale must be positive, but was $scale" } + } + constructor(number: Long, unit: ChronoUnit) : this(number * unit.scale, unit.component) + // it seems possible to provide 'times' operation + companion object { + val NANOSECOND = ChronoUnit(1, TimeComponent.NANOSECOND) + val MICROSECOND = ChronoUnit(1000, NANOSECOND) + val MILLISECOND = ChronoUnit(1000, MICROSECOND) + val SECOND = ChronoUnit(1000, MILLISECOND) + val MINUTE = ChronoUnit(60, SECOND) + val HOUR = ChronoUnit(60, MINUTE) + val DAY = ChronoUnit(1, TimeComponent.DAY) + val WEEK = ChronoUnit(7, DAY) + val MONTH = ChronoUnit(1, TimeComponent.MONTH) + val QUARTER = ChronoUnit(3, MONTH) + val YEAR = ChronoUnit(12, MONTH) + val CENTURY = ChronoUnit(100, YEAR) + } +} + +internal fun TimeComponent.toCalendarUnit(): CalendarUnit = when(this) { + TimeComponent.MONTH -> CalendarUnit.MONTH + TimeComponent.DAY -> CalendarUnit.DAY + TimeComponent.NANOSECOND -> CalendarUnit.NANOSECOND +} diff --git a/core/commonMain/src/Instant.kt b/core/commonMain/src/Instant.kt index 45ce9c53c..387971f6f 100644 --- a/core/commonMain/src/Instant.kt +++ b/core/commonMain/src/Instant.kt @@ -132,3 +132,15 @@ internal fun Long.clampToInt(): Int = public fun Instant.minus(other: Instant, zone: TimeZone): CalendarPeriod = other.periodUntil(this, zone) public fun Instant.minus(other: Instant, unit: CalendarUnit, zone: TimeZone): Long = other.until(this, unit, zone) + + + +public fun Instant.plus(unit: ChronoUnit, zone: TimeZone): Instant = + plus(unit.scale, unit.component.toCalendarUnit(), zone) +public fun Instant.plus(value: Int, unit: ChronoUnit, zone: TimeZone): Instant = + plus(value * unit.scale, unit.component.toCalendarUnit(), zone) +public fun Instant.plus(value: Long, unit: ChronoUnit, zone: TimeZone): Instant = + plus(value * unit.scale, unit.component.toCalendarUnit(), zone) + +public fun Instant.until(other: Instant, unit: ChronoUnit, zone: TimeZone): Long = + until(other, unit.component.toCalendarUnit(), zone) / unit.scale \ No newline at end of file diff --git a/core/commonMain/src/LocalDate.kt b/core/commonMain/src/LocalDate.kt index e37a433a6..6a9b68554 100644 --- a/core/commonMain/src/LocalDate.kt +++ b/core/commonMain/src/LocalDate.kt @@ -70,4 +70,18 @@ public fun LocalDate.until(other: LocalDate, unit: CalendarUnit): Int = when(uni CalendarUnit.DAY -> daysUntil(other) CalendarUnit.HOUR, CalendarUnit.MINUTE, CalendarUnit.SECOND, CalendarUnit.NANOSECOND -> throw UnsupportedOperationException("Only date based units can be used to express difference between LocalDate values.") -} \ No newline at end of file +} + +public fun LocalDate.plus(unit: ChronoUnit): LocalDate = + plus(unit.scale, unit.component.toCalendarUnit()) +public fun LocalDate.plus(value: Int, unit: ChronoUnit): LocalDate = + plus(value * unit.scale, unit.component.toCalendarUnit()) +public fun LocalDate.plus(value: Long, unit: ChronoUnit): LocalDate = + plus(value * unit.scale, unit.component.toCalendarUnit()) + +public fun LocalDate.until(other: LocalDate, unit: ChronoUnit): Int = when(unit.component) { + TimeComponent.MONTH -> (monthsUntil(other) / unit.scale).toInt() + TimeComponent.DAY -> (daysUntil(other) / unit.scale).toInt() + TimeComponent.NANOSECOND -> + throw UnsupportedOperationException("Only date based units can be used to express difference between LocalDate values.") +} diff --git a/core/commonTest/src/InstantTest.kt b/core/commonTest/src/InstantTest.kt index f63cedbe4..9b32f00a5 100644 --- a/core/commonTest/src/InstantTest.kt +++ b/core/commonTest/src/InstantTest.kt @@ -94,19 +94,28 @@ class InstantTest { assertEquals(24, instant1.until(instant2, CalendarUnit.HOUR, zone)) assertEquals(24, instant2.minus(instant1, CalendarUnit.HOUR, zone)) - val instant3 = instant1.plus(1, CalendarUnit.DAY, zone) + val instant3 = instant1.plus(ChronoUnit.DAY, zone) checkComponents(instant3.toLocalDateTime(zone), 2019, 10, 28, 2, 59) assertEquals(25.hours, instant3 - instant1) assertEquals(1, instant1.until(instant3, CalendarUnit.DAY, zone)) assertEquals(1, instant1.daysUntil(instant3, zone)) assertEquals(1, instant3.minus(instant1, CalendarUnit.DAY, zone)) + val instant4 = instant1.plus(14, ChronoUnit.MONTH, zone) + checkComponents(instant4.toLocalDateTime(zone), 2020, 12, 27, 2, 59) + assertEquals(1, instant1.until(instant4, ChronoUnit.YEAR, zone)) + assertEquals(4, instant1.until(instant4, ChronoUnit.QUARTER, zone)) + assertEquals(14, instant1.until(instant4, ChronoUnit.MONTH, zone)) + assertEquals(61, instant1.until(instant4, ChronoUnit.WEEK, zone)) + assertEquals(366 + 31 + 30, instant1.until(instant4, ChronoUnit.DAY, zone)) + assertEquals((366 + 31 + 30) * 24 + 1, instant1.until(instant4, ChronoUnit.HOUR, zone)) + val period = CalendarPeriod(days = 1, hours = 1) - val instant4 = instant1.plus(period, zone) - checkComponents(instant4.toLocalDateTime(zone), 2019, 10, 28, 3, 59) - assertEquals(period, instant1.periodUntil(instant4, zone)) - assertEquals(period, instant4.minus(instant1, zone)) - assertEquals(26.hours, instant4.minus(instant1)) + 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)) } @OptIn(ExperimentalTime::class) @@ -219,7 +228,7 @@ class InstantTest { /* Based on the ThreeTenBp project. * Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos */ - @ExperimentalTime +// @ExperimentalTime @Test fun strings() { assertEquals("0000-01-02T00:00:00Z", LocalDateTime(0, 1, 2, 0, 0, 0, 0).toInstant(TimeZone.UTC).toString()) diff --git a/core/commonTest/src/LocalDateTest.kt b/core/commonTest/src/LocalDateTest.kt index c048dba7e..9a3a72bc1 100644 --- a/core/commonTest/src/LocalDateTest.kt +++ b/core/commonTest/src/LocalDateTest.kt @@ -59,14 +59,15 @@ class LocalDateTest { @Test fun addComponents() { val startDate = LocalDate(2016, 2, 29) - checkComponents(startDate.plus(1, CalendarUnit.DAY), 2016, 3, 1) - checkComponents(startDate.plus(1, CalendarUnit.YEAR), 2017, 2, 28) + checkComponents(startDate.plus(1, ChronoUnit.DAY), 2016, 3, 1) + checkComponents(startDate.plus(ChronoUnit.YEAR), 2017, 2, 28) checkComponents(startDate + CalendarPeriod(years = 4), 2020, 2, 29) checkComponents(LocalDate.parse("2016-01-31") + CalendarPeriod(months = 1), 2016, 2, 29) assertFailsWith { startDate + CalendarPeriod(hours = 7) } assertFailsWith { startDate.plus(7, CalendarUnit.HOUR) } + assertFailsWith { startDate.plus(7, ChronoUnit.MINUTE) } } @Test From 5cd7e63809cc467a82b8594f273f27160353d932 Mon Sep 17 00:00:00 2001 From: Ilya Gorbunov Date: Sat, 4 Jul 2020 03:42:32 +0300 Subject: [PATCH 05/10] ChronoUnit->DateTimeUnit: sealed hierarchy of time-based and date-based units --- core/commonMain/src/ChronoUnit.kt | 41 ----------- core/commonMain/src/DateTimeUnit.kt | 106 +++++++++++++++++++++++++++ core/commonMain/src/Instant.kt | 20 ++--- core/commonMain/src/LocalDate.kt | 20 +++-- core/commonTest/src/InstantTest.kt | 20 +++-- core/commonTest/src/LocalDateTest.kt | 46 ++++++------ 6 files changed, 161 insertions(+), 92 deletions(-) delete mode 100644 core/commonMain/src/ChronoUnit.kt create mode 100644 core/commonMain/src/DateTimeUnit.kt diff --git a/core/commonMain/src/ChronoUnit.kt b/core/commonMain/src/ChronoUnit.kt deleted file mode 100644 index 9a649f9be..000000000 --- a/core/commonMain/src/ChronoUnit.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2019-2020 JetBrains s.r.o. - * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. - */ - -package kotlinx.datetime - - -enum class TimeComponent { - MONTH, - DAY, - NANOSECOND -} - -class ChronoUnit(val scale: Long, val component: TimeComponent) { - init { - require(scale > 0) { "Unit scale must be positive, but was $scale" } - } - constructor(number: Long, unit: ChronoUnit) : this(number * unit.scale, unit.component) - // it seems possible to provide 'times' operation - companion object { - val NANOSECOND = ChronoUnit(1, TimeComponent.NANOSECOND) - val MICROSECOND = ChronoUnit(1000, NANOSECOND) - val MILLISECOND = ChronoUnit(1000, MICROSECOND) - val SECOND = ChronoUnit(1000, MILLISECOND) - val MINUTE = ChronoUnit(60, SECOND) - val HOUR = ChronoUnit(60, MINUTE) - val DAY = ChronoUnit(1, TimeComponent.DAY) - val WEEK = ChronoUnit(7, DAY) - val MONTH = ChronoUnit(1, TimeComponent.MONTH) - val QUARTER = ChronoUnit(3, MONTH) - val YEAR = ChronoUnit(12, MONTH) - val CENTURY = ChronoUnit(100, YEAR) - } -} - -internal fun TimeComponent.toCalendarUnit(): CalendarUnit = when(this) { - TimeComponent.MONTH -> CalendarUnit.MONTH - TimeComponent.DAY -> CalendarUnit.DAY - TimeComponent.NANOSECOND -> CalendarUnit.NANOSECOND -} diff --git a/core/commonMain/src/DateTimeUnit.kt b/core/commonMain/src/DateTimeUnit.kt new file mode 100644 index 000000000..4bc20a8c5 --- /dev/null +++ b/core/commonMain/src/DateTimeUnit.kt @@ -0,0 +1,106 @@ +/* + * Copyright 2019-2020 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime + +import kotlin.time.Duration +import kotlin.time.ExperimentalTime +import kotlin.time.nanoseconds + +// TODO: toString +sealed class DateTimeUnit { + + abstract operator fun times(scalar: Int): DateTimeUnit + + internal abstract val calendarUnit: CalendarUnit + internal abstract val calendarScale: Long + + class TimeBased(val nanoseconds: Long) : DateTimeUnit() { + internal override val calendarUnit: CalendarUnit + internal override val calendarScale: Long + + init { + require(nanoseconds > 0) { "Unit duration must be positive, but was $nanoseconds ns." } + when { + nanoseconds % 3600_000_000_000 == 0L -> { + calendarUnit = CalendarUnit.HOUR + calendarScale = nanoseconds / 3600_000_000_000 + } + nanoseconds % 60_000_000_000 == 0L -> { + calendarUnit = CalendarUnit.MINUTE + calendarScale = nanoseconds / 60_000_000_000 + } + nanoseconds % 1_000_000_000 == 0L -> { + calendarUnit = CalendarUnit.SECOND + calendarScale = nanoseconds / 1_000_000_000 + } + else -> { + calendarUnit = CalendarUnit.NANOSECOND + calendarScale = nanoseconds + } + } + } + + override fun times(scalar: Int): TimeBased = TimeBased(nanoseconds * scalar) // TODO: prevent overflow + + @ExperimentalTime + val duration: Duration = nanoseconds.nanoseconds + + override fun equals(other: Any?): Boolean = + this === other || (other is TimeBased && this.nanoseconds == other.nanoseconds) + + override fun hashCode(): Int = nanoseconds.toInt() xor (nanoseconds shr Int.SIZE_BITS).toInt() + } + + sealed class DateBased : DateTimeUnit() { + // TODO: investigate how to move subclasses to ChronoUnit scope + class DayBased(val days: Int) : DateBased() { + init { + require(days > 0) { "Unit duration must be positive, but was $days days." } + } + + override fun times(scalar: Int): DayBased = DayBased(days * scalar) + + internal override val calendarUnit: CalendarUnit get() = CalendarUnit.DAY + internal override val calendarScale: Long get() = days.toLong() + + override fun equals(other: Any?): Boolean = + this === other || (other is DayBased && this.days == other.days) + + override fun hashCode(): Int = days xor 0x10000 + } + class MonthBased(val months: Int) : DateBased() { + init { + require(months > 0) { "Unit duration must be positive, but was $months months." } + } + + override fun times(scalar: Int): MonthBased = MonthBased(months * scalar) + + internal override val calendarUnit: CalendarUnit get() = CalendarUnit.MONTH + internal override val calendarScale: Long get() = months.toLong() + + override fun equals(other: Any?): Boolean = + this === other || (other is MonthBased && this.months == other.months) + + override fun hashCode(): Int = months xor 0x20000 + } + } + + + companion object { + val NANOSECOND = TimeBased(nanoseconds = 1) + val MICROSECOND = NANOSECOND * 1000 + val MILLISECOND = MICROSECOND * 1000 + val SECOND = MILLISECOND * 1000 + val MINUTE = SECOND * 60 + val HOUR = MINUTE * 60 + val DAY = DateBased.DayBased(days = 1) + val WEEK = DAY * 7 + val MONTH = DateBased.MonthBased(months = 1) + val QUARTER = MONTH * 3 + val YEAR = MONTH * 12 + val CENTURY = YEAR * 100 + } +} diff --git a/core/commonMain/src/Instant.kt b/core/commonMain/src/Instant.kt index 387971f6f..bed6b9f5e 100644 --- a/core/commonMain/src/Instant.kt +++ b/core/commonMain/src/Instant.kt @@ -135,12 +135,14 @@ public fun Instant.minus(other: Instant, unit: CalendarUnit, zone: TimeZone): Lo -public fun Instant.plus(unit: ChronoUnit, zone: TimeZone): Instant = - plus(unit.scale, unit.component.toCalendarUnit(), zone) -public fun Instant.plus(value: Int, unit: ChronoUnit, zone: TimeZone): Instant = - plus(value * unit.scale, unit.component.toCalendarUnit(), zone) -public fun Instant.plus(value: Long, unit: ChronoUnit, zone: TimeZone): Instant = - plus(value * unit.scale, unit.component.toCalendarUnit(), zone) - -public fun Instant.until(other: Instant, unit: ChronoUnit, zone: TimeZone): Long = - until(other, unit.component.toCalendarUnit(), zone) / unit.scale \ No newline at end of file +public fun Instant.plus(unit: DateTimeUnit, zone: TimeZone): Instant = + plus(unit.calendarScale, unit.calendarUnit, zone) +public fun Instant.plus(value: Int, unit: DateTimeUnit, zone: TimeZone): Instant = + plus(value * unit.calendarScale, unit.calendarUnit, zone) +public fun Instant.plus(value: Long, unit: DateTimeUnit, zone: TimeZone): Instant = + plus(value * unit.calendarScale, unit.calendarUnit, zone) + +public fun Instant.until(other: Instant, unit: DateTimeUnit, zone: TimeZone): Long = + until(other, unit.calendarUnit, zone) / unit.calendarScale + +public fun Instant.minus(other: Instant, unit: DateTimeUnit, zone: TimeZone): Long = other.until(this, unit, zone) diff --git a/core/commonMain/src/LocalDate.kt b/core/commonMain/src/LocalDate.kt index 6a9b68554..e4b678a8f 100644 --- a/core/commonMain/src/LocalDate.kt +++ b/core/commonMain/src/LocalDate.kt @@ -72,16 +72,14 @@ public fun LocalDate.until(other: LocalDate, unit: CalendarUnit): Int = when(uni throw UnsupportedOperationException("Only date based units can be used to express difference between LocalDate values.") } -public fun LocalDate.plus(unit: ChronoUnit): LocalDate = - plus(unit.scale, unit.component.toCalendarUnit()) -public fun LocalDate.plus(value: Int, unit: ChronoUnit): LocalDate = - plus(value * unit.scale, unit.component.toCalendarUnit()) -public fun LocalDate.plus(value: Long, unit: ChronoUnit): LocalDate = - plus(value * unit.scale, unit.component.toCalendarUnit()) +public fun LocalDate.plus(unit: DateTimeUnit.DateBased): LocalDate = + plus(unit.calendarScale, unit.calendarUnit) +public fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): LocalDate = + plus(value * unit.calendarScale, unit.calendarUnit) +public fun LocalDate.plus(value: Long, unit: DateTimeUnit.DateBased): LocalDate = + plus(value * unit.calendarScale, unit.calendarUnit) -public fun LocalDate.until(other: LocalDate, unit: ChronoUnit): Int = when(unit.component) { - TimeComponent.MONTH -> (monthsUntil(other) / unit.scale).toInt() - TimeComponent.DAY -> (daysUntil(other) / unit.scale).toInt() - TimeComponent.NANOSECOND -> - throw UnsupportedOperationException("Only date based units can be used to express difference between LocalDate values.") +public fun LocalDate.until(other: LocalDate, unit: DateTimeUnit.DateBased): Int = when(unit) { + is DateTimeUnit.DateBased.MonthBased -> (monthsUntil(other) / unit.months).toInt() + is DateTimeUnit.DateBased.DayBased -> (daysUntil(other) / unit.days).toInt() } diff --git a/core/commonTest/src/InstantTest.kt b/core/commonTest/src/InstantTest.kt index 9b32f00a5..f1dcfb028 100644 --- a/core/commonTest/src/InstantTest.kt +++ b/core/commonTest/src/InstantTest.kt @@ -94,21 +94,25 @@ class InstantTest { assertEquals(24, instant1.until(instant2, CalendarUnit.HOUR, zone)) assertEquals(24, instant2.minus(instant1, CalendarUnit.HOUR, zone)) - val instant3 = instant1.plus(ChronoUnit.DAY, zone) + val instant3 = instant1.plus(DateTimeUnit.DAY, zone) checkComponents(instant3.toLocalDateTime(zone), 2019, 10, 28, 2, 59) assertEquals(25.hours, instant3 - instant1) assertEquals(1, instant1.until(instant3, CalendarUnit.DAY, zone)) assertEquals(1, instant1.daysUntil(instant3, zone)) assertEquals(1, instant3.minus(instant1, CalendarUnit.DAY, zone)) - val instant4 = instant1.plus(14, ChronoUnit.MONTH, zone) + val instant4 = instant1.plus(14, DateTimeUnit.MONTH, zone) checkComponents(instant4.toLocalDateTime(zone), 2020, 12, 27, 2, 59) - assertEquals(1, instant1.until(instant4, ChronoUnit.YEAR, zone)) - assertEquals(4, instant1.until(instant4, ChronoUnit.QUARTER, zone)) - assertEquals(14, instant1.until(instant4, ChronoUnit.MONTH, zone)) - assertEquals(61, instant1.until(instant4, ChronoUnit.WEEK, zone)) - assertEquals(366 + 31 + 30, instant1.until(instant4, ChronoUnit.DAY, zone)) - assertEquals((366 + 31 + 30) * 24 + 1, instant1.until(instant4, ChronoUnit.HOUR, zone)) + assertEquals(1, instant1.until(instant4, DateTimeUnit.YEAR, zone)) + assertEquals(4, instant1.until(instant4, DateTimeUnit.QUARTER, zone)) + assertEquals(14, instant1.until(instant4, DateTimeUnit.MONTH, zone)) + assertEquals(61, instant1.until(instant4, DateTimeUnit.WEEK, zone)) + assertEquals(366 + 31 + 30, instant1.until(instant4, DateTimeUnit.DAY, zone)) + assertEquals((366 + 31 + 30) * 24 + 1, instant1.until(instant4, DateTimeUnit.HOUR, zone)) + + for (timeUnit in listOf(DateTimeUnit.SECOND, DateTimeUnit.MINUTE, DateTimeUnit.HOUR)) { + assertEquals(instant4 - instant1, timeUnit.duration * instant4.minus(instant1, timeUnit, zone).toDouble()) + } val period = CalendarPeriod(days = 1, hours = 1) val instant5 = instant1.plus(period, zone) diff --git a/core/commonTest/src/LocalDateTest.kt b/core/commonTest/src/LocalDateTest.kt index 9a3a72bc1..e1a210031 100644 --- a/core/commonTest/src/LocalDateTest.kt +++ b/core/commonTest/src/LocalDateTest.kt @@ -59,15 +59,15 @@ class LocalDateTest { @Test fun addComponents() { val startDate = LocalDate(2016, 2, 29) - checkComponents(startDate.plus(1, ChronoUnit.DAY), 2016, 3, 1) - checkComponents(startDate.plus(ChronoUnit.YEAR), 2017, 2, 28) + checkComponents(startDate.plus(1, DateTimeUnit.DAY), 2016, 3, 1) + checkComponents(startDate.plus(DateTimeUnit.YEAR), 2017, 2, 28) checkComponents(startDate + CalendarPeriod(years = 4), 2020, 2, 29) checkComponents(LocalDate.parse("2016-01-31") + CalendarPeriod(months = 1), 2016, 2, 29) assertFailsWith { startDate + CalendarPeriod(hours = 7) } assertFailsWith { startDate.plus(7, CalendarUnit.HOUR) } - assertFailsWith { startDate.plus(7, ChronoUnit.MINUTE) } +// assertFailsWith { startDate.plus(7, ChronoUnit.MINUTE) } // won't compile } @Test @@ -100,21 +100,21 @@ class LocalDateTest { @Test fun until() { val data = listOf( - Pair(Pair("2012-06-30", "2012-06-30"), Pair(CalendarUnit.DAY, 0)), - Pair(Pair("2012-06-30", "2012-06-30"), Pair(CalendarUnit.WEEK, 0)), - Pair(Pair("2012-06-30", "2012-06-30"), Pair(CalendarUnit.MONTH, 0)), - Pair(Pair("2012-06-30", "2012-06-30"), Pair(CalendarUnit.YEAR, 0)), - Pair(Pair("2012-06-30", "2012-07-01"), Pair(CalendarUnit.DAY, 1)), - Pair(Pair("2012-06-30", "2012-07-01"), Pair(CalendarUnit.WEEK, 0)), - Pair(Pair("2012-06-30", "2012-07-01"), Pair(CalendarUnit.MONTH, 0)), - Pair(Pair("2012-06-30", "2012-07-01"), Pair(CalendarUnit.YEAR, 0)), - Pair(Pair("2012-06-30", "2012-07-07"), Pair(CalendarUnit.DAY, 7)), - Pair(Pair("2012-06-30", "2012-07-07"), Pair(CalendarUnit.WEEK, 1)), - Pair(Pair("2012-06-30", "2012-07-07"), Pair(CalendarUnit.MONTH, 0)), - Pair(Pair("2012-06-30", "2012-07-07"), Pair(CalendarUnit.YEAR, 0)), - Pair(Pair("2012-06-30", "2012-07-29"), Pair(CalendarUnit.MONTH, 0)), - Pair(Pair("2012-06-30", "2012-07-30"), Pair(CalendarUnit.MONTH, 1)), - Pair(Pair("2012-06-30", "2012-07-31"), Pair(CalendarUnit.MONTH, 1))) + Pair(Pair("2012-06-30", "2012-06-30"), Pair(DateTimeUnit.DAY, 0)), + Pair(Pair("2012-06-30", "2012-06-30"), Pair(DateTimeUnit.WEEK, 0)), + Pair(Pair("2012-06-30", "2012-06-30"), Pair(DateTimeUnit.MONTH, 0)), + Pair(Pair("2012-06-30", "2012-06-30"), Pair(DateTimeUnit.YEAR, 0)), + Pair(Pair("2012-06-30", "2012-07-01"), Pair(DateTimeUnit.DAY, 1)), + Pair(Pair("2012-06-30", "2012-07-01"), Pair(DateTimeUnit.WEEK, 0)), + Pair(Pair("2012-06-30", "2012-07-01"), Pair(DateTimeUnit.MONTH, 0)), + Pair(Pair("2012-06-30", "2012-07-01"), Pair(DateTimeUnit.YEAR, 0)), + Pair(Pair("2012-06-30", "2012-07-07"), Pair(DateTimeUnit.DAY, 7)), + Pair(Pair("2012-06-30", "2012-07-07"), Pair(DateTimeUnit.WEEK, 1)), + Pair(Pair("2012-06-30", "2012-07-07"), Pair(DateTimeUnit.MONTH, 0)), + Pair(Pair("2012-06-30", "2012-07-07"), Pair(DateTimeUnit.YEAR, 0)), + Pair(Pair("2012-06-30", "2012-07-29"), Pair(DateTimeUnit.MONTH, 0)), + Pair(Pair("2012-06-30", "2012-07-30"), Pair(DateTimeUnit.MONTH, 1)), + Pair(Pair("2012-06-30", "2012-07-31"), Pair(DateTimeUnit.MONTH, 1))) for ((values, interval) in data) { val (v1, v2) = values val (unit, length) = interval @@ -122,12 +122,12 @@ class LocalDateTest { val end = LocalDate.parse(v2) assertEquals(length, start.until(end, unit), "$v2 - $v1 = $length($unit)") assertEquals(-length, end.until(start, unit), "$v1 - $v2 = -$length($unit)") - @Suppress("NON_EXHAUSTIVE_WHEN") + @Suppress("NON_EXHAUSTIVE_WHEN_ON_SEALED_CLASS") when (unit) { - CalendarUnit.YEAR -> assertEquals(length, start.yearsUntil(end)) - CalendarUnit.MONTH -> assertEquals(length, start.monthsUntil(end)) - CalendarUnit.WEEK -> assertEquals(length, start.daysUntil(end) / 7) - CalendarUnit.DAY -> assertEquals(length, start.daysUntil(end)) + DateTimeUnit.YEAR -> assertEquals(length, start.yearsUntil(end)) + DateTimeUnit.MONTH -> assertEquals(length, start.monthsUntil(end)) + DateTimeUnit.WEEK -> assertEquals(length, start.daysUntil(end) / 7) + DateTimeUnit.DAY -> assertEquals(length, start.daysUntil(end)) } } From 9eba478236057b55c1c96af75aec10a9e780d292 Mon Sep 17 00:00:00 2001 From: Ilya Gorbunov Date: Tue, 7 Jul 2020 09:00:05 +0300 Subject: [PATCH 06/10] Split CalendarPeriod into DatePeriod and DateTimePeriod --- .../{CalendarPeriod.kt => DateTimePeriod.kt} | 62 +++++++++++++--- core/commonMain/src/Instant.kt | 6 +- core/commonMain/src/LocalDate.kt | 6 +- core/commonTest/src/CalendarPeriodTest.kt | 33 --------- core/commonTest/src/DateTimePeriodTest.kt | 72 +++++++++++++++++++ core/commonTest/src/InstantTest.kt | 4 +- core/commonTest/src/LocalDateTest.kt | 10 +-- core/jsMain/src/Instant.kt | 6 +- core/jsMain/src/LocalDate.kt | 10 +-- core/jvmMain/src/Instant.kt | 6 +- core/jvmMain/src/LocalDate.kt | 10 +-- core/nativeMain/src/Instant.kt | 6 +- core/nativeMain/src/LocalDate.kt | 10 +-- 13 files changed, 157 insertions(+), 84 deletions(-) rename core/commonMain/src/{CalendarPeriod.kt => DateTimePeriod.kt} (60%) delete mode 100644 core/commonTest/src/CalendarPeriodTest.kt create mode 100644 core/commonTest/src/DateTimePeriodTest.kt diff --git a/core/commonMain/src/CalendarPeriod.kt b/core/commonMain/src/DateTimePeriod.kt similarity index 60% rename from core/commonMain/src/CalendarPeriod.kt rename to core/commonMain/src/DateTimePeriod.kt index 8f1325b88..b256f3ca3 100644 --- a/core/commonMain/src/CalendarPeriod.kt +++ b/core/commonMain/src/DateTimePeriod.kt @@ -8,17 +8,21 @@ package kotlinx.datetime import kotlin.time.Duration import kotlin.time.ExperimentalTime -typealias Period = CalendarPeriod // TODO: could be error-prone without explicitly named params -class CalendarPeriod(val years: Int = 0, val months: Int = 0, val days: Int = 0, - val hours: Int = 0, val minutes: Int = 0, val seconds: Long = 0, val nanoseconds: Long = 0) { +sealed class DateTimePeriod { + abstract val years: Int + abstract val months: Int + abstract val days: Int + abstract val hours: Int + abstract val minutes: Int + abstract val seconds: Long + abstract val nanoseconds: Long private fun allNotPositive() = years <= 0 && months <= 0 && days <= 0 && hours <= 0 && minutes <= 0 && seconds <= 0 && nanoseconds <= 0 && (years or months or days or hours or minutes != 0 || seconds or nanoseconds != 0L) - override fun toString(): String = buildString { val sign = if (allNotPositive()) { append('-'); -1 } else 1 append('P') @@ -39,7 +43,7 @@ class CalendarPeriod(val years: Int = 0, val months: Int = 0, val days: Int = 0, override fun equals(other: Any?): Boolean { if (this === other) return true - if (other !is CalendarPeriod) return false + if (other !is DateTimePeriod) return false if (years != other.years) return false if (months != other.months) return false @@ -62,14 +66,50 @@ class CalendarPeriod(val years: Int = 0, val months: Int = 0, val days: Int = 0, result = 31 * result + nanoseconds.hashCode() return result } + + // TODO: parsing from iso string } +class DatePeriod( + override val years: Int = 0, + override val months: Int = 0, + override val days: Int = 0 +) : DateTimePeriod() { + override val hours: Int get() = 0 + override val minutes: Int get() = 0 + override val seconds: Long get() = 0 + override val nanoseconds: Long get() = 0 +} + +private class DateTimePeriodImpl( + override val years: Int = 0, + override val months: Int = 0, + override val days: Int = 0, + override val hours: Int = 0, + override val minutes: Int = 0, + override val seconds: Long = 0, + override val nanoseconds: Long = 0 +) : DateTimePeriod() + +fun DateTimePeriod( + years: Int = 0, + months: Int = 0, + days: Int = 0, + hours: Int = 0, + minutes: Int = 0, + seconds: Long = 0, + nanoseconds: Long = 0 +): DateTimePeriod = if (hours or minutes != 0 || seconds or nanoseconds != 0L) + DateTimePeriodImpl(years, months, days, hours, minutes, seconds, nanoseconds) +else + DatePeriod(years, months, days) + @OptIn(ExperimentalTime::class) -fun Duration.toCalendarPeriod(): CalendarPeriod = toComponents { hours, minutes, seconds, nanoseconds -> - CalendarPeriod(hours = hours, minutes = minutes, seconds = seconds.toLong(), nanoseconds = nanoseconds.toLong()) +fun Duration.toDateTimePeriod(): DateTimePeriod = toComponents { hours, minutes, seconds, nanoseconds -> + DateTimePeriod(hours = hours, minutes = minutes, seconds = seconds.toLong(), nanoseconds = nanoseconds.toLong()) } -operator fun CalendarPeriod.plus(other: CalendarPeriod): CalendarPeriod = CalendarPeriod( +operator fun DateTimePeriod.plus(other: DateTimePeriod): DateTimePeriod = DateTimePeriod( this.years + other.years, this.months + other.months, this.days + other.days, @@ -79,6 +119,12 @@ operator fun CalendarPeriod.plus(other: CalendarPeriod): CalendarPeriod = Calend this.nanoseconds + other.nanoseconds ) +operator fun DatePeriod.plus(other: DatePeriod): DatePeriod = DatePeriod( + this.years + other.years, + this.months + other.months, + this.days + other.days +) + enum class CalendarUnit { YEAR, MONTH, diff --git a/core/commonMain/src/Instant.kt b/core/commonMain/src/Instant.kt index bed6b9f5e..002103da2 100644 --- a/core/commonMain/src/Instant.kt +++ b/core/commonMain/src/Instant.kt @@ -74,7 +74,7 @@ public fun String.toInstant(): Instant = Instant.parse(this) * @throws DateTimeArithmeticException if this value or the results of intermediate computations are too large to fit in * [LocalDateTime]. */ -public expect fun Instant.plus(period: CalendarPeriod, zone: TimeZone): Instant +public expect fun Instant.plus(period: DateTimePeriod, zone: TimeZone): Instant /** * @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. @@ -89,7 +89,7 @@ public expect fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone): /** * @throws DateTimeArithmeticException if this [Instant] or [other] is too large to fit in [LocalDateTime]. */ -public expect fun Instant.periodUntil(other: Instant, zone: TimeZone): CalendarPeriod +public expect fun Instant.periodUntil(other: Instant, zone: TimeZone): DateTimePeriod /** * The return value is clamped to [Long.MAX_VALUE] or [Long.MIN_VALUE] if [unit] is more granular than @@ -130,7 +130,7 @@ public fun Instant.yearsUntil(other: Instant, zone: TimeZone): Int = internal fun Long.clampToInt(): Int = if (this > Int.MAX_VALUE) Int.MAX_VALUE else if (this < Int.MIN_VALUE) Int.MIN_VALUE else toInt() -public fun Instant.minus(other: Instant, zone: TimeZone): CalendarPeriod = other.periodUntil(this, zone) +public fun Instant.minus(other: Instant, zone: TimeZone): DateTimePeriod = other.periodUntil(this, zone) public fun Instant.minus(other: Instant, unit: CalendarUnit, zone: TimeZone): Long = other.until(this, unit, zone) diff --git a/core/commonMain/src/LocalDate.kt b/core/commonMain/src/LocalDate.kt index e4b678a8f..325621c95 100644 --- a/core/commonMain/src/LocalDate.kt +++ b/core/commonMain/src/LocalDate.kt @@ -51,13 +51,13 @@ expect fun LocalDate.plus(value: Int, unit: CalendarUnit): LocalDate * @throws DateTimeArithmeticException if arithmetic overflow occurs or the boundaries of [LocalDate] are exceeded at * any point in intermediate computations. */ -expect operator fun LocalDate.plus(period: CalendarPeriod): LocalDate +expect operator fun LocalDate.plus(period: DatePeriod): LocalDate /** */ -expect fun LocalDate.periodUntil(other: LocalDate): CalendarPeriod +expect fun LocalDate.periodUntil(other: LocalDate): DatePeriod /** */ -operator fun LocalDate.minus(other: LocalDate): CalendarPeriod = other.periodUntil(this) +operator fun LocalDate.minus(other: LocalDate): DatePeriod = other.periodUntil(this) public expect fun LocalDate.daysUntil(other: LocalDate): Int public expect fun LocalDate.monthsUntil(other: LocalDate): Int diff --git a/core/commonTest/src/CalendarPeriodTest.kt b/core/commonTest/src/CalendarPeriodTest.kt deleted file mode 100644 index 22e9986f8..000000000 --- a/core/commonTest/src/CalendarPeriodTest.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2019-2020 JetBrains s.r.o. - * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. - */ - -package kotlinx.datetime.test - -import kotlin.test.* -import kotlinx.datetime.* - - -class CalendarPeriodTest { - - @Test - fun toStringConversion() { - assertEquals("P1Y", CalendarPeriod(years = 1).toString()) - assertEquals("P1Y1M", CalendarPeriod(years = 1, months = 1).toString()) - assertEquals("P11M", CalendarPeriod(months = 11).toString()) - assertEquals("P14M", CalendarPeriod(months = 14).toString()) // TODO: normalize or not - assertEquals("P10M5D", CalendarPeriod(months = 10, days = 5).toString()) - assertEquals("P1Y40D", CalendarPeriod(years = 1, days = 40).toString()) - - assertEquals("PT1H", CalendarPeriod(hours = 1).toString()) - assertEquals("P0D", CalendarPeriod().toString()) - - assertEquals("P1DT-1H", CalendarPeriod(days = 1, hours = -1).toString()) - assertEquals("-P1DT1H", CalendarPeriod(days = -1, hours = -1).toString()) - assertEquals("-P1M", CalendarPeriod(months = -1).toString()) - - assertEquals("P-1Y-2M-3DT-4H-5M0.500000000S", - CalendarPeriod(years = -1, months = -2, days = -3, hours = -4, minutes = -5, seconds = 0, nanoseconds = 500_000_000).toString()) - } -} \ No newline at end of file diff --git a/core/commonTest/src/DateTimePeriodTest.kt b/core/commonTest/src/DateTimePeriodTest.kt new file mode 100644 index 000000000..aa044fd0f --- /dev/null +++ b/core/commonTest/src/DateTimePeriodTest.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2019-2020 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.test + +import kotlin.test.* +import kotlinx.datetime.* +import kotlin.time.* + + +class DateTimePeriodTest { + + @Test + fun toStringConversion() { + assertEquals("P1Y", DateTimePeriod(years = 1).toString()) + assertEquals("P1Y1M", DatePeriod(years = 1, months = 1).toString()) + assertEquals("P11M", DateTimePeriod(months = 11).toString()) + assertEquals("P14M", DateTimePeriod(months = 14).toString()) // TODO: normalize or not + assertEquals("P10M5D", DateTimePeriod(months = 10, days = 5).toString()) + assertEquals("P1Y40D", DateTimePeriod(years = 1, days = 40).toString()) + + assertEquals("PT1H", DateTimePeriod(hours = 1).toString()) + assertEquals("P0D", DateTimePeriod().toString()) + assertEquals("P0D", DatePeriod().toString()) + + assertEquals("P1DT-1H", DateTimePeriod(days = 1, hours = -1).toString()) + assertEquals("-P1DT1H", DateTimePeriod(days = -1, hours = -1).toString()) + assertEquals("-P1M", DateTimePeriod(months = -1).toString()) + + assertEquals("P-1Y-2M-3DT-4H-5M0.500000000S", + DateTimePeriod(years = -1, months = -2, days = -3, hours = -4, minutes = -5, seconds = 0, nanoseconds = 500_000_000).toString()) + } + + @Test + fun periodArithmetic() { + val p1 = DateTimePeriod(years = 10) + val p2 = DateTimePeriod(days = 3) + val p3 = DateTimePeriod(hours = 2) + val p4 = DateTimePeriod(hours = -2) + + val dp1 = DatePeriod(years = 1, months = 6) + + assertEquals(DateTimePeriod(years = 10, days = 3, hours = 2), p1 + p2 + p3) + assertEquals(DatePeriod(years = 11, months = 6), dp1 + p1) + assertEquals(DatePeriod(years = 2, months = 12), dp1 + dp1) + assertEquals(DateTimePeriod(years = 1, months = 6, days = 3), p2 + dp1) + + val dp2 = dp1 + p3 + p4 + assertEquals(dp1, dp2) + assertTrue(dp2 is DatePeriod) + } + + @OptIn(ExperimentalTime::class) + @Test + fun durationConversion() { + val periodZero = Duration.ZERO.toDateTimePeriod() + assertEquals(DateTimePeriod(), periodZero) + assertEquals(DatePeriod(), periodZero) + assertTrue(periodZero is DatePeriod) + + for ((period, duration) in listOf( + DateTimePeriod(hours = 1) to 1.hours, + DateTimePeriod(hours = 2) to 120.minutes, + DateTimePeriod(minutes = 2, seconds = 30) to 150.seconds, + DateTimePeriod(seconds = 2) to 2e9.nanoseconds + )) { + assertEquals(period, duration.toDateTimePeriod()) + } + } +} \ No newline at end of file diff --git a/core/commonTest/src/InstantTest.kt b/core/commonTest/src/InstantTest.kt index f1dcfb028..42597859f 100644 --- a/core/commonTest/src/InstantTest.kt +++ b/core/commonTest/src/InstantTest.kt @@ -88,7 +88,7 @@ class InstantTest { val instant1 = LocalDateTime(2019, 10, 27, 2, 59, 0, 0).toInstant(zone) checkComponents(instant1.toLocalDateTime(zone), 2019, 10, 27, 2, 59) - val instant2 = instant1.plus(CalendarPeriod(hours = 24), zone) + val instant2 = instant1.plus(DateTimePeriod(hours = 24), zone) checkComponents(instant2.toLocalDateTime(zone), 2019, 10, 28, 1, 59) assertEquals(24.hours, instant2 - instant1) assertEquals(24, instant1.until(instant2, CalendarUnit.HOUR, zone)) @@ -114,7 +114,7 @@ class InstantTest { assertEquals(instant4 - instant1, timeUnit.duration * instant4.minus(instant1, timeUnit, zone).toDouble()) } - val period = CalendarPeriod(days = 1, hours = 1) + 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)) diff --git a/core/commonTest/src/LocalDateTest.kt b/core/commonTest/src/LocalDateTest.kt index e1a210031..c46a88396 100644 --- a/core/commonTest/src/LocalDateTest.kt +++ b/core/commonTest/src/LocalDateTest.kt @@ -61,11 +61,11 @@ class LocalDateTest { val startDate = LocalDate(2016, 2, 29) checkComponents(startDate.plus(1, DateTimeUnit.DAY), 2016, 3, 1) checkComponents(startDate.plus(DateTimeUnit.YEAR), 2017, 2, 28) - checkComponents(startDate + CalendarPeriod(years = 4), 2020, 2, 29) + checkComponents(startDate + DatePeriod(years = 4), 2020, 2, 29) - checkComponents(LocalDate.parse("2016-01-31") + CalendarPeriod(months = 1), 2016, 2, 29) + checkComponents(LocalDate.parse("2016-01-31") + DatePeriod(months = 1), 2016, 2, 29) - assertFailsWith { startDate + CalendarPeriod(hours = 7) } +// assertFailsWith { startDate + CalendarPeriod(hours = 7) } // won't compile assertFailsWith { startDate.plus(7, CalendarUnit.HOUR) } // assertFailsWith { startDate.plus(7, ChronoUnit.MINUTE) } // won't compile } @@ -75,7 +75,7 @@ class LocalDateTest { val today = Clock.System.todayAt(TimeZone.SYSTEM) val nextMonthPlusDay1 = today.plus(1, CalendarUnit.MONTH).plus(1, CalendarUnit.DAY) - val nextMonthPlusDay2 = today + CalendarPeriod(months = 1, days = 1) + val nextMonthPlusDay2 = today + DatePeriod(months = 1, days = 1) val nextMonthPlusDay3 = today.plus(1, CalendarUnit.DAY).plus(1, CalendarUnit.MONTH) } @@ -86,7 +86,7 @@ class LocalDateTest { repeat(1000) { val days1 = Random.nextInt(-3652..3652) val days2 = Random.nextInt(-3652..3652) - val ldtBefore = origin + CalendarPeriod(days = days1) + val ldtBefore = origin + DatePeriod(days = days1) val ldtNow = origin.plus(days2, CalendarUnit.DAY) val diff = ldtNow - ldtBefore diff --git a/core/jsMain/src/Instant.kt b/core/jsMain/src/Instant.kt index 8310cfd4b..67aafa402 100644 --- a/core/jsMain/src/Instant.kt +++ b/core/jsMain/src/Instant.kt @@ -63,7 +63,7 @@ public actual class Instant internal constructor(internal val value: jtInstant) } -public actual fun Instant.plus(period: CalendarPeriod, zone: TimeZone): Instant { +public actual fun Instant.plus(period: DateTimePeriod, zone: TimeZone): Instant { val thisZdt = this.value.atZone(zone.zoneId) return with(period) { thisZdt @@ -102,7 +102,7 @@ public actual fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone): }.let(::Instant) @OptIn(ExperimentalTime::class) -public actual fun Instant.periodUntil(other: Instant, zone: TimeZone): CalendarPeriod { +public actual fun Instant.periodUntil(other: Instant, zone: TimeZone): DateTimePeriod { var thisZdt = this.value.atZone(zone.zoneId) val otherZdt = other.value.atZone(zone.zoneId) @@ -111,7 +111,7 @@ public actual fun Instant.periodUntil(other: Instant, zone: TimeZone): CalendarP val time = thisZdt.until(otherZdt, ChronoUnit.NANOS).toDouble().nanoseconds time.toComponents { hours, minutes, seconds, nanoseconds -> - return CalendarPeriod((months / 12).toInt(), (months % 12).toInt(), days.toInt(), hours, minutes, seconds.toLong(), nanoseconds.toLong()) + return DateTimePeriod((months / 12).toInt(), (months % 12).toInt(), days.toInt(), hours, minutes, seconds.toLong(), nanoseconds.toLong()) } } diff --git a/core/jsMain/src/LocalDate.kt b/core/jsMain/src/LocalDate.kt index 8e8096218..7cad4b462 100644 --- a/core/jsMain/src/LocalDate.kt +++ b/core/jsMain/src/LocalDate.kt @@ -56,12 +56,8 @@ public actual fun LocalDate.plus(value: Int, unit: CalendarUnit): LocalDate = -public actual operator fun LocalDate.plus(period: CalendarPeriod): LocalDate = +public actual operator fun LocalDate.plus(period: DatePeriod): LocalDate = with(period) { - if (hours != 0 || minutes != 0 || seconds != 0L || nanoseconds != 0L) { - throw IllegalArgumentException("Only date based units can be added to LocalDate") - } - return@with value .run { if (years != 0 && months == 0) plusYears(years) else this } .run { if (months != 0) plusMonths(years.toDouble() * 12 + months) else this } @@ -71,13 +67,13 @@ public actual operator fun LocalDate.plus(period: CalendarPeriod): LocalDate = -public actual fun LocalDate.periodUntil(other: LocalDate): CalendarPeriod { +public actual fun LocalDate.periodUntil(other: LocalDate): DatePeriod { var startD = this.value val endD = other.value val months = startD.until(endD, ChronoUnit.MONTHS).toInt(); startD = startD.plusMonths(months) val days = startD.until(endD, ChronoUnit.DAYS).toInt() - return CalendarPeriod(months / 12, months % 12, days) + return DatePeriod(months / 12, months % 12, days) } public actual fun LocalDate.daysUntil(other: LocalDate): Int = diff --git a/core/jvmMain/src/Instant.kt b/core/jvmMain/src/Instant.kt index f64739e37..1621400a6 100644 --- a/core/jvmMain/src/Instant.kt +++ b/core/jvmMain/src/Instant.kt @@ -57,7 +57,7 @@ public actual class Instant internal constructor(internal val value: jtInstant) } } -public actual fun Instant.plus(period: CalendarPeriod, zone: TimeZone): Instant { +public actual fun Instant.plus(period: DateTimePeriod, zone: TimeZone): Instant { val thisZdt = this.value.atZone(zone.zoneId) return with(period) { thisZdt @@ -87,7 +87,7 @@ public actual fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone): }.let(::Instant) @OptIn(ExperimentalTime::class) -public actual fun Instant.periodUntil(other: Instant, zone: TimeZone): CalendarPeriod { +public actual fun Instant.periodUntil(other: Instant, zone: TimeZone): DateTimePeriod { var thisZdt = this.value.atZone(zone.zoneId) val otherZdt = other.value.atZone(zone.zoneId) @@ -96,7 +96,7 @@ public actual fun Instant.periodUntil(other: Instant, zone: TimeZone): CalendarP val time = thisZdt.until(otherZdt, ChronoUnit.NANOS).nanoseconds time.toComponents { hours, minutes, seconds, nanoseconds -> - return CalendarPeriod((months / 12).toInt(), (months % 12).toInt(), days.toInt(), hours, minutes, seconds.toLong(), nanoseconds.toLong()) + return DateTimePeriod((months / 12).toInt(), (months % 12).toInt(), days.toInt(), hours, minutes, seconds.toLong(), nanoseconds.toLong()) } } diff --git a/core/jvmMain/src/LocalDate.kt b/core/jvmMain/src/LocalDate.kt index bfc628057..c80f4f39d 100644 --- a/core/jvmMain/src/LocalDate.kt +++ b/core/jvmMain/src/LocalDate.kt @@ -52,12 +52,8 @@ public actual fun LocalDate.plus(value: Long, unit: CalendarUnit): LocalDate = public actual fun LocalDate.plus(value: Int, unit: CalendarUnit): LocalDate = plus(value.toLong(), unit) -public actual operator fun LocalDate.plus(period: CalendarPeriod): LocalDate = +public actual operator fun LocalDate.plus(period: DatePeriod): LocalDate = with(period) { - if (hours != 0 || minutes != 0 || seconds != 0L || nanoseconds != 0L) { - throw IllegalArgumentException("Only date based units can be added to LocalDate") - } - return@with value .run { if (years != 0 && months == 0) plusYears(years.toLong()) else this } .run { if (months != 0) plusMonths(years * 12L + months.toLong()) else this } @@ -66,13 +62,13 @@ public actual operator fun LocalDate.plus(period: CalendarPeriod): LocalDate = }.let(::LocalDate) -public actual fun LocalDate.periodUntil(other: LocalDate): CalendarPeriod { +public actual fun LocalDate.periodUntil(other: LocalDate): DatePeriod { var startD = this.value val endD = other.value val months = startD.until(endD, ChronoUnit.MONTHS); startD = startD.plusMonths(months) val days = startD.until(endD, ChronoUnit.DAYS) - return CalendarPeriod((months / 12).toInt(), (months % 12).toInt(), days.toInt()) + return DatePeriod((months / 12).toInt(), (months % 12).toInt(), days.toInt()) } public actual fun LocalDate.daysUntil(other: LocalDate): Int = diff --git a/core/nativeMain/src/Instant.kt b/core/nativeMain/src/Instant.kt index 18a94a510..eb255b249 100644 --- a/core/nativeMain/src/Instant.kt +++ b/core/nativeMain/src/Instant.kt @@ -282,7 +282,7 @@ private fun Instant.check(zone: TimeZone): Instant = this@check.also { toZonedLocalDateTimeFailing(zone) } -actual fun Instant.plus(period: CalendarPeriod, zone: TimeZone): Instant = try { +actual fun Instant.plus(period: DateTimePeriod, zone: TimeZone): Instant = try { with(period) { val withDate = toZonedLocalDateTimeFailing(zone) .run { if (years != 0 && months == 0) plusYears(years.toLong()) else this } @@ -336,7 +336,7 @@ actual fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone): Instan } @OptIn(ExperimentalTime::class) -actual fun Instant.periodUntil(other: Instant, zone: TimeZone): CalendarPeriod { +actual fun Instant.periodUntil(other: Instant, zone: TimeZone): DateTimePeriod { var thisLdt = toZonedLocalDateTimeFailing(zone) val otherLdt = other.toZonedLocalDateTimeFailing(zone) @@ -347,7 +347,7 @@ actual fun Instant.periodUntil(other: Instant, zone: TimeZone): CalendarPeriod { val time = thisLdt.until(otherLdt, CalendarUnit.NANOSECOND).nanoseconds // |otherLdt - thisLdt| < 24h time.toComponents { hours, minutes, seconds, nanoseconds -> - return CalendarPeriod((months / 12).toInt(), (months % 12).toInt(), days.toInt(), hours, minutes, seconds.toLong(), nanoseconds.toLong()) + return DateTimePeriod((months / 12).toInt(), (months % 12).toInt(), days.toInt(), hours, minutes, seconds.toLong(), nanoseconds.toLong()) } } diff --git a/core/nativeMain/src/LocalDate.kt b/core/nativeMain/src/LocalDate.kt index 50cff3356..89fa8ea16 100644 --- a/core/nativeMain/src/LocalDate.kt +++ b/core/nativeMain/src/LocalDate.kt @@ -273,12 +273,8 @@ actual fun LocalDate.plus(value: Long, unit: CalendarUnit): LocalDate = actual fun LocalDate.plus(value: Int, unit: CalendarUnit): LocalDate = plus(value.toLong(), unit) -actual operator fun LocalDate.plus(period: CalendarPeriod): LocalDate = +actual operator fun LocalDate.plus(period: DatePeriod): LocalDate = with(period) { - require (hours == 0 && minutes == 0 && seconds == 0L && nanoseconds == 0L) { - "Only date based units can be added to LocalDate" - } - try { this@plus .run { if (years != 0 && months == 0) plusYears(years.toLong()) else this } @@ -324,8 +320,8 @@ internal fun LocalDate.longUntil(end: LocalDate, unit: CalendarUnit): Long = CalendarUnit.NANOSECOND -> throw IllegalArgumentException("Unsupported unit: $unit") } -actual fun LocalDate.periodUntil(other: LocalDate): CalendarPeriod { +actual fun LocalDate.periodUntil(other: LocalDate): DatePeriod { val months = longUntil(other, CalendarUnit.MONTH) val days = plusMonths(months).longUntil(other, CalendarUnit.DAY) - return CalendarPeriod((months / 12).toInt(), (months % 12).toInt(), days.toInt()) + return DatePeriod((months / 12).toInt(), (months % 12).toInt(), days.toInt()) } From c719ba9ae0acaf65612224e5dc59d663cb8b76ff Mon Sep 17 00:00:00 2001 From: Ilya Gorbunov Date: Sat, 11 Jul 2020 06:04:10 +0300 Subject: [PATCH 07/10] Fix incorrect plus(Long,...) behavior in js-joda When 0L was passed as JS number argument, it failed to equal strictly to zero number, thus triggering an incorrect execution path. --- core/commonTest/src/LocalDateTest.kt | 2 ++ core/jsMain/src/LocalDate.kt | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/core/commonTest/src/LocalDateTest.kt b/core/commonTest/src/LocalDateTest.kt index c46a88396..626de415a 100644 --- a/core/commonTest/src/LocalDateTest.kt +++ b/core/commonTest/src/LocalDateTest.kt @@ -82,6 +82,8 @@ class LocalDateTest { @Test fun diffInvariant() { val origin = LocalDate(2001, 1, 1) + assertEquals(origin, origin.plus(0, DateTimeUnit.DAY)) + assertEquals(origin, origin.plus(DatePeriod(days = 0))) repeat(1000) { val days1 = Random.nextInt(-3652..3652) diff --git a/core/jsMain/src/LocalDate.kt b/core/jsMain/src/LocalDate.kt index 7cad4b462..f575f45a3 100644 --- a/core/jsMain/src/LocalDate.kt +++ b/core/jsMain/src/LocalDate.kt @@ -49,7 +49,7 @@ private fun LocalDate.plusNumber(value: Number, unit: CalendarUnit): LocalDate = }.let(::LocalDate) public actual fun LocalDate.plus(value: Long, unit: CalendarUnit): LocalDate = - plusNumber(value, unit) + plusNumber(value.toDouble(), unit) public actual fun LocalDate.plus(value: Int, unit: CalendarUnit): LocalDate = plusNumber(value, unit) From 545f8a47cb076ddce1af72a2738188225f8c9123 Mon Sep 17 00:00:00 2001 From: Ilya Gorbunov Date: Sat, 11 Jul 2020 05:08:25 +0300 Subject: [PATCH 08/10] Make CalendarUnit an internal helper enum Drop WEEK and introduce MILLISECOND and MICROSECOND --- core/commonMain/src/DateTimePeriod.kt | 10 ---------- core/commonMain/src/DateTimeUnit.kt | 21 +++++++++++++++++++++ core/commonMain/src/Instant.kt | 7 +++---- core/commonMain/src/LocalDate.kt | 13 ++----------- core/commonTest/src/InstantTest.kt | 14 ++++++++------ core/commonTest/src/LocalDateTest.kt | 12 +++--------- core/jsMain/src/Instant.kt | 15 +++++++++------ core/jsMain/src/LocalDate.kt | 7 ++++--- core/jvmMain/src/Instant.kt | 12 +++++++----- core/jvmMain/src/LocalDate.kt | 7 ++++--- core/nativeMain/src/Instant.kt | 9 +++++---- core/nativeMain/src/LocalDate.kt | 12 +++++++----- core/nativeMain/src/LocalDateTime.kt | 8 ++++++-- core/nativeMain/src/Util.kt | 2 ++ core/nativeMain/src/ZonedDateTime.kt | 4 ++-- 15 files changed, 83 insertions(+), 70 deletions(-) diff --git a/core/commonMain/src/DateTimePeriod.kt b/core/commonMain/src/DateTimePeriod.kt index b256f3ca3..f444a1e31 100644 --- a/core/commonMain/src/DateTimePeriod.kt +++ b/core/commonMain/src/DateTimePeriod.kt @@ -125,13 +125,3 @@ operator fun DatePeriod.plus(other: DatePeriod): DatePeriod = DatePeriod( this.days + other.days ) -enum class CalendarUnit { - YEAR, - MONTH, - WEEK, - DAY, - HOUR, - MINUTE, - SECOND, - NANOSECOND -} diff --git a/core/commonMain/src/DateTimeUnit.kt b/core/commonMain/src/DateTimeUnit.kt index 4bc20a8c5..bedc4efe6 100644 --- a/core/commonMain/src/DateTimeUnit.kt +++ b/core/commonMain/src/DateTimeUnit.kt @@ -36,6 +36,14 @@ sealed class DateTimeUnit { calendarUnit = CalendarUnit.SECOND calendarScale = nanoseconds / 1_000_000_000 } + nanoseconds % 1_000_000 == 0L -> { + calendarUnit = CalendarUnit.MILLISECOND + calendarScale = nanoseconds / 1_000_000 + } + nanoseconds % 1_000 == 0L -> { + calendarUnit = CalendarUnit.MICROSECOND + calendarScale = nanoseconds / 1_000 + } else -> { calendarUnit = CalendarUnit.NANOSECOND calendarScale = nanoseconds @@ -104,3 +112,16 @@ sealed class DateTimeUnit { val CENTURY = YEAR * 100 } } + + +internal enum class CalendarUnit { + YEAR, + MONTH, + DAY, + HOUR, + MINUTE, + SECOND, + MILLISECOND, + MICROSECOND, + NANOSECOND +} diff --git a/core/commonMain/src/Instant.kt b/core/commonMain/src/Instant.kt index 002103da2..7b13f9b47 100644 --- a/core/commonMain/src/Instant.kt +++ b/core/commonMain/src/Instant.kt @@ -79,12 +79,12 @@ public expect fun Instant.plus(period: DateTimePeriod, zone: TimeZone): Instant /** * @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. */ -public expect fun Instant.plus(value: Int, unit: CalendarUnit, zone: TimeZone): Instant +internal expect fun Instant.plus(value: Int, unit: CalendarUnit, zone: TimeZone): Instant /** * @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. */ -public expect fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone): Instant +internal expect fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone): Instant /** * @throws DateTimeArithmeticException if this [Instant] or [other] is too large to fit in [LocalDateTime]. @@ -97,7 +97,7 @@ public expect fun Instant.periodUntil(other: Instant, zone: TimeZone): DateTimeP * * @throws DateTimeArithmeticException if this [Instant] or [other] is too large to fit in [LocalDateTime]. */ -public expect fun Instant.until(other: Instant, unit: CalendarUnit, zone: TimeZone): Long +internal expect fun Instant.until(other: Instant, unit: CalendarUnit, zone: TimeZone): Long /** * The return value is clamped to [Int.MAX_VALUE] or [Int.MIN_VALUE] if the result would otherwise cause an arithmetic @@ -131,7 +131,6 @@ internal fun Long.clampToInt(): Int = if (this > Int.MAX_VALUE) Int.MAX_VALUE else if (this < Int.MIN_VALUE) Int.MIN_VALUE else toInt() public fun Instant.minus(other: Instant, zone: TimeZone): DateTimePeriod = other.periodUntil(this, zone) -public fun Instant.minus(other: Instant, unit: CalendarUnit, zone: TimeZone): Long = other.until(this, unit, zone) diff --git a/core/commonMain/src/LocalDate.kt b/core/commonMain/src/LocalDate.kt index 325621c95..652543c6a 100644 --- a/core/commonMain/src/LocalDate.kt +++ b/core/commonMain/src/LocalDate.kt @@ -38,13 +38,13 @@ public fun String.toLocalDate(): LocalDate = LocalDate.parse(this) * @throws IllegalArgumentException if the calendar unit is not date-based. * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. */ -expect fun LocalDate.plus(value: Long, unit: CalendarUnit): LocalDate +internal expect fun LocalDate.plus(value: Long, unit: CalendarUnit): LocalDate /** * @throws IllegalArgumentException if the calendar unit is not date-based. * @throws DateTimeArithmeticException if the result exceeds the boundaries of [LocalDate]. */ -expect fun LocalDate.plus(value: Int, unit: CalendarUnit): LocalDate +internal expect fun LocalDate.plus(value: Int, unit: CalendarUnit): LocalDate /** * @throws IllegalArgumentException if [period] has non-zero time (as opposed to date) components. @@ -63,15 +63,6 @@ public expect fun LocalDate.daysUntil(other: LocalDate): Int public expect fun LocalDate.monthsUntil(other: LocalDate): Int public expect fun LocalDate.yearsUntil(other: LocalDate): Int -public fun LocalDate.until(other: LocalDate, unit: CalendarUnit): Int = when(unit) { - CalendarUnit.YEAR -> yearsUntil(other) - CalendarUnit.MONTH -> monthsUntil(other) - CalendarUnit.WEEK -> daysUntil(other) / 7 - CalendarUnit.DAY -> daysUntil(other) - CalendarUnit.HOUR, CalendarUnit.MINUTE, CalendarUnit.SECOND, CalendarUnit.NANOSECOND -> - throw UnsupportedOperationException("Only date based units can be used to express difference between LocalDate values.") -} - public fun LocalDate.plus(unit: DateTimeUnit.DateBased): LocalDate = plus(unit.calendarScale, unit.calendarUnit) public fun LocalDate.plus(value: Int, unit: DateTimeUnit.DateBased): LocalDate = diff --git a/core/commonTest/src/InstantTest.kt b/core/commonTest/src/InstantTest.kt index 42597859f..8700bf8b8 100644 --- a/core/commonTest/src/InstantTest.kt +++ b/core/commonTest/src/InstantTest.kt @@ -91,15 +91,15 @@ class InstantTest { val instant2 = instant1.plus(DateTimePeriod(hours = 24), zone) checkComponents(instant2.toLocalDateTime(zone), 2019, 10, 28, 1, 59) assertEquals(24.hours, instant2 - instant1) - assertEquals(24, instant1.until(instant2, CalendarUnit.HOUR, zone)) - assertEquals(24, instant2.minus(instant1, CalendarUnit.HOUR, zone)) + assertEquals(24, instant1.until(instant2, DateTimeUnit.HOUR, zone)) + assertEquals(24, instant2.minus(instant1, DateTimeUnit.HOUR, zone)) val instant3 = instant1.plus(DateTimeUnit.DAY, zone) checkComponents(instant3.toLocalDateTime(zone), 2019, 10, 28, 2, 59) assertEquals(25.hours, instant3 - instant1) - assertEquals(1, instant1.until(instant3, CalendarUnit.DAY, zone)) + assertEquals(1, instant1.until(instant3, DateTimeUnit.DAY, zone)) assertEquals(1, instant1.daysUntil(instant3, zone)) - assertEquals(1, instant3.minus(instant1, CalendarUnit.DAY, zone)) + assertEquals(1, instant3.minus(instant1, DateTimeUnit.DAY, zone)) val instant4 = instant1.plus(14, DateTimeUnit.MONTH, zone) checkComponents(instant4.toLocalDateTime(zone), 2020, 12, 27, 2, 59) @@ -110,8 +110,10 @@ class InstantTest { assertEquals(366 + 31 + 30, instant1.until(instant4, DateTimeUnit.DAY, zone)) assertEquals((366 + 31 + 30) * 24 + 1, instant1.until(instant4, DateTimeUnit.HOUR, zone)) - for (timeUnit in listOf(DateTimeUnit.SECOND, DateTimeUnit.MINUTE, DateTimeUnit.HOUR)) { - assertEquals(instant4 - instant1, timeUnit.duration * instant4.minus(instant1, timeUnit, zone).toDouble()) + for (timeUnit in listOf(DateTimeUnit.MICROSECOND, DateTimeUnit.MILLISECOND, DateTimeUnit.SECOND, DateTimeUnit.MINUTE, DateTimeUnit.HOUR)) { + val diff = instant4.minus(instant1, timeUnit, zone) + assertEquals(instant4 - instant1, timeUnit.duration * diff.toDouble()) + assertEquals(instant4, instant1.plus(diff, timeUnit, zone)) } val period = DateTimePeriod(days = 1, hours = 1) diff --git a/core/commonTest/src/LocalDateTest.kt b/core/commonTest/src/LocalDateTest.kt index 626de415a..db8c0bb34 100644 --- a/core/commonTest/src/LocalDateTest.kt +++ b/core/commonTest/src/LocalDateTest.kt @@ -66,7 +66,6 @@ class LocalDateTest { checkComponents(LocalDate.parse("2016-01-31") + DatePeriod(months = 1), 2016, 2, 29) // assertFailsWith { startDate + CalendarPeriod(hours = 7) } // won't compile - assertFailsWith { startDate.plus(7, CalendarUnit.HOUR) } // assertFailsWith { startDate.plus(7, ChronoUnit.MINUTE) } // won't compile } @@ -74,9 +73,9 @@ class LocalDateTest { fun tomorrow() { val today = Clock.System.todayAt(TimeZone.SYSTEM) - val nextMonthPlusDay1 = today.plus(1, CalendarUnit.MONTH).plus(1, CalendarUnit.DAY) + val nextMonthPlusDay1 = today.plus(DateTimeUnit.MONTH).plus(1, DateTimeUnit.DAY) val nextMonthPlusDay2 = today + DatePeriod(months = 1, days = 1) - val nextMonthPlusDay3 = today.plus(1, CalendarUnit.DAY).plus(1, CalendarUnit.MONTH) + val nextMonthPlusDay3 = today.plus(DateTimeUnit.DAY).plus(1, DateTimeUnit.MONTH) } @Test @@ -89,7 +88,7 @@ class LocalDateTest { val days1 = Random.nextInt(-3652..3652) val days2 = Random.nextInt(-3652..3652) val ldtBefore = origin + DatePeriod(days = days1) - val ldtNow = origin.plus(days2, CalendarUnit.DAY) + val ldtNow = origin.plus(days2, DateTimeUnit.DAY) val diff = ldtNow - ldtBefore val ldtAfter = ldtBefore + diff @@ -133,11 +132,6 @@ class LocalDateTest { } } - val d1 = LocalDate(2012, 6, 21) - val d2 = LocalDate(2012, 7, 21) - - for (unit in listOf(CalendarUnit.HOUR, CalendarUnit.MINUTE, CalendarUnit.SECOND, CalendarUnit.NANOSECOND)) - assertFailsWith { d1.until(d2, unit) } } @Test diff --git a/core/jsMain/src/Instant.kt b/core/jsMain/src/Instant.kt index 67aafa402..a6e8ed535 100644 --- a/core/jsMain/src/Instant.kt +++ b/core/jsMain/src/Instant.kt @@ -77,27 +77,29 @@ public actual fun Instant.plus(period: DateTimePeriod, zone: TimeZone): Instant }.toInstant().let(::Instant) } -public actual fun Instant.plus(value: Int, unit: CalendarUnit, zone: TimeZone): Instant = +internal actual fun Instant.plus(value: Int, unit: CalendarUnit, zone: TimeZone): Instant = when (unit) { CalendarUnit.YEAR -> this.value.atZone(zone.zoneId).plusYears(value).toInstant() CalendarUnit.MONTH -> this.value.atZone(zone.zoneId).plusMonths(value).toInstant() - CalendarUnit.WEEK -> this.value.atZone(zone.zoneId).plusWeeks(value).let { it as ZonedDateTime }.toInstant() CalendarUnit.DAY -> this.value.atZone(zone.zoneId).plusDays(value).let { it as ZonedDateTime }.toInstant() CalendarUnit.HOUR -> this.value.atZone(zone.zoneId).plusHours(value).toInstant() CalendarUnit.MINUTE -> this.value.atZone(zone.zoneId).plusMinutes(value).toInstant() CalendarUnit.SECOND -> this.value.plusSeconds(value) + CalendarUnit.MILLISECOND -> this.value.plusMillis(value) + CalendarUnit.MICROSECOND -> this.value.plusNanos(value * 1000) CalendarUnit.NANOSECOND -> this.value.plusNanos(value) }.let(::Instant) -public actual fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone): Instant = +internal actual fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone): Instant = when (unit) { CalendarUnit.YEAR -> this.value.atZone(zone.zoneId).plusYears(value).toInstant() CalendarUnit.MONTH -> this.value.atZone(zone.zoneId).plusMonths(value).toInstant() - CalendarUnit.WEEK -> this.value.atZone(zone.zoneId).plusWeeks(value).let { it as ZonedDateTime }.toInstant() CalendarUnit.DAY -> this.value.atZone(zone.zoneId).plusDays(value).let { it as ZonedDateTime }.toInstant() CalendarUnit.HOUR -> this.value.atZone(zone.zoneId).plusHours(value).toInstant() CalendarUnit.MINUTE -> this.value.atZone(zone.zoneId).plusMinutes(value).toInstant() CalendarUnit.SECOND -> this.value.plusSeconds(value) + CalendarUnit.MILLISECOND -> this.value.plusMillis(value) + CalendarUnit.MICROSECOND -> this.value.plusSeconds(value / 1_000_000).plusNanos((value % 1_000_000).toInt() * 1000) CalendarUnit.NANOSECOND -> this.value.plusNanos(value) }.let(::Instant) @@ -115,7 +117,7 @@ public actual fun Instant.periodUntil(other: Instant, zone: TimeZone): DateTimeP } } -actual fun Instant.until(other: Instant, unit: CalendarUnit, zone: TimeZone): Long = +internal actual fun Instant.until(other: Instant, unit: CalendarUnit, zone: TimeZone): Long = until(other, unit.toChronoUnit(), zone.zoneId) private fun Instant.until(other: Instant, unit: ChronoUnit, zone: ZoneId): Long = @@ -124,10 +126,11 @@ private fun Instant.until(other: Instant, unit: ChronoUnit, zone: ZoneId): Long private fun CalendarUnit.toChronoUnit(): ChronoUnit = when(this) { CalendarUnit.YEAR -> ChronoUnit.YEARS CalendarUnit.MONTH -> ChronoUnit.MONTHS - CalendarUnit.WEEK -> ChronoUnit.WEEKS CalendarUnit.DAY -> ChronoUnit.DAYS CalendarUnit.HOUR -> ChronoUnit.HOURS CalendarUnit.MINUTE -> ChronoUnit.MINUTES CalendarUnit.SECOND -> ChronoUnit.SECONDS + CalendarUnit.MILLISECOND -> ChronoUnit.MILLIS + CalendarUnit.MICROSECOND -> ChronoUnit.MICROS CalendarUnit.NANOSECOND -> ChronoUnit.NANOS } diff --git a/core/jsMain/src/LocalDate.kt b/core/jsMain/src/LocalDate.kt index f575f45a3..3763d2f19 100644 --- a/core/jsMain/src/LocalDate.kt +++ b/core/jsMain/src/LocalDate.kt @@ -40,18 +40,19 @@ private fun LocalDate.plusNumber(value: Number, unit: CalendarUnit): LocalDate = when (unit) { CalendarUnit.YEAR -> this.value.plusYears(value) CalendarUnit.MONTH -> this.value.plusMonths(value) - CalendarUnit.WEEK -> this.value.plusWeeks(value) CalendarUnit.DAY -> this.value.plusDays(value) CalendarUnit.HOUR, CalendarUnit.MINUTE, CalendarUnit.SECOND, + CalendarUnit.MILLISECOND, + CalendarUnit.MICROSECOND, CalendarUnit.NANOSECOND -> throw IllegalArgumentException("Only date based units can be added to LocalDate") }.let(::LocalDate) -public actual fun LocalDate.plus(value: Long, unit: CalendarUnit): LocalDate = +internal actual fun LocalDate.plus(value: Long, unit: CalendarUnit): LocalDate = plusNumber(value.toDouble(), unit) -public actual fun LocalDate.plus(value: Int, unit: CalendarUnit): LocalDate = +internal actual fun LocalDate.plus(value: Int, unit: CalendarUnit): LocalDate = plusNumber(value, unit) diff --git a/core/jvmMain/src/Instant.kt b/core/jvmMain/src/Instant.kt index 1621400a6..521acc99a 100644 --- a/core/jvmMain/src/Instant.kt +++ b/core/jvmMain/src/Instant.kt @@ -71,18 +71,19 @@ public actual fun Instant.plus(period: DateTimePeriod, zone: TimeZone): Instant }.toInstant().let(::Instant) } -public actual fun Instant.plus(value: Int, unit: CalendarUnit, zone: TimeZone): Instant = +internal actual fun Instant.plus(value: Int, unit: CalendarUnit, zone: TimeZone): Instant = plus(value.toLong(), unit, zone) -public actual fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone): Instant = +internal actual fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone): Instant = when (unit) { CalendarUnit.YEAR -> this.value.atZone(zone.zoneId).plusYears(value).toInstant() CalendarUnit.MONTH -> this.value.atZone(zone.zoneId).plusMonths(value).toInstant() - CalendarUnit.WEEK -> this.value.atZone(zone.zoneId).plusWeeks(value).toInstant() CalendarUnit.DAY -> this.value.atZone(zone.zoneId).plusDays(value).toInstant() CalendarUnit.HOUR -> this.value.atZone(zone.zoneId).plusHours(value).toInstant() CalendarUnit.MINUTE -> this.value.atZone(zone.zoneId).plusMinutes(value).toInstant() CalendarUnit.SECOND -> this.value.plusSeconds(value) + CalendarUnit.MILLISECOND -> this.value.plusMillis(value) + CalendarUnit.MICROSECOND -> this.value.plusSeconds(value / 1_000_000).plusNanos((value % 1_000_000) * 1000) CalendarUnit.NANOSECOND -> this.value.plusNanos(value) }.let(::Instant) @@ -100,7 +101,7 @@ public actual fun Instant.periodUntil(other: Instant, zone: TimeZone): DateTimeP } } -actual fun Instant.until(other: Instant, unit: CalendarUnit, zone: TimeZone): Long = +internal actual fun Instant.until(other: Instant, unit: CalendarUnit, zone: TimeZone): Long = until(other, unit.toChronoUnit(), zone.zoneId) private fun Instant.until(other: Instant, unit: ChronoUnit, zone: ZoneId): Long = @@ -109,10 +110,11 @@ private fun Instant.until(other: Instant, unit: ChronoUnit, zone: ZoneId): Long private fun CalendarUnit.toChronoUnit(): ChronoUnit = when(this) { CalendarUnit.YEAR -> ChronoUnit.YEARS CalendarUnit.MONTH -> ChronoUnit.MONTHS - CalendarUnit.WEEK -> ChronoUnit.WEEKS CalendarUnit.DAY -> ChronoUnit.DAYS CalendarUnit.HOUR -> ChronoUnit.HOURS CalendarUnit.MINUTE -> ChronoUnit.MINUTES CalendarUnit.SECOND -> ChronoUnit.SECONDS + CalendarUnit.MILLISECOND -> ChronoUnit.MILLIS + CalendarUnit.MICROSECOND -> ChronoUnit.MICROS CalendarUnit.NANOSECOND -> ChronoUnit.NANOS } diff --git a/core/jvmMain/src/LocalDate.kt b/core/jvmMain/src/LocalDate.kt index c80f4f39d..c815b6021 100644 --- a/core/jvmMain/src/LocalDate.kt +++ b/core/jvmMain/src/LocalDate.kt @@ -37,19 +37,20 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa } -public actual fun LocalDate.plus(value: Long, unit: CalendarUnit): LocalDate = +internal actual fun LocalDate.plus(value: Long, unit: CalendarUnit): LocalDate = when (unit) { CalendarUnit.YEAR -> this.value.plusYears(value) CalendarUnit.MONTH -> this.value.plusMonths(value) - CalendarUnit.WEEK -> this.value.plusWeeks(value) CalendarUnit.DAY -> this.value.plusDays(value) CalendarUnit.HOUR, CalendarUnit.MINUTE, CalendarUnit.SECOND, + CalendarUnit.MILLISECOND, + CalendarUnit.MICROSECOND, CalendarUnit.NANOSECOND -> throw IllegalArgumentException("Only date based units can be added to LocalDate") }.let(::LocalDate) -public actual fun LocalDate.plus(value: Int, unit: CalendarUnit): LocalDate = +internal actual fun LocalDate.plus(value: Int, unit: CalendarUnit): LocalDate = plus(value.toLong(), unit) public actual operator fun LocalDate.plus(period: DatePeriod): LocalDate = diff --git a/core/nativeMain/src/Instant.kt b/core/nativeMain/src/Instant.kt index eb255b249..2629b6c84 100644 --- a/core/nativeMain/src/Instant.kt +++ b/core/nativeMain/src/Instant.kt @@ -299,14 +299,13 @@ actual fun Instant.plus(period: DateTimePeriod, zone: TimeZone): Instant = try { throw DateTimeArithmeticException("Boundaries of Instant exceeded when adding CalendarPeriod", e) } -actual fun Instant.plus(value: Int, unit: CalendarUnit, zone: TimeZone): Instant = +internal actual fun Instant.plus(value: Int, unit: CalendarUnit, zone: TimeZone): Instant = plus(value.toLong(), unit, zone) -actual fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone): Instant = try { +internal actual fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone): Instant = try { when (unit) { CalendarUnit.YEAR -> toZonedLocalDateTimeFailing(zone).plusYears(value).toInstant() CalendarUnit.MONTH -> toZonedLocalDateTimeFailing(zone).plusMonths(value).toInstant() - CalendarUnit.WEEK -> toZonedLocalDateTimeFailing(zone).plusDays(safeMultiply(value, 7)).toInstant() CalendarUnit.DAY -> toZonedLocalDateTimeFailing(zone).plusDays(value).toInstant() /* From org.threeten.bp.ZonedDateTime#plusHours: the time is added to the raw LocalDateTime, then org.threeten.bp.ZonedDateTime#create is called on the absolute instant @@ -327,6 +326,8 @@ actual fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone): Instan CalendarUnit.HOUR -> plus(safeMultiply(value, SECONDS_PER_HOUR.toLong()), 0).check(zone) CalendarUnit.MINUTE -> plus(safeMultiply(value, SECONDS_PER_MINUTE.toLong()), 0).check(zone) CalendarUnit.SECOND -> plus(value, 0).check(zone) + CalendarUnit.MILLISECOND -> plus(value / MILLIS_PER_ONE, (value % MILLIS_PER_ONE) * NANOS_PER_MILLI).check(zone) + CalendarUnit.MICROSECOND -> plus(value / MICROS_PER_ONE, (value % MICROS_PER_ONE) * NANOS_PER_MICRO).check(zone) CalendarUnit.NANOSECOND -> plus(0, value).check(zone) } } catch (e: ArithmeticException) { @@ -351,7 +352,7 @@ actual fun Instant.periodUntil(other: Instant, zone: TimeZone): DateTimePeriod { } } -actual fun Instant.until(other: Instant, unit: CalendarUnit, zone: TimeZone): Long = +internal actual fun Instant.until(other: Instant, unit: CalendarUnit, zone: TimeZone): Long = try { toZonedLocalDateTimeFailing(zone).until(other.toZonedLocalDateTimeFailing(zone), unit) } catch (e: ArithmeticException) { diff --git a/core/nativeMain/src/LocalDate.kt b/core/nativeMain/src/LocalDate.kt index 89fa8ea16..99b8697d5 100644 --- a/core/nativeMain/src/LocalDate.kt +++ b/core/nativeMain/src/LocalDate.kt @@ -249,13 +249,12 @@ public actual class LocalDate actual constructor(actual val year: Int, actual va } } -actual fun LocalDate.plus(value: Long, unit: CalendarUnit): LocalDate = +internal actual fun LocalDate.plus(value: Long, unit: CalendarUnit): LocalDate = when (unit) { - CalendarUnit.YEAR, CalendarUnit.MONTH, CalendarUnit.WEEK, CalendarUnit.DAY -> try { + CalendarUnit.YEAR, CalendarUnit.MONTH, CalendarUnit.DAY -> try { when (unit) { CalendarUnit.YEAR -> plusYears(value) CalendarUnit.MONTH -> plusMonths(value) - CalendarUnit.WEEK -> plusWeeks(value) CalendarUnit.DAY -> plusDays(value) else -> throw RuntimeException("impossible") } @@ -267,10 +266,12 @@ actual fun LocalDate.plus(value: Long, unit: CalendarUnit): LocalDate = CalendarUnit.HOUR, CalendarUnit.MINUTE, CalendarUnit.SECOND, + CalendarUnit.MILLISECOND, + CalendarUnit.MICROSECOND, CalendarUnit.NANOSECOND -> throw IllegalArgumentException("Only date based units can be added to LocalDate") } -actual fun LocalDate.plus(value: Int, unit: CalendarUnit): LocalDate = +internal actual fun LocalDate.plus(value: Int, unit: CalendarUnit): LocalDate = plus(value.toLong(), unit) actual operator fun LocalDate.plus(period: DatePeriod): LocalDate = @@ -311,12 +312,13 @@ internal fun LocalDate.longMonthsUntil(other: LocalDate): Long { internal fun LocalDate.longUntil(end: LocalDate, unit: CalendarUnit): Long = when (unit) { CalendarUnit.DAY -> longDaysUntil(end) - CalendarUnit.WEEK -> longDaysUntil(end) / 7 CalendarUnit.MONTH -> longMonthsUntil(end) CalendarUnit.YEAR -> longMonthsUntil(end) / 12 CalendarUnit.HOUR, CalendarUnit.MINUTE, CalendarUnit.SECOND, + CalendarUnit.MILLISECOND, + CalendarUnit.MICROSECOND, CalendarUnit.NANOSECOND -> throw IllegalArgumentException("Unsupported unit: $unit") } diff --git a/core/nativeMain/src/LocalDateTime.kt b/core/nativeMain/src/LocalDateTime.kt index 1acea5797..eea8420c5 100644 --- a/core/nativeMain/src/LocalDateTime.kt +++ b/core/nativeMain/src/LocalDateTime.kt @@ -98,7 +98,7 @@ actual fun Instant.offsetAt(timeZone: TimeZone): ZoneOffset = /** @throws ArithmeticException on arithmetic overflow. Only possible for time-based units. */ internal fun LocalDateTime.until(other: LocalDateTime, unit: CalendarUnit): Long = when (unit) { - CalendarUnit.YEAR, CalendarUnit.MONTH, CalendarUnit.WEEK, CalendarUnit.DAY -> { + CalendarUnit.YEAR, CalendarUnit.MONTH, CalendarUnit.DAY -> { var endDate: LocalDate = other.date if (endDate > date && other.time < time) { endDate = endDate.plusDays(-1) // won't throw: endDate - date >= 1 @@ -107,7 +107,7 @@ internal fun LocalDateTime.until(other: LocalDateTime, unit: CalendarUnit): Long } date.longUntil(endDate, unit) } - CalendarUnit.HOUR, CalendarUnit.MINUTE, CalendarUnit.SECOND, CalendarUnit.NANOSECOND -> { + CalendarUnit.HOUR, CalendarUnit.MINUTE, CalendarUnit.SECOND, CalendarUnit.MILLISECOND, CalendarUnit.MICROSECOND, CalendarUnit.NANOSECOND -> { var daysUntil = date.longDaysUntil(other.date) var timeUntil: Long = other.time.toNanoOfDay() - time.toNanoOfDay() if (daysUntil > 0 && timeUntil < 0) { @@ -124,6 +124,10 @@ internal fun LocalDateTime.until(other: LocalDateTime, unit: CalendarUnit): Long safeMultiply(daysUntil, MINUTES_PER_DAY.toLong())) CalendarUnit.SECOND -> safeAdd(nanos / NANOS_PER_ONE, safeMultiply(daysUntil, SECONDS_PER_DAY.toLong())) + CalendarUnit.MILLISECOND -> safeAdd(nanos / NANOS_PER_MILLI, + safeMultiply(daysUntil, SECONDS_PER_DAY.toLong() * MILLIS_PER_ONE)) + CalendarUnit.MICROSECOND -> safeAdd(nanos / NANOS_PER_MICRO, + safeMultiply(daysUntil, SECONDS_PER_DAY.toLong() * MICROS_PER_ONE)) CalendarUnit.NANOSECOND -> safeAdd(nanos, safeMultiply(daysUntil, NANOS_PER_DAY)) else -> throw RuntimeException("impossible") } diff --git a/core/nativeMain/src/Util.kt b/core/nativeMain/src/Util.kt index 8b8266493..fd4dc4b6d 100644 --- a/core/nativeMain/src/Util.kt +++ b/core/nativeMain/src/Util.kt @@ -14,7 +14,9 @@ import platform.posix.* */ internal const val NANOS_PER_MILLI = 1_000_000 +internal const val NANOS_PER_MICRO = 1_000 internal const val MILLIS_PER_ONE = 1_000 +internal const val MICROS_PER_ONE = 1_000_000 internal const val NANOS_PER_ONE = 1_000_000_000 /** diff --git a/core/nativeMain/src/ZonedDateTime.kt b/core/nativeMain/src/ZonedDateTime.kt index 61f5e3031..16c0678da 100644 --- a/core/nativeMain/src/ZonedDateTime.kt +++ b/core/nativeMain/src/ZonedDateTime.kt @@ -73,11 +73,11 @@ internal fun Instant.toZonedLocalDateTime(zone: TimeZone): ZonedDateTime { internal fun ZonedDateTime.until(other: ZonedDateTime, unit: CalendarUnit): Long = when (unit) { // if the time unit is date-based, the offsets are disregarded and only the dates and times are compared. - CalendarUnit.YEAR, CalendarUnit.MONTH, CalendarUnit.WEEK, CalendarUnit.DAY -> { + CalendarUnit.YEAR, CalendarUnit.MONTH, CalendarUnit.DAY -> { dateTime.until(other.dateTime, unit) } // if the time unit is not date-based, we need to make sure that [other] is at the same offset as [this]. - CalendarUnit.HOUR, CalendarUnit.MINUTE, CalendarUnit.SECOND, CalendarUnit.NANOSECOND -> { + CalendarUnit.HOUR, CalendarUnit.MINUTE, CalendarUnit.SECOND, CalendarUnit.MILLISECOND, CalendarUnit.MICROSECOND, CalendarUnit.NANOSECOND -> { val offsetDiff = offset.totalSeconds - other.offset.totalSeconds val otherLdtAdjusted = try { other.dateTime.plusSeconds(offsetDiff.toLong()) From 1beb5ea84c8436ddb29bdb60bfab2f8f6c4ef1e9 Mon Sep 17 00:00:00 2001 From: Ilya Gorbunov Date: Thu, 16 Jul 2020 01:37:42 +0300 Subject: [PATCH 09/10] Implement DateTimeUnit.toString and add tests --- core/commonMain/src/DateTimeUnit.kt | 17 ++++++- core/commonTest/src/DateTimeUnitTest.kt | 63 +++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 core/commonTest/src/DateTimeUnitTest.kt diff --git a/core/commonMain/src/DateTimeUnit.kt b/core/commonMain/src/DateTimeUnit.kt index bedc4efe6..ca6b7341b 100644 --- a/core/commonMain/src/DateTimeUnit.kt +++ b/core/commonMain/src/DateTimeUnit.kt @@ -9,7 +9,6 @@ import kotlin.time.Duration import kotlin.time.ExperimentalTime import kotlin.time.nanoseconds -// TODO: toString sealed class DateTimeUnit { abstract operator fun times(scalar: Int): DateTimeUnit @@ -60,6 +59,8 @@ sealed class DateTimeUnit { this === other || (other is TimeBased && this.nanoseconds == other.nanoseconds) override fun hashCode(): Int = nanoseconds.toInt() xor (nanoseconds shr Int.SIZE_BITS).toInt() + + override fun toString(): String = formatToString(calendarScale, calendarUnit.toString()) } sealed class DateBased : DateTimeUnit() { @@ -78,6 +79,11 @@ sealed class DateTimeUnit { this === other || (other is DayBased && this.days == other.days) override fun hashCode(): Int = days xor 0x10000 + + override fun toString(): String = if (days % 7 == 0) + formatToString(days / 7, "WEEK") + else + formatToString(days, "DAY") } class MonthBased(val months: Int) : DateBased() { init { @@ -93,9 +99,18 @@ sealed class DateTimeUnit { this === other || (other is MonthBased && this.months == other.months) override fun hashCode(): Int = months xor 0x20000 + + override fun toString(): String = when { + months % 12_00 == 0 -> formatToString(months / 12_00, "CENTURY") + months % 12 == 0 -> formatToString(months / 12, "YEAR") + months % 3 == 0 -> formatToString(months / 3, "QUARTER") + else -> formatToString(months, "MONTH") + } } } + protected fun formatToString(value: Int, unit: String): String = if (value == 1) unit else "$value-$unit" + protected fun formatToString(value: Long, unit: String): String = if (value == 1L) unit else "$value-$unit" companion object { val NANOSECOND = TimeBased(nanoseconds = 1) diff --git a/core/commonTest/src/DateTimeUnitTest.kt b/core/commonTest/src/DateTimeUnitTest.kt new file mode 100644 index 000000000..02afa48d6 --- /dev/null +++ b/core/commonTest/src/DateTimeUnitTest.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2019-2020 JetBrains s.r.o. + * Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file. + */ + +package kotlinx.datetime.test + +import kotlinx.datetime.* +import kotlin.test.* + +class DateTimeUnitTest { + val U = DateTimeUnit // alias + + @Test + fun baseUnits() { + val baseUnitProps = listOf( + U::NANOSECOND, U::MICROSECOND, U::MILLISECOND, U::SECOND, U::MINUTE, U::HOUR, + U::DAY, U::WEEK, U::MONTH, U::QUARTER, U::YEAR, U::CENTURY + ) + for (unit in baseUnitProps) { + assertEquals(unit.name, unit.get().toString()) + } + + val allUnits = baseUnitProps.map { it.get() } + + assertEquals(allUnits.size, allUnits.map { it.hashCode() }.distinct().size) + + for (unit in allUnits) { + assertSame(unit, allUnits.singleOrNull { it == unit }) // should be no not same, but equal + } + } + + @Test + fun productUnits() { + ensureEquality(U.MICROSECOND, U.NANOSECOND * 1000, "MICROSECOND") + ensureEquality(U.MICROSECOND * 2000, U.NANOSECOND * 2000_000, "2-MILLISECOND") + + val twoDays = U.DAY * 2 + assertEquals("2-DAY", twoDays.toString()) + + val twoWeeks = U.WEEK * 2 + assertEquals("2-WEEK", twoWeeks.toString()) + assertNotEquals(twoDays, twoWeeks) + + val fortnight = U.DAY * 14 + ensureEquality(twoWeeks, fortnight, "2-WEEK") + + val fourQuarters = U.QUARTER * 4 + ensureEquality(U.YEAR, fourQuarters, "YEAR") + + val twoFourMonths = U.MONTH * 24 + val twoYears = U.YEAR * 2 + ensureEquality(twoYears, twoFourMonths, "2-YEAR") + } + + private fun ensureEquality(v1: Any, v2: Any, expectToString: String) { + assertEquals(v1, v2) + assertEquals(v1.hashCode(), v2.hashCode()) + assertEquals(v1.toString(), v2.toString()) + assertEquals(expectToString, v2.toString()) + } + +} \ No newline at end of file From b4b0c01834eb2ef63e6140a0bf880781b0980baa Mon Sep 17 00:00:00 2001 From: Ilya Gorbunov Date: Thu, 16 Jul 2020 05:34:28 +0300 Subject: [PATCH 10/10] Cleanup some CalendarUnit usages --- core/commonMain/src/Instant.kt | 28 ++++++++++++++++------------ core/jsMain/src/Instant.kt | 18 ++---------------- core/jvmMain/src/Instant.kt | 7 ++----- core/nativeMain/src/Instant.kt | 8 +++----- core/nativeMain/src/ZonedDateTime.kt | 2 ++ 5 files changed, 25 insertions(+), 38 deletions(-) diff --git a/core/commonMain/src/Instant.kt b/core/commonMain/src/Instant.kt index 7b13f9b47..8164b2441 100644 --- a/core/commonMain/src/Instant.kt +++ b/core/commonMain/src/Instant.kt @@ -76,11 +76,6 @@ public fun String.toInstant(): Instant = Instant.parse(this) */ public expect fun Instant.plus(period: DateTimePeriod, zone: TimeZone): Instant -/** - * @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. - */ -internal expect fun Instant.plus(value: Int, unit: CalendarUnit, zone: TimeZone): Instant - /** * @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. */ @@ -97,7 +92,7 @@ public expect fun Instant.periodUntil(other: Instant, zone: TimeZone): DateTimeP * * @throws DateTimeArithmeticException if this [Instant] or [other] is too large to fit in [LocalDateTime]. */ -internal expect fun Instant.until(other: Instant, unit: CalendarUnit, zone: TimeZone): Long +public expect fun Instant.until(other: Instant, unit: DateTimeUnit, zone: TimeZone): Long /** * The return value is clamped to [Int.MAX_VALUE] or [Int.MIN_VALUE] if the result would otherwise cause an arithmetic @@ -106,7 +101,7 @@ internal expect fun Instant.until(other: Instant, unit: CalendarUnit, zone: Time * @throws DateTimeArithmeticException if this [Instant] or [other] is too large to fit in [LocalDateTime]. */ public fun Instant.daysUntil(other: Instant, zone: TimeZone): Int = - until(other, CalendarUnit.DAY, zone).clampToInt() + until(other, DateTimeUnit.DAY, zone).clampToInt() /** * The return value is clamped to [Int.MAX_VALUE] or [Int.MIN_VALUE] if the result would otherwise cause an arithmetic @@ -115,7 +110,7 @@ public fun Instant.daysUntil(other: Instant, zone: TimeZone): Int = * @throws DateTimeArithmeticException if this [Instant] or [other] is too large to fit in [LocalDateTime]. */ public fun Instant.monthsUntil(other: Instant, zone: TimeZone): Int = - until(other, CalendarUnit.MONTH, zone).clampToInt() + until(other, DateTimeUnit.MONTH, zone).clampToInt() /** * The return value is clamped to [Int.MAX_VALUE] or [Int.MIN_VALUE] if the result would otherwise cause an arithmetic @@ -124,7 +119,7 @@ public fun Instant.monthsUntil(other: Instant, zone: TimeZone): Int = * @throws DateTimeArithmeticException if this [Instant] or [other] is too large to fit in [LocalDateTime]. */ public fun Instant.yearsUntil(other: Instant, zone: TimeZone): Int = - until(other, CalendarUnit.YEAR, zone).clampToInt() + until(other, DateTimeUnit.YEAR, zone).clampToInt() // TODO: move to internal utils internal fun Long.clampToInt(): Int = @@ -133,15 +128,24 @@ internal fun Long.clampToInt(): Int = public fun Instant.minus(other: Instant, zone: TimeZone): DateTimePeriod = other.periodUntil(this, zone) - +/** + * @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. + */ public fun Instant.plus(unit: DateTimeUnit, zone: TimeZone): Instant = plus(unit.calendarScale, unit.calendarUnit, zone) + +// TODO: safeMultiply +/** + * @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. + */ public fun Instant.plus(value: Int, unit: DateTimeUnit, zone: TimeZone): Instant = plus(value * unit.calendarScale, unit.calendarUnit, zone) + +/** + * @throws DateTimeArithmeticException if this value or the result is too large to fit in [LocalDateTime]. + */ public fun Instant.plus(value: Long, unit: DateTimeUnit, zone: TimeZone): Instant = plus(value * unit.calendarScale, unit.calendarUnit, zone) -public fun Instant.until(other: Instant, unit: DateTimeUnit, zone: TimeZone): Long = - until(other, unit.calendarUnit, zone) / unit.calendarScale public fun Instant.minus(other: Instant, unit: DateTimeUnit, zone: TimeZone): Long = other.until(this, unit, zone) diff --git a/core/jsMain/src/Instant.kt b/core/jsMain/src/Instant.kt index a6e8ed535..aaa972ce5 100644 --- a/core/jsMain/src/Instant.kt +++ b/core/jsMain/src/Instant.kt @@ -77,19 +77,6 @@ public actual fun Instant.plus(period: DateTimePeriod, zone: TimeZone): Instant }.toInstant().let(::Instant) } -internal actual fun Instant.plus(value: Int, unit: CalendarUnit, zone: TimeZone): Instant = - when (unit) { - CalendarUnit.YEAR -> this.value.atZone(zone.zoneId).plusYears(value).toInstant() - CalendarUnit.MONTH -> this.value.atZone(zone.zoneId).plusMonths(value).toInstant() - CalendarUnit.DAY -> this.value.atZone(zone.zoneId).plusDays(value).let { it as ZonedDateTime }.toInstant() - CalendarUnit.HOUR -> this.value.atZone(zone.zoneId).plusHours(value).toInstant() - CalendarUnit.MINUTE -> this.value.atZone(zone.zoneId).plusMinutes(value).toInstant() - CalendarUnit.SECOND -> this.value.plusSeconds(value) - CalendarUnit.MILLISECOND -> this.value.plusMillis(value) - CalendarUnit.MICROSECOND -> this.value.plusNanos(value * 1000) - CalendarUnit.NANOSECOND -> this.value.plusNanos(value) - }.let(::Instant) - internal actual fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone): Instant = when (unit) { CalendarUnit.YEAR -> this.value.atZone(zone.zoneId).plusYears(value).toInstant() @@ -116,9 +103,8 @@ public actual fun Instant.periodUntil(other: Instant, zone: TimeZone): DateTimeP return DateTimePeriod((months / 12).toInt(), (months % 12).toInt(), days.toInt(), hours, minutes, seconds.toLong(), nanoseconds.toLong()) } } - -internal actual fun Instant.until(other: Instant, unit: CalendarUnit, zone: TimeZone): Long = - until(other, unit.toChronoUnit(), zone.zoneId) +public actual fun Instant.until(other: Instant, unit: DateTimeUnit, zone: TimeZone): Long = + until(other, unit.calendarUnit.toChronoUnit(), zone.zoneId) / unit.calendarScale private fun Instant.until(other: Instant, unit: ChronoUnit, zone: ZoneId): Long = this.value.atZone(zone).until(other.value.atZone(zone), unit).toLong() diff --git a/core/jvmMain/src/Instant.kt b/core/jvmMain/src/Instant.kt index 521acc99a..e82f46509 100644 --- a/core/jvmMain/src/Instant.kt +++ b/core/jvmMain/src/Instant.kt @@ -71,9 +71,6 @@ public actual fun Instant.plus(period: DateTimePeriod, zone: TimeZone): Instant }.toInstant().let(::Instant) } -internal actual fun Instant.plus(value: Int, unit: CalendarUnit, zone: TimeZone): Instant = - plus(value.toLong(), unit, zone) - internal actual fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone): Instant = when (unit) { CalendarUnit.YEAR -> this.value.atZone(zone.zoneId).plusYears(value).toInstant() @@ -101,8 +98,8 @@ public actual fun Instant.periodUntil(other: Instant, zone: TimeZone): DateTimeP } } -internal actual fun Instant.until(other: Instant, unit: CalendarUnit, zone: TimeZone): Long = - until(other, unit.toChronoUnit(), zone.zoneId) +public actual fun Instant.until(other: Instant, unit: DateTimeUnit, zone: TimeZone): Long = + until(other, unit.calendarUnit.toChronoUnit(), zone.zoneId) / unit.calendarScale private fun Instant.until(other: Instant, unit: ChronoUnit, zone: ZoneId): Long = this.value.atZone(zone).until(other.value.atZone(zone), unit) diff --git a/core/nativeMain/src/Instant.kt b/core/nativeMain/src/Instant.kt index 2629b6c84..252e42243 100644 --- a/core/nativeMain/src/Instant.kt +++ b/core/nativeMain/src/Instant.kt @@ -299,9 +299,6 @@ actual fun Instant.plus(period: DateTimePeriod, zone: TimeZone): Instant = try { throw DateTimeArithmeticException("Boundaries of Instant exceeded when adding CalendarPeriod", e) } -internal actual fun Instant.plus(value: Int, unit: CalendarUnit, zone: TimeZone): Instant = - plus(value.toLong(), unit, zone) - internal actual fun Instant.plus(value: Long, unit: CalendarUnit, zone: TimeZone): Instant = try { when (unit) { CalendarUnit.YEAR -> toZonedLocalDateTimeFailing(zone).plusYears(value).toInstant() @@ -352,9 +349,10 @@ actual fun Instant.periodUntil(other: Instant, zone: TimeZone): DateTimePeriod { } } -internal actual fun Instant.until(other: Instant, unit: CalendarUnit, zone: TimeZone): Long = +public actual fun Instant.until(other: Instant, unit: DateTimeUnit, zone: TimeZone): Long = try { - toZonedLocalDateTimeFailing(zone).until(other.toZonedLocalDateTimeFailing(zone), unit) + // TODO: inline 'until' here and simplify operation for time-based units + toZonedLocalDateTimeFailing(zone).until(other.toZonedLocalDateTimeFailing(zone), unit.calendarUnit) / unit.calendarScale } catch (e: ArithmeticException) { if (this < other) Long.MAX_VALUE else Long.MIN_VALUE } diff --git a/core/nativeMain/src/ZonedDateTime.kt b/core/nativeMain/src/ZonedDateTime.kt index 16c0678da..17c76ac1b 100644 --- a/core/nativeMain/src/ZonedDateTime.kt +++ b/core/nativeMain/src/ZonedDateTime.kt @@ -70,6 +70,8 @@ internal fun Instant.toZonedLocalDateTime(zone: TimeZone): ZonedDateTime { * @throws DateTimeArithmeticException if setting [other] to the offset of [this] leads to exceeding boundaries of * [LocalDateTime]. */ + +// TODO: use DateTimeUnit internal fun ZonedDateTime.until(other: ZonedDateTime, unit: CalendarUnit): Long = when (unit) { // if the time unit is date-based, the offsets are disregarded and only the dates and times are compared.