diff --git a/core/commonMain/src/CalendarPeriod.kt b/core/commonMain/src/DateTimePeriod.kt similarity index 58% rename from core/commonMain/src/CalendarPeriod.kt rename to core/commonMain/src/DateTimePeriod.kt index 947e64db5..f444a1e31 100644 --- a/core/commonMain/src/CalendarPeriod.kt +++ b/core/commonMain/src/DateTimePeriod.kt @@ -8,21 +8,21 @@ package kotlinx.datetime import kotlin.time.Duration import kotlin.time.ExperimentalTime -typealias Period = CalendarPeriod - -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) - } + +// TODO: could be error-prone without explicitly named params +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') @@ -43,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 @@ -66,21 +66,50 @@ class CalendarPeriod(val years: Int = 0, val months: Int = 0, val days: Int = 0, result = 31 * result + nanoseconds.hashCode() return result } -} -inline fun period(builder: CalendarPeriod.Builder.() -> CalendarPeriod): CalendarPeriod = CalendarPeriod.Builder.builder() + // TODO: parsing from iso string +} -val Int.calendarDays: CalendarPeriod get() = CalendarPeriod(days = this) -val Int.calendarMonths: CalendarPeriod get() = CalendarPeriod(months = this) -val Int.calendarYears: CalendarPeriod get() = CalendarPeriod(years = this) +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, @@ -90,13 +119,9 @@ operator fun CalendarPeriod.plus(other: CalendarPeriod): CalendarPeriod = Calend this.nanoseconds + other.nanoseconds ) -enum class CalendarUnit { - YEAR, - MONTH, - WEEK, - DAY, - HOUR, - MINUTE, - SECOND, - NANOSECOND -} +operator fun DatePeriod.plus(other: DatePeriod): DatePeriod = DatePeriod( + this.years + other.years, + this.months + other.months, + this.days + other.days +) + diff --git a/core/commonMain/src/DateTimeUnit.kt b/core/commonMain/src/DateTimeUnit.kt new file mode 100644 index 000000000..ca6b7341b --- /dev/null +++ b/core/commonMain/src/DateTimeUnit.kt @@ -0,0 +1,142 @@ +/* + * 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 + +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 + } + 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 + } + } + } + + 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() + + override fun toString(): String = formatToString(calendarScale, calendarUnit.toString()) + } + + 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 + + override fun toString(): String = if (days % 7 == 0) + formatToString(days / 7, "WEEK") + else + formatToString(days, "DAY") + } + 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 + + 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) + 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 + } +} + + +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 b58069fce..8164b2441 100644 --- a/core/commonMain/src/Instant.kt +++ b/core/commonMain/src/Instant.kt @@ -74,22 +74,17 @@ 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]. */ -public 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]. */ -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 @@ -97,7 +92,7 @@ public expect fun Instant.periodUntil(other: Instant, zone: TimeZone): CalendarP * * @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 +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 @@ public expect fun Instant.until(other: Instant, unit: CalendarUnit, zone: TimeZo * @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,33 @@ 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() -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): 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.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 65c4fa948..652543c6a 100644 --- a/core/commonMain/src/LocalDate.kt +++ b/core/commonMain/src/LocalDate.kt @@ -38,23 +38,39 @@ 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. * @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 +public expect fun LocalDate.yearsUntil(other: LocalDate): Int + +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: 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/CalendarPeriodTest.kt b/core/commonTest/src/CalendarPeriodTest.kt deleted file mode 100644 index f717b25e9..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", 1.calendarYears.toString()) - assertEquals("P1Y1M", CalendarPeriod(years = 1, months = 1).toString()) - assertEquals("P11M", 11.calendarMonths.toString()) - assertEquals("P14M", 14.calendarMonths.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/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 diff --git a/core/commonTest/src/InstantTest.kt b/core/commonTest/src/InstantTest.kt index c26555edb..8700bf8b8 100644 --- a/core/commonTest/src/InstantTest.kt +++ b/core/commonTest/src/InstantTest.kt @@ -88,21 +88,40 @@ 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)) + assertEquals(24, instant1.until(instant2, DateTimeUnit.HOUR, zone)) + assertEquals(24, instant2.minus(instant1, DateTimeUnit.HOUR, zone)) - val instant3 = instant1.plus(1, CalendarUnit.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.until(instant3, DateTimeUnit.DAY, zone)) assertEquals(1, instant1.daysUntil(instant3, 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) + 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.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 = 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)) + val period = DateTimePeriod(days = 1, hours = 1) + val instant5 = instant1.plus(period, zone) + checkComponents(instant5.toLocalDateTime(zone), 2019, 10, 28, 3, 59) + assertEquals(period, instant1.periodUntil(instant5, zone)) + assertEquals(period, instant5.minus(instant1, zone)) + assertEquals(26.hours, instant5.minus(instant1)) } @OptIn(ExperimentalTime::class) @@ -215,7 +234,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 3bd5bff70..db8c0bb34 100644 --- a/core/commonTest/src/LocalDateTest.kt +++ b/core/commonTest/src/LocalDateTest.kt @@ -59,35 +59,36 @@ 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, DateTimeUnit.DAY), 2016, 3, 1) + checkComponents(startDate.plus(DateTimeUnit.YEAR), 2017, 2, 28) + checkComponents(startDate + DatePeriod(years = 4), 2020, 2, 29) - checkComponents(LocalDate.parse("2016-01-31") + 1.calendarMonths, 2016, 2, 29) + checkComponents(LocalDate.parse("2016-01-31") + DatePeriod(months = 1), 2016, 2, 29) - assertFailsWith { startDate + CalendarPeriod(hours = 7) } - assertFailsWith { startDate.plus(7, CalendarUnit.HOUR) } +// assertFailsWith { startDate + CalendarPeriod(hours = 7) } // won't compile +// assertFailsWith { startDate.plus(7, ChronoUnit.MINUTE) } // won't compile } @Test 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(DateTimeUnit.MONTH).plus(1, DateTimeUnit.DAY) + val nextMonthPlusDay2 = today + DatePeriod(months = 1, days = 1) + val nextMonthPlusDay3 = today.plus(DateTimeUnit.DAY).plus(1, DateTimeUnit.MONTH) } @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) val days2 = Random.nextInt(-3652..3652) - val ldtBefore = origin + days1.calendarDays - val ldtNow = origin + days2.calendarDays + val ldtBefore = origin + DatePeriod(days = days1) + val ldtNow = origin.plus(days2, DateTimeUnit.DAY) val diff = ldtNow - ldtBefore val ldtAfter = ldtBefore + diff @@ -96,6 +97,43 @@ class LocalDateTest { } } + // based on threetenbp test for until() + @Test + fun until() { + val data = listOf( + 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 + 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_ON_SEALED_CLASS") + when (unit) { + 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)) + } + } + + } + @Test fun invalidDate() { assertFailsWith { LocalDate(2007, 2, 29) } diff --git a/core/jsMain/src/Instant.kt b/core/jsMain/src/Instant.kt index 8310cfd4b..aaa972ce5 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 @@ -77,32 +77,21 @@ public actual fun Instant.plus(period: CalendarPeriod, zone: TimeZone): Instant }.toInstant().let(::Instant) } -public actual fun Instant.plus(value: Int, 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.NANOSECOND -> this.value.plusNanos(value) - }.let(::Instant) - -public 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) @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,12 +100,11 @@ 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()) } } - -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() @@ -124,10 +112,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 53376b482..3763d2f19 100644 --- a/core/jsMain/src/LocalDate.kt +++ b/core/jsMain/src/LocalDate.kt @@ -40,28 +40,25 @@ 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 = - plusNumber(value, unit) +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) -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,11 +68,20 @@ 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) -} \ No newline at end of file + return DatePeriod(months / 12, months % 12, days) +} + +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/Instant.kt b/core/jvmMain/src/Instant.kt index f64739e37..e82f46509 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 @@ -71,23 +71,21 @@ public actual fun Instant.plus(period: CalendarPeriod, zone: TimeZone): Instant }.toInstant().let(::Instant) } -public 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) @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,12 +94,12 @@ 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()) } } -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) @@ -109,10 +107,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 20c903590..c815b6021 100644 --- a/core/jvmMain/src/LocalDate.kt +++ b/core/jvmMain/src/LocalDate.kt @@ -37,27 +37,24 @@ 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: 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,11 +63,20 @@ 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()) -} \ No newline at end of file + return DatePeriod((months / 12).toInt(), (months % 12).toInt(), days.toInt()) +} + +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/Instant.kt b/core/nativeMain/src/Instant.kt index 18a94a510..252e42243 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 } @@ -299,14 +299,10 @@ actual fun Instant.plus(period: CalendarPeriod, 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 = - 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 +323,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) { @@ -336,7 +334,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,13 +345,14 @@ 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()) } } -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/LocalDate.kt b/core/nativeMain/src/LocalDate.kt index d0564099c..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,18 +266,16 @@ 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: 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 } @@ -291,35 +288,42 @@ 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.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") } -actual fun LocalDate.periodUntil(other: LocalDate): CalendarPeriod { - val months = until(other, CalendarUnit.MONTH) - val days = plusMonths(months).until(other, CalendarUnit.DAY) - return CalendarPeriod((months / 12).toInt(), (months % 12).toInt(), days.toInt()) +actual fun LocalDate.periodUntil(other: LocalDate): DatePeriod { + val months = longUntil(other, CalendarUnit.MONTH) + val days = plusMonths(months).longUntil(other, CalendarUnit.DAY) + return DatePeriod((months / 12).toInt(), (months % 12).toInt(), days.toInt()) } diff --git a/core/nativeMain/src/LocalDateTime.kt b/core/nativeMain/src/LocalDateTime.kt index 7199a0fba..eea8420c5 100644 --- a/core/nativeMain/src/LocalDateTime.kt +++ b/core/nativeMain/src/LocalDateTime.kt @@ -98,17 +98,17 @@ 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 } 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) + 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) { daysUntil-- @@ -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..17c76ac1b 100644 --- a/core/nativeMain/src/ZonedDateTime.kt +++ b/core/nativeMain/src/ZonedDateTime.kt @@ -70,14 +70,16 @@ 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. - 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()) 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(