From ac5ec5f057bf94921d826a8f8c345b0f62a43dc7 Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 2 Mar 2022 15:18:09 -0800 Subject: [PATCH 01/57] Add minus operator to DatePeriod and DateTimePeriod --- core/common/src/DateTimePeriod.kt | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/core/common/src/DateTimePeriod.kt b/core/common/src/DateTimePeriod.kt index 139dfcd09..cde1bab5f 100644 --- a/core/common/src/DateTimePeriod.kt +++ b/core/common/src/DateTimePeriod.kt @@ -448,6 +448,17 @@ public operator fun DateTimePeriod.plus(other: DateTimePeriod): DateTimePeriod = safeAdd(totalNanoseconds, other.totalNanoseconds), ) +/** + * Subtracts one [DateTimePeriod] instance from another. + * + * @throws DateTimeArithmeticException if arithmetic overflow happens. + */ +public operator fun DateTimePeriod.minus(other: DateTimePeriod) : DateTimePeriod = buildDateTimePeriod( + safeAdd(totalMonths, -other.totalMonths), + safeAdd(days, -other.days), + safeAdd(totalNanoseconds, -other.totalNanoseconds) +) + /** * Adds two [DatePeriod] instances. * @@ -457,3 +468,13 @@ public operator fun DatePeriod.plus(other: DatePeriod): DatePeriod = DatePeriod( safeAdd(totalMonths, other.totalMonths), safeAdd(days, other.days), ) + +/** + * Subtracts one [DatePeriod] instance from another. + * + * @throws DateTimeArithmeticException if arithmetic overflow happens. + */ +public operator fun DatePeriod.minus(other: DatePeriod): DatePeriod = DatePeriod( + safeAdd(totalMonths, -other.totalMonths), + safeAdd(days, -other.days) +) From a962bde0b0a5d6cd78982615dccdc8a593274815 Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 2 Mar 2022 15:18:49 -0800 Subject: [PATCH 02/57] Add unary minus operator to DatePeriod and DateTimePeriod --- core/common/src/DateTimePeriod.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/common/src/DateTimePeriod.kt b/core/common/src/DateTimePeriod.kt index cde1bab5f..87cc21e8d 100644 --- a/core/common/src/DateTimePeriod.kt +++ b/core/common/src/DateTimePeriod.kt @@ -459,6 +459,8 @@ public operator fun DateTimePeriod.minus(other: DateTimePeriod) : DateTimePeriod safeAdd(totalNanoseconds, -other.totalNanoseconds) ) +public operator fun DateTimePeriod.unaryMinus(): DateTimePeriod = buildDateTimePeriod(-totalMonths, -days, -totalNanoseconds) + /** * Adds two [DatePeriod] instances. * @@ -478,3 +480,5 @@ public operator fun DatePeriod.minus(other: DatePeriod): DatePeriod = DatePeriod safeAdd(totalMonths, -other.totalMonths), safeAdd(days, -other.days) ) + +public operator fun DatePeriod.unaryMinus(): DatePeriod = DatePeriod(-totalMonths, -days) From 740434f628759c3ce9b75f558335e6022abbf8b0 Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 2 Mar 2022 15:19:02 -0800 Subject: [PATCH 03/57] Add LocalDateRange class and associated tests. Exists to enable Kotlin range and progression semantics with the LocalDate and DatePeriod classes. --- core/common/src/LocalDateRange.kt | 109 ++++++++ core/common/test/LocalDateRangeTest.kt | 341 +++++++++++++++++++++++++ 2 files changed, 450 insertions(+) create mode 100644 core/common/src/LocalDateRange.kt create mode 100644 core/common/test/LocalDateRangeTest.kt diff --git a/core/common/src/LocalDateRange.kt b/core/common/src/LocalDateRange.kt new file mode 100644 index 000000000..1b3ce5198 --- /dev/null +++ b/core/common/src/LocalDateRange.kt @@ -0,0 +1,109 @@ +/* + * Copyright 2019-2022 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 + +public abstract class LocalDateIterator : Iterator { + final override fun next(): LocalDate = nextLocalDate() + public abstract fun nextLocalDate(): LocalDate +} + +internal class LocalDateProgressionIterator(first: LocalDate, last: LocalDate, val step: DatePeriod) : LocalDateIterator() { + private val finalElement: LocalDate = last + private val increasing = step.positive() + private var hasNext: Boolean = if (increasing) first <= last else first >= last + private var next: LocalDate = if (hasNext) first else finalElement + + override fun hasNext(): Boolean = hasNext + + override fun nextLocalDate(): LocalDate { + if(!hasNext) throw NoSuchElementException() + val value = next + next += step + /** + * Some [DatePeriod]s with opposite-signed constituent parts can get stuck in an infinite loop rather than progressing toward the far future or far past. + * A period of P1M-28D for example, when added to any date in February, will return that same date, thus leading to a loop. + */ + if(next == value) throw IllegalStateException("Progression has hit an infinite loop. Check to ensure that the the values for total months and days in the provided step DatePeriod are not equal and opposite for certain month(s).") + if ((increasing && next > finalElement) || (!increasing && next < finalElement)) { + hasNext = false + } + return value + } +} + +public open class LocalDateProgression +internal constructor + ( + start: LocalDate, + endInclusive: LocalDate, + public val step: DatePeriod +) : Iterable { + init { + if(!step.positive() && !step.negative()) throw IllegalArgumentException("Provided step DatePeriod is of size zero (or equivalent over an arbitrarily long timeline)") + } + public val first: LocalDate = start + public val last: LocalDate = endInclusive + + override fun iterator(): LocalDateIterator = LocalDateProgressionIterator(first, last, step) + + public open fun isEmpty(): Boolean = if (step.positive()) first > last else first < last + + + override fun toString(): String = if (step.positive()) "$first..$last step $step" else "$first downTo $last step $step" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as LocalDateProgression + + if (first != other.first) return false + if (last != other.last) return false + if (step != other.step) return false + + return true + } + + override fun hashCode(): Int { + var result = first.hashCode() + result = 31 * result + last.hashCode() + result = 31 * result + step.hashCode() + return result + } + + public companion object { + public fun fromClosedRange(rangeStart: LocalDate, rangeEnd: LocalDate, step: DatePeriod): LocalDateProgression = LocalDateProgression(rangeStart, rangeEnd, step) + } +} + +public class LocalDateRange(start: LocalDate, endInclusive: LocalDate) : LocalDateProgression(start, endInclusive, DatePeriod(days = 1)), ClosedRange { + override val start: LocalDate get() = first + override val endInclusive: LocalDate get() = last + + @Suppress("ConvertTwoComparisonsToRangeCheck") + override fun contains(value: LocalDate): Boolean = first <= value && value <= last + + override fun isEmpty(): Boolean = first > last + + override fun toString(): String = "$first..$last" + + public companion object { + private val DATE_ONE = LocalDate(1, 1, 1) + private val DATE_TWO = LocalDate(1, 1, 2) + public val EMPTY: LocalDateRange = LocalDateRange(DATE_TWO, DATE_ONE) + } +} + +/** + * On an arbitrarily long timeline, the average month will be 30.436875 days long (146097 days over 400 years). + */ +public fun DatePeriod.positive() : Boolean = totalMonths * 30.436875 + days > 0 +public fun DatePeriod.negative() : Boolean = totalMonths * 30.436875 + days < 0 + +public infix fun LocalDateProgression.step(step: DatePeriod) : LocalDateProgression = LocalDateProgression.fromClosedRange(first, last, step) +public infix fun LocalDate.downTo(that: LocalDate) : LocalDateProgression = LocalDateProgression.fromClosedRange(this, that, DatePeriod(days = -1)) + +public operator fun LocalDate.rangeTo(that: LocalDate): LocalDateRange = LocalDateRange(this, that) \ No newline at end of file diff --git a/core/common/test/LocalDateRangeTest.kt b/core/common/test/LocalDateRangeTest.kt new file mode 100644 index 000000000..5370d35dd --- /dev/null +++ b/core/common/test/LocalDateRangeTest.kt @@ -0,0 +1,341 @@ +/* + * Copyright 2019-2022 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 LocalDateRangeTest { + val Dec_24_1900 = LocalDate(1900, 12, 24) + val Dec_30_1999 = LocalDate(1999, 12, 30) + val Jan_01_2000 = LocalDate(2000, 1, 1) + val Jan_02_2000 = LocalDate(2000, 1, 2) + val Jan_05_2000 = LocalDate(2000, 1, 5) + val Jan_24_2000 = LocalDate(2000, 1, 24) + val Dec_24_2000 = LocalDate(2000, 12, 24) + + @Test + fun emptyRange() { + assertTrue { (Jan_05_2000..Jan_01_2000).isEmpty() } + assertTrue { (Jan_01_2000 downTo Jan_05_2000).isEmpty() } + assertTrue { LocalDateRange.EMPTY.isEmpty() } + } + + @Test + fun forwardRange() { + assertContentEquals( + (1..5).map { LocalDate(2000, 1, it) }, + Jan_01_2000..Jan_05_2000 + ) + assertContentEquals( + listOf(Jan_01_2000), + Jan_01_2000..Jan_01_2000 + ) + assertContentEquals( + listOf( + LocalDate(1999, 12, 30), + LocalDate(1999, 12, 31), + LocalDate(2000, 1, 1), + LocalDate(2000, 1, 2) + ), + Dec_30_1999..Jan_02_2000 + ) + } + + @Test + fun backwardRange() { + assertContentEquals( + (5 downTo 1).map { LocalDate(2000, 1, it) }, + Jan_05_2000 downTo Jan_01_2000 + ) + assertContentEquals( + listOf(Jan_01_2000), + Jan_01_2000 downTo Jan_01_2000 + ) + assertContentEquals( + listOf( + LocalDate(2000, 1, 2), + LocalDate(2000, 1, 1), + LocalDate(1999, 12, 31), + LocalDate(1999, 12, 30), + ), + Jan_02_2000 downTo Dec_30_1999 + ) + } + + @Test + fun step() { + assertContentEquals( + (1900..2000).map { LocalDate(it, 12, 24) }, + (Dec_24_1900..Dec_24_2000 step DatePeriod(years = 1)) + ) + assertContentEquals( + (1..12).map { LocalDate(2000, it, 24) }, + (Jan_24_2000..Dec_24_2000 step DatePeriod(months = 1)) + ) + assertContentEquals( + (1..12).map { LocalDate(2000, it, it * 2) }, + (Jan_02_2000..Dec_24_2000 step DatePeriod(months = 1, days = 2)) + ) + } + + @Test + fun invalidSteps() { + val range = Dec_24_1900..Jan_01_2000 + val downRange = Jan_01_2000 downTo Dec_24_1900 + + listOf( + 0 to 0, + 4800 to -146097 + ).map { + DatePeriod(months = it.first, days = it.second) + }.forEach { + assertFalse { it.positive() } + assertFalse { it.negative() } + assertFailsWith { range step it } + assertFailsWith { downRange step it } + } + + listOf( + 1 to -28, + 1 to -29, + 1 to -30, + 1 to -31, + 2 to -59, + 2 to -60, + 2 to -61, + 2 to -62, + 3 to -89, + 3 to -90, + 3 to -91, + 3 to -92, + 4 to -120, + 4 to -121, + 4 to -122, + 4 to -123, + 5 to -150, + 5 to -151, + 5 to -152, + 5 to -153, + 6 to -181, + 6 to -182, + 6 to -183, + 6 to -184, + 7 to -212, + 7 to -213, + 7 to -214, + 7 to -215, + 8 to -242, + 8 to -243, + 8 to -244, + 8 to -245, + 9 to -273, + 9 to -274, + 9 to -275, + 9 to -276, + 10 to -303, + 10 to -304, + 10 to -305, + 10 to -306, + 11 to -334, + 11 to -335, + 11 to -336, + 11 to -337, + 12 to -365, + 12 to -366, + 13 to -393, + 13 to -394, + 13 to -395, + 13 to -396, + 13 to -397, + 14 to -424, + 14 to -425, + 14 to -426, + 14 to -427, + 14 to -428, + 15 to -454, + 15 to -455, + 15 to -456, + 15 to -457, + 15 to -458, + 16 to -485, + 16 to -486, + 16 to -487, + 16 to -488, + 16 to -489, + 17 to -515, + 17 to -516, + 17 to -517, + 17 to -518, + 17 to -519, + 18 to -546, + 18 to -547, + 18 to -548, + 18 to -549, + 18 to -550, + 19 to -577, + 19 to -578, + 19 to -579, + 19 to -580, + 19 to -581, + 20 to -607, + 20 to -608, + 20 to -609, + 20 to -610, + 20 to -611, + 21 to -638, + 21 to -639, + 21 to -640, + 21 to -641, + 21 to -642, + 22 to -668, + 22 to -669, + 22 to -670, + 22 to -671, + 22 to -672, + 23 to -699, + 23 to -700, + 23 to -701, + 23 to -702, + 23 to -703, + 24 to -730, + 24 to -731, + 25 to -758, + 25 to -759, + 25 to -760, + 25 to -761, + 25 to -762, + 26 to -789, + 26 to -790, + 26 to -791, + 26 to -792, + 26 to -793, + 27 to -819, + 27 to -820, + 27 to -821, + 27 to -822, + 27 to -823, + 28 to -850, + 28 to -851, + 28 to -852, + 28 to -853, + 28 to -854, + 29 to -880, + 29 to -881, + 29 to -882, + 29 to -883, + 29 to -884, + 30 to -911, + 30 to -912, + 30 to -913, + 30 to -914, + 30 to -915, + 31 to -942, + 31 to -943, + 31 to -944, + 31 to -945, + 31 to -946, + 32 to -972, + 32 to -973, + 32 to -974, + 32 to -975, + 32 to -976, + 33 to -1003, + 33 to -1004, + 33 to -1005, + 33 to -1006, + 33 to -1007, + 34 to -1033, + 34 to -1034, + 34 to -1035, + 34 to -1036, + 34 to -1037, + 35 to -1064, + 35 to -1065, + 35 to -1066, + 35 to -1067, + 35 to -1068, + 36 to -1095, + 36 to -1096, + 37 to -1124, + 37 to -1125, + 37 to -1126, + 37 to -1127, + 38 to -1155, + 38 to -1156, + 38 to -1157, + 38 to -1158, + 39 to -1185, + 39 to -1186, + 39 to -1187, + 39 to -1188, + 40 to -1216, + 40 to -1217, + 40 to -1218, + 40 to -1219, + 41 to -1246, + 41 to -1247, + 41 to -1248, + 41 to -1249, + 42 to -1277, + 42 to -1278, + 42 to -1279, + 42 to -1280, + 43 to -1308, + 43 to -1309, + 43 to -1310, + 43 to -1311, + 44 to -1338, + 44 to -1339, + 44 to -1340, + 44 to -1341, + 45 to -1369, + 45 to -1370, + 45 to -1371, + 45 to -1372, + 46 to -1399, + 46 to -1400, + 46 to -1401, + 46 to -1402, + 47 to -1430, + 47 to -1431, + 47 to -1432, + 47 to -1433, + 48 to -1461 + ).map { + DatePeriod(months = it.first, days = it.second) + }.flatMap{ + listOf(it, -it) + }.forEach { + val step = when { + it.positive() -> it + else -> -it + } + assertFailsWith { (range step step).toList() } + assertFailsWith { (downRange step -step).toList() } + } + } + + @Test + fun string() { + assertEquals( + "2000-01-01..2000-01-05", + (Jan_01_2000..Jan_05_2000).toString() + ) + assertEquals( + "2000-01-05 downTo 2000-01-01 step -P1D", + (Jan_05_2000 downTo Jan_01_2000).toString() + ) + assertEquals( + "2000-01-01..2000-01-05 step P1D", + LocalDateProgression.fromClosedRange(Jan_01_2000, Jan_05_2000, DatePeriod(days=1)).toString() + ) + assertEquals( + "2000-01-05 downTo 2000-01-01 step -P1D", + LocalDateProgression.fromClosedRange(Jan_05_2000, Jan_01_2000, DatePeriod(days=-1)).toString() + ) + } +} From 2ea4cdb4fff220b8ae6731d8caadf9a6e862052b Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 2 Mar 2022 15:35:33 -0800 Subject: [PATCH 04/57] Add tests for minus and unaryMinus operators for DatePeriod and DateTimePeriod --- core/common/test/DateTimePeriodTest.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/common/test/DateTimePeriodTest.kt b/core/common/test/DateTimePeriodTest.kt index ccf516c03..7e74e4764 100644 --- a/core/common/test/DateTimePeriodTest.kt +++ b/core/common/test/DateTimePeriodTest.kt @@ -149,9 +149,19 @@ class DateTimePeriodTest { assertEquals(DatePeriod(years = 2, months = 12), dp1 + dp1) assertEquals(DateTimePeriod(years = 1, months = 6, days = 3), p2 + dp1) + assertEquals(p1, DateTimePeriod(years=10, days=3, hours=2) - p2 -p3) + assertEquals(p1, DatePeriod(years = 11, months = 6) - dp1) + assertEquals(dp1, DatePeriod(years = 2, months = 12) - dp1) + assertEquals(p2, DateTimePeriod(years = 1, months = 6, days = 3) - dp1) + val dp2 = dp1 + p3 + p4 + val dp3 = dp2 - p3 -p4 assertEquals(dp1, dp2) + assertEquals(dp1, dp3) assertTrue(dp2 is DatePeriod) + assertTrue(dp3 is DatePeriod) + assertEquals(DateTimePeriod(years = -10, days=-3, hours = -2), -(p1 + p2 + p3)) + assertEquals(DatePeriod(years = -11, months = -6), -(dp1 + p1)) } @Test From 59bd4fd5c3da5c3a5867ad5ed77a41ead5b3459e Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Wed, 4 Sep 2024 17:07:56 -0400 Subject: [PATCH 05/57] Rework LocalDateRange to match proposed spec --- core/common/src/LocalDateRange.kt | 127 +++++----- core/common/test/LocalDateRangeTest.kt | 324 ++++++------------------- 2 files changed, 142 insertions(+), 309 deletions(-) diff --git a/core/common/src/LocalDateRange.kt b/core/common/src/LocalDateRange.kt index 1b3ce5198..2c659bac1 100644 --- a/core/common/src/LocalDateRange.kt +++ b/core/common/src/LocalDateRange.kt @@ -5,54 +5,31 @@ package kotlinx.datetime -public abstract class LocalDateIterator : Iterator { - final override fun next(): LocalDate = nextLocalDate() - public abstract fun nextLocalDate(): LocalDate -} +import kotlin.random.Random +import kotlin.random.nextInt -internal class LocalDateProgressionIterator(first: LocalDate, last: LocalDate, val step: DatePeriod) : LocalDateIterator() { - private val finalElement: LocalDate = last - private val increasing = step.positive() - private var hasNext: Boolean = if (increasing) first <= last else first >= last - private var next: LocalDate = if (hasNext) first else finalElement - - override fun hasNext(): Boolean = hasNext - - override fun nextLocalDate(): LocalDate { - if(!hasNext) throw NoSuchElementException() - val value = next - next += step - /** - * Some [DatePeriod]s with opposite-signed constituent parts can get stuck in an infinite loop rather than progressing toward the far future or far past. - * A period of P1M-28D for example, when added to any date in February, will return that same date, thus leading to a loop. - */ - if(next == value) throw IllegalStateException("Progression has hit an infinite loop. Check to ensure that the the values for total months and days in the provided step DatePeriod are not equal and opposite for certain month(s).") - if ((increasing && next > finalElement) || (!increasing && next < finalElement)) { - hasNext = false - } - return value - } +internal class LocalDateProgressionIterator(private val iterator: IntIterator) : Iterator { + override fun hasNext(): Boolean = iterator.hasNext() + override fun next(): LocalDate = LocalDate.fromEpochDays(iterator.next()) } public open class LocalDateProgression -internal constructor - ( - start: LocalDate, - endInclusive: LocalDate, - public val step: DatePeriod -) : Iterable { - init { - if(!step.positive() && !step.negative()) throw IllegalArgumentException("Provided step DatePeriod is of size zero (or equivalent over an arbitrarily long timeline)") - } - public val first: LocalDate = start - public val last: LocalDate = endInclusive +internal constructor(internal val intProgression: IntProgression) : Iterable { - override fun iterator(): LocalDateIterator = LocalDateProgressionIterator(first, last, step) + internal constructor( + start: LocalDate, + endInclusive: LocalDate, + step: Int + ) : this(IntProgression.fromClosedRange(start.toEpochDays(), endInclusive.toEpochDays(), step)) - public open fun isEmpty(): Boolean = if (step.positive()) first > last else first < last + public val first: LocalDate = LocalDate.fromEpochDays(intProgression.first) + public val last: LocalDate = LocalDate.fromEpochDays(intProgression.last) + override fun iterator(): Iterator = LocalDateProgressionIterator(intProgression.iterator()) - override fun toString(): String = if (step.positive()) "$first..$last step $step" else "$first downTo $last step $step" + public open fun isEmpty(): Boolean = intProgression.isEmpty() + + override fun toString(): String = if (intProgression.step > 0) "$first..$last step ${intProgression.step}" else "$first downTo $last step ${intProgression.step}" override fun equals(other: Any?): Boolean { if (this === other) return true @@ -60,28 +37,28 @@ internal constructor other as LocalDateProgression - if (first != other.first) return false - if (last != other.last) return false - if (step != other.step) return false - - return true + return intProgression == other.intProgression } override fun hashCode(): Int { - var result = first.hashCode() - result = 31 * result + last.hashCode() - result = 31 * result + step.hashCode() - return result + return intProgression.hashCode() } + public companion object { - public fun fromClosedRange(rangeStart: LocalDate, rangeEnd: LocalDate, step: DatePeriod): LocalDateProgression = LocalDateProgression(rangeStart, rangeEnd, step) + public fun fromClosedRange( + rangeStart: LocalDate, + rangeEnd: LocalDate, + stepValue: Int, + stepUnit: DateTimeUnit.DayBased + ): LocalDateProgression = LocalDateProgression(rangeStart, rangeEnd, stepValue * stepUnit.days) } } -public class LocalDateRange(start: LocalDate, endInclusive: LocalDate) : LocalDateProgression(start, endInclusive, DatePeriod(days = 1)), ClosedRange { +public class LocalDateRange(start: LocalDate, endInclusive: LocalDate) : LocalDateProgression(start, endInclusive, 1), ClosedRange, OpenEndRange { override val start: LocalDate get() = first override val endInclusive: LocalDate get() = last + override val endExclusive: LocalDate get() = endInclusive.plus(1, DateTimeUnit.DAY) @Suppress("ConvertTwoComparisonsToRangeCheck") override fun contains(value: LocalDate): Boolean = first <= value && value <= last @@ -97,13 +74,45 @@ public class LocalDateRange(start: LocalDate, endInclusive: LocalDate) : LocalDa } } -/** - * On an arbitrarily long timeline, the average month will be 30.436875 days long (146097 days over 400 years). - */ -public fun DatePeriod.positive() : Boolean = totalMonths * 30.436875 + days > 0 -public fun DatePeriod.negative() : Boolean = totalMonths * 30.436875 + days < 0 +public fun LocalDateProgression.first(): LocalDate { + if (isEmpty()) + throw NoSuchElementException("Progression $this is empty.") + return this.first +} +public fun LocalDateProgression.last(): LocalDate { + if (isEmpty()) + throw NoSuchElementException("Progression $this is empty.") + return this.last +} +public fun LocalDateProgression.firstOrNull(): LocalDate? = if (isEmpty()) null else this.first +public fun LocalDateProgression.lastOrNull(): LocalDate? = if (isEmpty()) null else this.last + +public fun LocalDateProgression.reversed(): LocalDateProgression = LocalDateProgression(intProgression.reversed()) + +public fun LocalDateProgression.step(value: Int, unit: DateTimeUnit.DayBased) : LocalDateProgression = LocalDateProgression(intProgression.step(value * unit.days)) +public fun LocalDateProgression.step(value: Long, unit: DateTimeUnit.DayBased) : LocalDateProgression = step(value.toInt(), unit) + +public infix fun LocalDate.downTo(that: LocalDate) : LocalDateProgression = LocalDateProgression.fromClosedRange(this, that, -1, DateTimeUnit.DAY) +public infix fun LocalDate.downUntil(that: LocalDate) : LocalDateProgression = downTo(that.plus(1, DateTimeUnit.DAY)) + +public operator fun LocalDate.rangeTo(that: LocalDate): LocalDateRange = LocalDateRange(this, that) +public operator fun LocalDate.rangeUntil(that: LocalDate) : LocalDateRange = rangeTo(that.minus(1, DateTimeUnit.DAY)) + +public fun LocalDateProgression.random(randomIntFunction: (IntRange) -> Int) : LocalDate = intProgression.random(randomIntFunction) + .let(LocalDate.Companion::fromEpochDays) + +public fun LocalDateProgression.random(random: Random = Random) : LocalDate = intProgression.random(random).let(LocalDate.Companion::fromEpochDays) + +public fun LocalDateProgression.randomOrNull(randomIntFunction: (range: IntRange) -> Int) : LocalDate? = intProgression.randomOrNull(randomIntFunction) + ?.let(LocalDate.Companion::fromEpochDays) + +public fun LocalDateProgression.randomOrNull(random: Random = Random) : LocalDate? = intProgression.randomOrNull(random) + ?.let(LocalDate.Companion::fromEpochDays) + +public inline fun IntProgression.random(func: (IntRange) -> Int) : Int = func(0..(last - first) / step) * step + first + +public fun IntProgression.random(random: Random = Random) : Int = random(random::nextInt) -public infix fun LocalDateProgression.step(step: DatePeriod) : LocalDateProgression = LocalDateProgression.fromClosedRange(first, last, step) -public infix fun LocalDate.downTo(that: LocalDate) : LocalDateProgression = LocalDateProgression.fromClosedRange(this, that, DatePeriod(days = -1)) +public inline fun IntProgression.randomOrNull(randomIntFunction: (range: IntRange) -> Int) : Int? = if (isEmpty()) null else random(randomIntFunction) -public operator fun LocalDate.rangeTo(that: LocalDate): LocalDateRange = LocalDateRange(this, that) \ No newline at end of file +public fun IntProgression.randomOrNull(random: Random = Random) : Int? = if (isEmpty()) null else random(random) diff --git a/core/common/test/LocalDateRangeTest.kt b/core/common/test/LocalDateRangeTest.kt index 5370d35dd..ce30e63d4 100644 --- a/core/common/test/LocalDateRangeTest.kt +++ b/core/common/test/LocalDateRangeTest.kt @@ -6,6 +6,8 @@ package kotlinx.datetime.test import kotlinx.datetime.* +import kotlin.random.Random +import kotlin.random.nextInt import kotlin.test.* class LocalDateRangeTest { @@ -30,6 +32,10 @@ class LocalDateRangeTest { (1..5).map { LocalDate(2000, 1, it) }, Jan_01_2000..Jan_05_2000 ) + assertContentEquals( + (1..<5).map { LocalDate(2000, 1, it) }, + Jan_01_2000.. { range step it } - assertFailsWith { downRange step it } - } - - listOf( - 1 to -28, - 1 to -29, - 1 to -30, - 1 to -31, - 2 to -59, - 2 to -60, - 2 to -61, - 2 to -62, - 3 to -89, - 3 to -90, - 3 to -91, - 3 to -92, - 4 to -120, - 4 to -121, - 4 to -122, - 4 to -123, - 5 to -150, - 5 to -151, - 5 to -152, - 5 to -153, - 6 to -181, - 6 to -182, - 6 to -183, - 6 to -184, - 7 to -212, - 7 to -213, - 7 to -214, - 7 to -215, - 8 to -242, - 8 to -243, - 8 to -244, - 8 to -245, - 9 to -273, - 9 to -274, - 9 to -275, - 9 to -276, - 10 to -303, - 10 to -304, - 10 to -305, - 10 to -306, - 11 to -334, - 11 to -335, - 11 to -336, - 11 to -337, - 12 to -365, - 12 to -366, - 13 to -393, - 13 to -394, - 13 to -395, - 13 to -396, - 13 to -397, - 14 to -424, - 14 to -425, - 14 to -426, - 14 to -427, - 14 to -428, - 15 to -454, - 15 to -455, - 15 to -456, - 15 to -457, - 15 to -458, - 16 to -485, - 16 to -486, - 16 to -487, - 16 to -488, - 16 to -489, - 17 to -515, - 17 to -516, - 17 to -517, - 17 to -518, - 17 to -519, - 18 to -546, - 18 to -547, - 18 to -548, - 18 to -549, - 18 to -550, - 19 to -577, - 19 to -578, - 19 to -579, - 19 to -580, - 19 to -581, - 20 to -607, - 20 to -608, - 20 to -609, - 20 to -610, - 20 to -611, - 21 to -638, - 21 to -639, - 21 to -640, - 21 to -641, - 21 to -642, - 22 to -668, - 22 to -669, - 22 to -670, - 22 to -671, - 22 to -672, - 23 to -699, - 23 to -700, - 23 to -701, - 23 to -702, - 23 to -703, - 24 to -730, - 24 to -731, - 25 to -758, - 25 to -759, - 25 to -760, - 25 to -761, - 25 to -762, - 26 to -789, - 26 to -790, - 26 to -791, - 26 to -792, - 26 to -793, - 27 to -819, - 27 to -820, - 27 to -821, - 27 to -822, - 27 to -823, - 28 to -850, - 28 to -851, - 28 to -852, - 28 to -853, - 28 to -854, - 29 to -880, - 29 to -881, - 29 to -882, - 29 to -883, - 29 to -884, - 30 to -911, - 30 to -912, - 30 to -913, - 30 to -914, - 30 to -915, - 31 to -942, - 31 to -943, - 31 to -944, - 31 to -945, - 31 to -946, - 32 to -972, - 32 to -973, - 32 to -974, - 32 to -975, - 32 to -976, - 33 to -1003, - 33 to -1004, - 33 to -1005, - 33 to -1006, - 33 to -1007, - 34 to -1033, - 34 to -1034, - 34 to -1035, - 34 to -1036, - 34 to -1037, - 35 to -1064, - 35 to -1065, - 35 to -1066, - 35 to -1067, - 35 to -1068, - 36 to -1095, - 36 to -1096, - 37 to -1124, - 37 to -1125, - 37 to -1126, - 37 to -1127, - 38 to -1155, - 38 to -1156, - 38 to -1157, - 38 to -1158, - 39 to -1185, - 39 to -1186, - 39 to -1187, - 39 to -1188, - 40 to -1216, - 40 to -1217, - 40 to -1218, - 40 to -1219, - 41 to -1246, - 41 to -1247, - 41 to -1248, - 41 to -1249, - 42 to -1277, - 42 to -1278, - 42 to -1279, - 42 to -1280, - 43 to -1308, - 43 to -1309, - 43 to -1310, - 43 to -1311, - 44 to -1338, - 44 to -1339, - 44 to -1340, - 44 to -1341, - 45 to -1369, - 45 to -1370, - 45 to -1371, - 45 to -1372, - 46 to -1399, - 46 to -1400, - 46 to -1401, - 46 to -1402, - 47 to -1430, - 47 to -1431, - 47 to -1432, - 47 to -1433, - 48 to -1461 - ).map { - DatePeriod(months = it.first, days = it.second) - }.flatMap{ - listOf(it, -it) - }.forEach { - val step = when { - it.positive() -> it - else -> -it - } - assertFailsWith { (range step step).toList() } - assertFailsWith { (downRange step -step).toList() } - } } @Test @@ -326,16 +95,71 @@ class LocalDateRangeTest { (Jan_01_2000..Jan_05_2000).toString() ) assertEquals( - "2000-01-05 downTo 2000-01-01 step -P1D", + "2000-01-05 downTo 2000-01-01 step -1", (Jan_05_2000 downTo Jan_01_2000).toString() ) assertEquals( - "2000-01-01..2000-01-05 step P1D", - LocalDateProgression.fromClosedRange(Jan_01_2000, Jan_05_2000, DatePeriod(days=1)).toString() + "2000-01-01..2000-01-05 step 1", + LocalDateProgression.fromClosedRange(Jan_01_2000, Jan_05_2000, 1, DateTimeUnit.DAY).toString() ) assertEquals( - "2000-01-05 downTo 2000-01-01 step -P1D", - LocalDateProgression.fromClosedRange(Jan_05_2000, Jan_01_2000, DatePeriod(days=-1)).toString() + "2000-01-05 downTo 2000-01-01 step -1", + LocalDateProgression.fromClosedRange(Jan_05_2000, Jan_01_2000, -1, DateTimeUnit.DAY).toString() ) } + + @Test + fun random() { + assertEquals( + Jan_01_2000, + (Jan_01_2000..Jan_01_2000).random() + ) + + assertEquals( + Jan_01_2000, + (Jan_01_2000 downTo Jan_01_2000).random() + ) + + assertFails { + (Jan_02_2000..Jan_01_2000).random() + } + + assertNull((Jan_02_2000..Jan_01_2000).randomOrNull()) + + val seed = 123456 + val expectedRand = Random(seed) + val actualRand = Random(seed) + + repeat(20) { + assertEquals( + expectedRand.nextInt(0..23).let { Jan_01_2000.plus(it, DateTimeUnit.DAY) }, + (Jan_01_2000..Jan_24_2000).random(actualRand) + ) + } + + repeat(20) { + assertEquals( + expectedRand.nextInt(0..23).let { Jan_24_2000.minus(it, DateTimeUnit.DAY) }, + (Jan_24_2000 downTo Jan_01_2000).random(actualRand) + ) + } + + listOf(1, 2, 5, 30).forEach { step -> + repeat(20) { + val range = (0..23 step step) + assertEquals( + expectedRand.nextInt(0..range.last / step).let { Jan_01_2000.plus(it * step, DateTimeUnit.DAY) }, + (Jan_01_2000..Jan_24_2000).step(step, DateTimeUnit.DAY).random(actualRand) + ) + } + + repeat(20) { + val range = (0..23 step step) + assertEquals( + expectedRand.nextInt(0..range.last / step).let { Jan_24_2000.minus(it * step, DateTimeUnit.DAY) }, + (Jan_24_2000 downTo Jan_01_2000).step(step, DateTimeUnit.DAY).random(actualRand) + ) + } + } + } } From e249b203c23e1d39338aa4d7aee7e50a7b80e933 Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Thu, 5 Sep 2024 09:51:55 -0400 Subject: [PATCH 06/57] Revert "Add tests for minus and unaryMinus operators for DatePeriod and" This reverts commit 2ea4cdb4fff220b8ae6731d8caadf9a6e862052b. --- core/common/test/DateTimePeriodTest.kt | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/core/common/test/DateTimePeriodTest.kt b/core/common/test/DateTimePeriodTest.kt index 7e74e4764..ccf516c03 100644 --- a/core/common/test/DateTimePeriodTest.kt +++ b/core/common/test/DateTimePeriodTest.kt @@ -149,19 +149,9 @@ class DateTimePeriodTest { assertEquals(DatePeriod(years = 2, months = 12), dp1 + dp1) assertEquals(DateTimePeriod(years = 1, months = 6, days = 3), p2 + dp1) - assertEquals(p1, DateTimePeriod(years=10, days=3, hours=2) - p2 -p3) - assertEquals(p1, DatePeriod(years = 11, months = 6) - dp1) - assertEquals(dp1, DatePeriod(years = 2, months = 12) - dp1) - assertEquals(p2, DateTimePeriod(years = 1, months = 6, days = 3) - dp1) - val dp2 = dp1 + p3 + p4 - val dp3 = dp2 - p3 -p4 assertEquals(dp1, dp2) - assertEquals(dp1, dp3) assertTrue(dp2 is DatePeriod) - assertTrue(dp3 is DatePeriod) - assertEquals(DateTimePeriod(years = -10, days=-3, hours = -2), -(p1 + p2 + p3)) - assertEquals(DatePeriod(years = -11, months = -6), -(dp1 + p1)) } @Test From 2e0b957f34a1f0897d5b423b9c3e046c1bf206b0 Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Thu, 5 Sep 2024 09:52:51 -0400 Subject: [PATCH 07/57] Revert "Add unary minus operator to DatePeriod and DateTimePeriod" This reverts commit a962bde0b0a5d6cd78982615dccdc8a593274815. --- core/common/src/DateTimePeriod.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/core/common/src/DateTimePeriod.kt b/core/common/src/DateTimePeriod.kt index 9b5718898..e7d2583fb 100644 --- a/core/common/src/DateTimePeriod.kt +++ b/core/common/src/DateTimePeriod.kt @@ -594,8 +594,6 @@ public operator fun DateTimePeriod.minus(other: DateTimePeriod) : DateTimePeriod safeAdd(totalNanoseconds, -other.totalNanoseconds) ) -public operator fun DateTimePeriod.unaryMinus(): DateTimePeriod = buildDateTimePeriod(-totalMonths, -days, -totalNanoseconds) - /** * Adds two [DatePeriod] instances. * @@ -618,5 +616,3 @@ public operator fun DatePeriod.minus(other: DatePeriod): DatePeriod = DatePeriod safeAdd(totalMonths, -other.totalMonths), safeAdd(days, -other.days) ) - -public operator fun DatePeriod.unaryMinus(): DatePeriod = DatePeriod(-totalMonths, -days) From 375b6dafd4086c58c1b91919d761c3db49302aca Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Thu, 5 Sep 2024 09:52:51 -0400 Subject: [PATCH 08/57] Revert "Add minus operator to DatePeriod and DateTimePeriod" This reverts commit ac5ec5f057bf94921d826a8f8c345b0f62a43dc7. --- core/common/src/DateTimePeriod.kt | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/core/common/src/DateTimePeriod.kt b/core/common/src/DateTimePeriod.kt index e7d2583fb..f9b2d3f3b 100644 --- a/core/common/src/DateTimePeriod.kt +++ b/core/common/src/DateTimePeriod.kt @@ -583,17 +583,6 @@ public operator fun DateTimePeriod.plus(other: DateTimePeriod): DateTimePeriod = safeAdd(totalNanoseconds, other.totalNanoseconds), ) -/** - * Subtracts one [DateTimePeriod] instance from another. - * - * @throws DateTimeArithmeticException if arithmetic overflow happens. - */ -public operator fun DateTimePeriod.minus(other: DateTimePeriod) : DateTimePeriod = buildDateTimePeriod( - safeAdd(totalMonths, -other.totalMonths), - safeAdd(days, -other.days), - safeAdd(totalNanoseconds, -other.totalNanoseconds) -) - /** * Adds two [DatePeriod] instances. * @@ -606,13 +595,3 @@ public operator fun DatePeriod.plus(other: DatePeriod): DatePeriod = DatePeriod( safeAdd(totalMonths, other.totalMonths), safeAdd(days, other.days), ) - -/** - * Subtracts one [DatePeriod] instance from another. - * - * @throws DateTimeArithmeticException if arithmetic overflow happens. - */ -public operator fun DatePeriod.minus(other: DatePeriod): DatePeriod = DatePeriod( - safeAdd(totalMonths, -other.totalMonths), - safeAdd(days, -other.days) -) From 9a7c50743abb96bcad0de4e452341a2e566bc195 Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Thu, 5 Sep 2024 13:24:01 -0400 Subject: [PATCH 09/57] Make LocalDateProgressionIterator private --- core/common/src/LocalDateRange.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/common/src/LocalDateRange.kt b/core/common/src/LocalDateRange.kt index 2c659bac1..08006a5da 100644 --- a/core/common/src/LocalDateRange.kt +++ b/core/common/src/LocalDateRange.kt @@ -8,7 +8,7 @@ package kotlinx.datetime import kotlin.random.Random import kotlin.random.nextInt -internal class LocalDateProgressionIterator(private val iterator: IntIterator) : Iterator { +private class LocalDateProgressionIterator(private val iterator: IntIterator) : Iterator { override fun hasNext(): Boolean = iterator.hasNext() override fun next(): LocalDate = LocalDate.fromEpochDays(iterator.next()) } From 33222c638c180f5d425acc30620f707b0339f015 Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Thu, 5 Sep 2024 13:24:18 -0400 Subject: [PATCH 10/57] Specify that steps are in days in string representation of LocalDateProgression --- core/common/src/LocalDateRange.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/common/src/LocalDateRange.kt b/core/common/src/LocalDateRange.kt index 08006a5da..26cdd675d 100644 --- a/core/common/src/LocalDateRange.kt +++ b/core/common/src/LocalDateRange.kt @@ -29,7 +29,7 @@ internal constructor(internal val intProgression: IntProgression) : Iterable 0) "$first..$last step ${intProgression.step}" else "$first downTo $last step ${intProgression.step}" + override fun toString(): String = if (intProgression.step > 0) "$first..$last step ${intProgression.step}D" else "$first downTo $last step ${intProgression.step}D" override fun equals(other: Any?): Boolean { if (this === other) return true From 372fa1b16deb537bbba78a0487a9be174c550367 Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Thu, 5 Sep 2024 13:25:42 -0400 Subject: [PATCH 11/57] Add deprecation warning to endExclusive of LocalDateRange --- core/common/src/LocalDateRange.kt | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/core/common/src/LocalDateRange.kt b/core/common/src/LocalDateRange.kt index 26cdd675d..4161b2ba3 100644 --- a/core/common/src/LocalDateRange.kt +++ b/core/common/src/LocalDateRange.kt @@ -58,7 +58,16 @@ internal constructor(internal val intProgression: IntProgression) : Iterable, OpenEndRange { override val start: LocalDate get() = first override val endInclusive: LocalDate get() = last - override val endExclusive: LocalDate get() = endInclusive.plus(1, DateTimeUnit.DAY) + @Deprecated( + "This throws an exception if the exclusive end if not inside " + + "the platform-specific boundaries for LocalDate. " + + "The 'endInclusive' property does not throw and should be preferred.", + level = DeprecationLevel.WARNING + ) + override val endExclusive: LocalDate get(){ + if (last == LocalDate.MAX) error("Cannot return the exclusive upper bound of a range that includes LocalDate.MAX.") + return endInclusive.plus(1, DateTimeUnit.DAY) + } @Suppress("ConvertTwoComparisonsToRangeCheck") override fun contains(value: LocalDate): Boolean = first <= value && value <= last From 72fa3a5f3d01b77e7e859f93f0a7ff2512fc4ba6 Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Thu, 5 Sep 2024 13:33:43 -0400 Subject: [PATCH 12/57] Remove downUntil from LocalDateRange --- core/common/src/LocalDateRange.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/core/common/src/LocalDateRange.kt b/core/common/src/LocalDateRange.kt index 4161b2ba3..1dc452bb4 100644 --- a/core/common/src/LocalDateRange.kt +++ b/core/common/src/LocalDateRange.kt @@ -102,7 +102,6 @@ public fun LocalDateProgression.step(value: Int, unit: DateTimeUnit.DayBased) : public fun LocalDateProgression.step(value: Long, unit: DateTimeUnit.DayBased) : LocalDateProgression = step(value.toInt(), unit) public infix fun LocalDate.downTo(that: LocalDate) : LocalDateProgression = LocalDateProgression.fromClosedRange(this, that, -1, DateTimeUnit.DAY) -public infix fun LocalDate.downUntil(that: LocalDate) : LocalDateProgression = downTo(that.plus(1, DateTimeUnit.DAY)) public operator fun LocalDate.rangeTo(that: LocalDate): LocalDateRange = LocalDateRange(this, that) public operator fun LocalDate.rangeUntil(that: LocalDate) : LocalDateRange = rangeTo(that.minus(1, DateTimeUnit.DAY)) From df405c7f56eb47f1bb632f07b8a6f8a5abc4d27d Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Thu, 5 Sep 2024 13:35:01 -0400 Subject: [PATCH 13/57] Simplify LocalDateProgression.random() --- core/common/src/LocalDateRange.kt | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/core/common/src/LocalDateRange.kt b/core/common/src/LocalDateRange.kt index 1dc452bb4..0d2b882ce 100644 --- a/core/common/src/LocalDateRange.kt +++ b/core/common/src/LocalDateRange.kt @@ -106,21 +106,12 @@ public infix fun LocalDate.downTo(that: LocalDate) : LocalDateProgression = Loca public operator fun LocalDate.rangeTo(that: LocalDate): LocalDateRange = LocalDateRange(this, that) public operator fun LocalDate.rangeUntil(that: LocalDate) : LocalDateRange = rangeTo(that.minus(1, DateTimeUnit.DAY)) -public fun LocalDateProgression.random(randomIntFunction: (IntRange) -> Int) : LocalDate = intProgression.random(randomIntFunction) - .let(LocalDate.Companion::fromEpochDays) public fun LocalDateProgression.random(random: Random = Random) : LocalDate = intProgression.random(random).let(LocalDate.Companion::fromEpochDays) -public fun LocalDateProgression.randomOrNull(randomIntFunction: (range: IntRange) -> Int) : LocalDate? = intProgression.randomOrNull(randomIntFunction) - ?.let(LocalDate.Companion::fromEpochDays) - public fun LocalDateProgression.randomOrNull(random: Random = Random) : LocalDate? = intProgression.randomOrNull(random) ?.let(LocalDate.Companion::fromEpochDays) -public inline fun IntProgression.random(func: (IntRange) -> Int) : Int = func(0..(last - first) / step) * step + first - -public fun IntProgression.random(random: Random = Random) : Int = random(random::nextInt) - -public inline fun IntProgression.randomOrNull(randomIntFunction: (range: IntRange) -> Int) : Int? = if (isEmpty()) null else random(randomIntFunction) +public fun IntProgression.random(random: Random = Random) : Int = random.nextInt(0..(last - first) / step) * step + first public fun IntProgression.randomOrNull(random: Random = Random) : Int? = if (isEmpty()) null else random(random) From f2d2566d4797b286bfc8ee6273827d0f03bcafd2 Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Thu, 5 Sep 2024 13:54:38 -0400 Subject: [PATCH 14/57] Use safeMultiply in LocalDateRange --- core/common/src/LocalDateRange.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/common/src/LocalDateRange.kt b/core/common/src/LocalDateRange.kt index 0d2b882ce..a781ff837 100644 --- a/core/common/src/LocalDateRange.kt +++ b/core/common/src/LocalDateRange.kt @@ -5,6 +5,7 @@ package kotlinx.datetime +import kotlinx.datetime.internal.safeMultiply import kotlin.random.Random import kotlin.random.nextInt @@ -51,7 +52,7 @@ internal constructor(internal val intProgression: IntProgression) : Iterable Date: Thu, 5 Sep 2024 13:56:39 -0400 Subject: [PATCH 15/57] Make fromClosedRange internal --- core/common/src/LocalDateRange.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/common/src/LocalDateRange.kt b/core/common/src/LocalDateRange.kt index a781ff837..8e94042c2 100644 --- a/core/common/src/LocalDateRange.kt +++ b/core/common/src/LocalDateRange.kt @@ -47,7 +47,7 @@ internal constructor(internal val intProgression: IntProgression) : Iterable Date: Thu, 5 Sep 2024 18:00:37 -0400 Subject: [PATCH 16/57] Simplify equals method in LocalDateProgression --- core/common/src/LocalDateRange.kt | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/core/common/src/LocalDateRange.kt b/core/common/src/LocalDateRange.kt index 8e94042c2..388a91cda 100644 --- a/core/common/src/LocalDateRange.kt +++ b/core/common/src/LocalDateRange.kt @@ -33,12 +33,7 @@ internal constructor(internal val intProgression: IntProgression) : Iterable 0) "$first..$last step ${intProgression.step}D" else "$first downTo $last step ${intProgression.step}D" override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other == null || this::class != other::class) return false - - other as LocalDateProgression - - return intProgression == other.intProgression + return other is LocalDateProgression && intProgression == other.intProgression } override fun hashCode(): Int { From 358e60d716f8865928d31ca8bdff3a7693df024f Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Mon, 9 Sep 2024 17:22:43 -0400 Subject: [PATCH 17/57] Add to/from epoch days in Long to LocalDate --- core/common/src/LocalDate.kt | 19 +++++++++++++++++++ core/commonJs/src/LocalDate.kt | 9 +++++++++ core/jvm/src/LocalDate.kt | 5 +++++ core/native/src/LocalDate.kt | 11 +++++++++++ 4 files changed, 44 insertions(+) diff --git a/core/common/src/LocalDate.kt b/core/common/src/LocalDate.kt index 0146d6f95..57a6b67c2 100644 --- a/core/common/src/LocalDate.kt +++ b/core/common/src/LocalDate.kt @@ -92,6 +92,15 @@ public expect class LocalDate : Comparable { */ public fun fromEpochDays(epochDays: Int): LocalDate + /** + * Returns a [LocalDate] that is [epochDays] number of days from the epoch day `1970-01-01`. + * + * @throws IllegalArgumentException if the result exceeds the platform-specific boundaries of [LocalDate]. + * @see LocalDate.toEpochDaysLong + * @sample kotlinx.datetime.test.samples.LocalDateSamples.fromAndToEpochDays + */ + internal fun fromEpochDays(epochDays: Long): LocalDate + /** * Creates a new format for parsing and formatting [LocalDate] values. * @@ -238,6 +247,16 @@ public expect class LocalDate : Comparable { */ public fun toEpochDays(): Int + /** + * Returns the number of days since the epoch day `1970-01-01`. + * + * If the result does not fit in [Long], returns [Long.MAX_VALUE] for a positive result or [Long.MIN_VALUE] for a negative result. + * + * @see LocalDate.fromEpochDays + * @sample kotlinx.datetime.test.samples.LocalDateSamples.toEpochDays + */ + internal fun toEpochDaysLong(): Long + /** * Compares `this` date with the [other] date. * Returns zero if this date represents the same day as the other (meaning they are equal to one other), diff --git a/core/commonJs/src/LocalDate.kt b/core/commonJs/src/LocalDate.kt index a650665d7..2c2333c8d 100644 --- a/core/commonJs/src/LocalDate.kt +++ b/core/commonJs/src/LocalDate.kt @@ -42,6 +42,13 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa throw e } + internal actual fun fromEpochDays(epochDays: Long): LocalDate { + require(epochDays in Int.MIN_VALUE..Int.MAX_VALUE) { + "Invalid date: boundaries of LocalDate exceeded" + } + return fromEpochDays(epochDays.toInt()) + } + @Suppress("FunctionName") public actual fun Format(block: DateTimeFormatBuilder.WithDate.() -> Unit): DateTimeFormat = LocalDateFormat.build(block) @@ -80,6 +87,8 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa actual override fun compareTo(other: LocalDate): Int = this.value.compareTo(other.value) public actual fun toEpochDays(): Int = value.toEpochDay().toInt() + + internal actual fun toEpochDaysLong(): Long = value.toEpochDay().toLong() } @Deprecated("Use the plus overload with an explicit number of units", ReplaceWith("this.plus(1, unit)")) diff --git a/core/jvm/src/LocalDate.kt b/core/jvm/src/LocalDate.kt index fe3b9ae1a..95bc7aabd 100644 --- a/core/jvm/src/LocalDate.kt +++ b/core/jvm/src/LocalDate.kt @@ -39,6 +39,9 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa public actual fun fromEpochDays(epochDays: Int): LocalDate = LocalDate(jtLocalDate.ofEpochDay(epochDays.toLong())) + internal actual fun fromEpochDays(epochDays: Long) : LocalDate = + LocalDate(jtLocalDate.ofEpochDay(epochDays)) + @Suppress("FunctionName") public actual fun Format(block: DateTimeFormatBuilder.WithDate.() -> Unit): DateTimeFormat = LocalDateFormat.build(block) @@ -76,6 +79,8 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa actual override fun compareTo(other: LocalDate): Int = this.value.compareTo(other.value) public actual fun toEpochDays(): Int = value.toEpochDay().clampToInt() + + internal actual fun toEpochDaysLong(): Long = value.toEpochDay() } @Deprecated("Use the plus overload with an explicit number of units", ReplaceWith("this.plus(1, unit)")) diff --git a/core/native/src/LocalDate.kt b/core/native/src/LocalDate.kt index 14ee3a173..9abc840c4 100644 --- a/core/native/src/LocalDate.kt +++ b/core/native/src/LocalDate.kt @@ -82,6 +82,13 @@ public actual class LocalDate actual constructor(public actual val year: Int, pu return LocalDate(yearEst, month, dom) } + public actual fun fromEpochDays(epochDays: Long): LocalDate { + require(epochDays in MIN_EPOCH_DAY..MAX_EPOCH_DAY) { + "Invalid date: boundaries of LocalDate exceeded" + } + return fromEpochDays(epochDays.toInt()) + } + internal actual val MIN = LocalDate(YEAR_MIN, 1, 1) internal actual val MAX = LocalDate(YEAR_MAX, 12, 31) @@ -121,6 +128,10 @@ public actual class LocalDate actual constructor(public actual val year: Int, pu return total - DAYS_0000_TO_1970 } + internal actual fun toEpochDaysLong() : Long { + return toEpochDays().toLong() + } + public actual val month: Month get() = Month(monthNumber) From 4911221321b23b812882720d18b8e1a65c921385 Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Mon, 9 Sep 2024 17:27:35 -0400 Subject: [PATCH 18/57] Correct LocalDateRangeTest --- core/common/test/LocalDateRangeTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/common/test/LocalDateRangeTest.kt b/core/common/test/LocalDateRangeTest.kt index ce30e63d4..5fa3cfc4f 100644 --- a/core/common/test/LocalDateRangeTest.kt +++ b/core/common/test/LocalDateRangeTest.kt @@ -95,15 +95,15 @@ class LocalDateRangeTest { (Jan_01_2000..Jan_05_2000).toString() ) assertEquals( - "2000-01-05 downTo 2000-01-01 step -1", + "2000-01-05 downTo 2000-01-01 step -1D", (Jan_05_2000 downTo Jan_01_2000).toString() ) assertEquals( - "2000-01-01..2000-01-05 step 1", + "2000-01-01..2000-01-05 step 1D", LocalDateProgression.fromClosedRange(Jan_01_2000, Jan_05_2000, 1, DateTimeUnit.DAY).toString() ) assertEquals( - "2000-01-05 downTo 2000-01-01 step -1", + "2000-01-05 downTo 2000-01-01 step -1D", LocalDateProgression.fromClosedRange(Jan_05_2000, Jan_01_2000, -1, DateTimeUnit.DAY).toString() ) } From 430bfdce6e0c48375fb4987aab2c8606ce4b75f4 Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Mon, 9 Sep 2024 17:34:10 -0400 Subject: [PATCH 19/57] Switch from IntProgression to LongProgression in LocalDateProgression --- core/common/src/LocalDateRange.kt | 66 ++++++++++++++++---------- core/common/test/LocalDateRangeTest.kt | 15 +++--- 2 files changed, 48 insertions(+), 33 deletions(-) diff --git a/core/common/src/LocalDateRange.kt b/core/common/src/LocalDateRange.kt index 388a91cda..560609fc8 100644 --- a/core/common/src/LocalDateRange.kt +++ b/core/common/src/LocalDateRange.kt @@ -5,49 +5,54 @@ package kotlinx.datetime +import kotlinx.datetime.internal.clampToInt +import kotlinx.datetime.internal.safeAdd import kotlinx.datetime.internal.safeMultiply import kotlin.random.Random -import kotlin.random.nextInt +import kotlin.random.nextLong -private class LocalDateProgressionIterator(private val iterator: IntIterator) : Iterator { +private class LocalDateProgressionIterator(private val iterator: LongIterator) : Iterator { override fun hasNext(): Boolean = iterator.hasNext() override fun next(): LocalDate = LocalDate.fromEpochDays(iterator.next()) } public open class LocalDateProgression -internal constructor(internal val intProgression: IntProgression) : Iterable { +internal constructor(internal val longProgression: LongProgression) : Collection { internal constructor( start: LocalDate, endInclusive: LocalDate, - step: Int - ) : this(IntProgression.fromClosedRange(start.toEpochDays(), endInclusive.toEpochDays(), step)) + step: Long + ) : this(LongProgression.fromClosedRange(start.toEpochDaysLong(), endInclusive.toEpochDaysLong(), step)) - public val first: LocalDate = LocalDate.fromEpochDays(intProgression.first) - public val last: LocalDate = LocalDate.fromEpochDays(intProgression.last) + public val first: LocalDate = LocalDate.fromEpochDays(longProgression.first) - override fun iterator(): Iterator = LocalDateProgressionIterator(intProgression.iterator()) + public val last: LocalDate = LocalDate.fromEpochDays(longProgression.last) - public open fun isEmpty(): Boolean = intProgression.isEmpty() + override fun iterator(): Iterator = LocalDateProgressionIterator(longProgression.iterator()) - override fun toString(): String = if (intProgression.step > 0) "$first..$last step ${intProgression.step}D" else "$first downTo $last step ${intProgression.step}D" + public override fun isEmpty(): Boolean = longProgression.isEmpty() - override fun equals(other: Any?): Boolean { - return other is LocalDateProgression && intProgression == other.intProgression - } + override fun toString(): String = if (longProgression.step > 0) "$first..$last step ${longProgression.step}D" else "$first downTo $last step ${longProgression.step}D" - override fun hashCode(): Int { - return intProgression.hashCode() - } + override val size: Int + get() = longProgression.size + + override fun containsAll(elements: Collection): Boolean = elements.all(::contains) + + override fun contains(element: LocalDate): Boolean = longProgression.contains(element.toEpochDaysLong()) + + override fun equals(other: Any?): Boolean = other is LocalDateProgression && longProgression == other.longProgression + override fun hashCode(): Int = longProgression.hashCode() public companion object { internal fun fromClosedRange( rangeStart: LocalDate, rangeEnd: LocalDate, - stepValue: Int, + stepValue: Long, stepUnit: DateTimeUnit.DayBased - ): LocalDateProgression = LocalDateProgression(rangeStart, rangeEnd, safeMultiply(stepValue, stepUnit.days)) + ): LocalDateProgression = LocalDateProgression(rangeStart, rangeEnd, safeMultiply(stepValue, stepUnit.days.toLong())) } } @@ -90,24 +95,33 @@ public fun LocalDateProgression.last(): LocalDate { return this.last } public fun LocalDateProgression.firstOrNull(): LocalDate? = if (isEmpty()) null else this.first + public fun LocalDateProgression.lastOrNull(): LocalDate? = if (isEmpty()) null else this.last -public fun LocalDateProgression.reversed(): LocalDateProgression = LocalDateProgression(intProgression.reversed()) +public fun LocalDateProgression.reversed(): LocalDateProgression = LocalDateProgression(longProgression.reversed()) -public fun LocalDateProgression.step(value: Int, unit: DateTimeUnit.DayBased) : LocalDateProgression = LocalDateProgression(intProgression.step(safeMultiply(value, unit.days))) -public fun LocalDateProgression.step(value: Long, unit: DateTimeUnit.DayBased) : LocalDateProgression = step(value.toInt(), unit) +public fun LocalDateProgression.step(value: Int, unit: DateTimeUnit.DayBased) : LocalDateProgression = step(value.toLong(), unit) +public fun LocalDateProgression.step(value: Long, unit: DateTimeUnit.DayBased) : LocalDateProgression = LocalDateProgression(longProgression.step(safeMultiply(value, unit.days.toLong()))) public infix fun LocalDate.downTo(that: LocalDate) : LocalDateProgression = LocalDateProgression.fromClosedRange(this, that, -1, DateTimeUnit.DAY) public operator fun LocalDate.rangeTo(that: LocalDate): LocalDateRange = LocalDateRange(this, that) public operator fun LocalDate.rangeUntil(that: LocalDate) : LocalDateRange = rangeTo(that.minus(1, DateTimeUnit.DAY)) +public fun LocalDateProgression.random(random: Random = Random) : LocalDate = longProgression.random(random).let(LocalDate.Companion::fromEpochDays) -public fun LocalDateProgression.random(random: Random = Random) : LocalDate = intProgression.random(random).let(LocalDate.Companion::fromEpochDays) - -public fun LocalDateProgression.randomOrNull(random: Random = Random) : LocalDate? = intProgression.randomOrNull(random) +public fun LocalDateProgression.randomOrNull(random: Random = Random) : LocalDate? = longProgression.randomOrNull(random) ?.let(LocalDate.Companion::fromEpochDays) -public fun IntProgression.random(random: Random = Random) : Int = random.nextInt(0..(last - first) / step) * step + first +internal fun LongProgression.random(random: Random = Random) : Long = random.nextLong(0L..(last - first) / step) * step + first + +internal fun LongProgression.randomOrNull(random: Random = Random) : Long? = if (isEmpty()) null else random(random) + +internal fun LongProgression.contains(element: Long) : Boolean = element in first..last && (element - first) % step == 0L -public fun IntProgression.randomOrNull(random: Random = Random) : Int? = if (isEmpty()) null else random(random) +internal val LongProgression.size: Int + get() = if(isEmpty()) 0 else try { + (safeAdd(last, -first) / step + 1).clampToInt() + } catch (e: ArithmeticException) { + Int.MAX_VALUE + } \ No newline at end of file diff --git a/core/common/test/LocalDateRangeTest.kt b/core/common/test/LocalDateRangeTest.kt index 5fa3cfc4f..b3791567c 100644 --- a/core/common/test/LocalDateRangeTest.kt +++ b/core/common/test/LocalDateRangeTest.kt @@ -8,6 +8,7 @@ package kotlinx.datetime.test import kotlinx.datetime.* import kotlin.random.Random import kotlin.random.nextInt +import kotlin.random.nextLong import kotlin.test.* class LocalDateRangeTest { @@ -132,31 +133,31 @@ class LocalDateRangeTest { repeat(20) { assertEquals( - expectedRand.nextInt(0..23).let { Jan_01_2000.plus(it, DateTimeUnit.DAY) }, + expectedRand.nextLong(0L..23L).let { Jan_01_2000.plus(it, DateTimeUnit.DAY) }, (Jan_01_2000..Jan_24_2000).random(actualRand) ) } repeat(20) { assertEquals( - expectedRand.nextInt(0..23).let { Jan_24_2000.minus(it, DateTimeUnit.DAY) }, + expectedRand.nextLong(0L..23L).let { Jan_24_2000.minus(it, DateTimeUnit.DAY) }, (Jan_24_2000 downTo Jan_01_2000).random(actualRand) ) } - listOf(1, 2, 5, 30).forEach { step -> + listOf(1L, 2L, 5L, 30L).forEach { step -> repeat(20) { - val range = (0..23 step step) + val range = (0L..23L step step) assertEquals( - expectedRand.nextInt(0..range.last / step).let { Jan_01_2000.plus(it * step, DateTimeUnit.DAY) }, + expectedRand.nextLong(0L..range.last / step).let { Jan_01_2000.plus(it * step, DateTimeUnit.DAY) }, (Jan_01_2000..Jan_24_2000).step(step, DateTimeUnit.DAY).random(actualRand) ) } repeat(20) { - val range = (0..23 step step) + val range = (0L..23L step step) assertEquals( - expectedRand.nextInt(0..range.last / step).let { Jan_24_2000.minus(it * step, DateTimeUnit.DAY) }, + expectedRand.nextLong(0..range.last / step).let { Jan_24_2000.minus(it * step, DateTimeUnit.DAY) }, (Jan_24_2000 downTo Jan_01_2000).step(step, DateTimeUnit.DAY).random(actualRand) ) } From 77c9ab103c9da20bdfb5b463d2f04b41cb1b29ee Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Mon, 9 Sep 2024 17:34:42 -0400 Subject: [PATCH 20/57] Delete downUntil test --- core/common/test/LocalDateRangeTest.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/core/common/test/LocalDateRangeTest.kt b/core/common/test/LocalDateRangeTest.kt index b3791567c..4d623f551 100644 --- a/core/common/test/LocalDateRangeTest.kt +++ b/core/common/test/LocalDateRangeTest.kt @@ -58,10 +58,6 @@ class LocalDateRangeTest { (5 downTo 1).map { LocalDate(2000, 1, it) }, Jan_05_2000 downTo Jan_01_2000 ) - assertContentEquals( - (5 downTo 2).map { LocalDate(2000, 1, it) }, - Jan_05_2000 downUntil Jan_01_2000 - ) assertContentEquals( listOf(Jan_01_2000), Jan_01_2000 downTo Jan_01_2000 From c04b640b0bc28613f5290c35634586da69fb112f Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Mon, 9 Sep 2024 17:40:58 -0400 Subject: [PATCH 21/57] Samples and documentation for LocalDateRange --- core/common/src/LocalDateRange.kt | 144 ++++++++++++++++++ .../test/samples/LocalDateRangeSamples.kt | 57 +++++++ 2 files changed, 201 insertions(+) create mode 100644 core/common/test/samples/LocalDateRangeSamples.kt diff --git a/core/common/src/LocalDateRange.kt b/core/common/src/LocalDateRange.kt index 560609fc8..e3c3df927 100644 --- a/core/common/src/LocalDateRange.kt +++ b/core/common/src/LocalDateRange.kt @@ -16,6 +16,13 @@ private class LocalDateProgressionIterator(private val iterator: LongIterator) : override fun next(): LocalDate = LocalDate.fromEpochDays(iterator.next()) } +/** + * A progression of values of type [LocalDate]. + * + * @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.progressionWithStep + * @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.reversedProgression + * @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.firstAndLast + */ public open class LocalDateProgression internal constructor(internal val longProgression: LongProgression) : Collection { @@ -25,21 +32,49 @@ internal constructor(internal val longProgression: LongProgression) : Collection step: Long ) : this(LongProgression.fromClosedRange(start.toEpochDaysLong(), endInclusive.toEpochDaysLong(), step)) + /** + * Returns the first [LocalDate] of the progression + */ public val first: LocalDate = LocalDate.fromEpochDays(longProgression.first) + /** + * Returns the last [LocalDate] of the progression + */ public val last: LocalDate = LocalDate.fromEpochDays(longProgression.last) + /** + * Returns an [Iterator] that traverses the progression from [first] to [last] + */ override fun iterator(): Iterator = LocalDateProgressionIterator(longProgression.iterator()) + /** + * Returns true iff the progression contains no values. + * i.e. [first] < [last] if step is positive, or [first] > [last] if step is negative. + */ public override fun isEmpty(): Boolean = longProgression.isEmpty() + /** + * Returns a string representation of the progression. + * Uses the range operator notation if the progression is increasing, and `downTo` if it is decreasing. + * The step is referenced in days. + */ override fun toString(): String = if (longProgression.step > 0) "$first..$last step ${longProgression.step}D" else "$first downTo $last step ${longProgression.step}D" + /** + * Returns the number of dates in the progression. + * Returns [Int.MAX_VALUE] if the number of dates overflows [Int] + */ override val size: Int get() = longProgression.size + /** + * Returns true iff every element in [elements] is a member of the progression. + */ override fun containsAll(elements: Collection): Boolean = elements.all(::contains) + /** + * Returns true iff [element] is a member of the progression. + */ override fun contains(element: LocalDate): Boolean = longProgression.contains(element.toEpochDaysLong()) override fun equals(other: Any?): Boolean = other is LocalDateProgression && longProgression == other.longProgression @@ -56,9 +91,25 @@ internal constructor(internal val longProgression: LongProgression) : Collection } } +/** + * A range of values of type [LocalDate]. + * + * @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.simpleRangeCreation + */ public class LocalDateRange(start: LocalDate, endInclusive: LocalDate) : LocalDateProgression(start, endInclusive, 1), ClosedRange, OpenEndRange { + /** + * Returns the lower bound of the range, inclusive. + */ override val start: LocalDate get() = first + + /** + * Returns the upper bound of the range, inclusive. + */ override val endInclusive: LocalDate get() = last + + /** + * returns the upper bound of the range, exclusive. + */ @Deprecated( "This throws an exception if the exclusive end if not inside " + "the platform-specific boundaries for LocalDate. " + @@ -70,11 +121,21 @@ public class LocalDateRange(start: LocalDate, endInclusive: LocalDate) : LocalDa return endInclusive.plus(1, DateTimeUnit.DAY) } + /** + * Returns true iff [value] is contained within the range. + * i.e. [value] is between [start] and [endInclusive]. + */ @Suppress("ConvertTwoComparisonsToRangeCheck") override fun contains(value: LocalDate): Boolean = first <= value && value <= last + /** + * Returns true iff there are no dates contained within the range. + */ override fun isEmpty(): Boolean = first > last + /** + * Returns a string representation of the range using the range operator notation. + */ override fun toString(): String = "$first..$last" public companion object { @@ -84,32 +145,115 @@ public class LocalDateRange(start: LocalDate, endInclusive: LocalDate) : LocalDa } } +/** + * Returns the first [LocalDate] of the [LocalDateProgression]. + * + * @throws NoSuchElementException if the progression is empty. + * + * @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.firstAndLast + */ public fun LocalDateProgression.first(): LocalDate { if (isEmpty()) throw NoSuchElementException("Progression $this is empty.") return this.first } + +/** + * Returns the last [LocalDate] of the [LocalDateProgression]. + * + * @throws NoSuchElementException if the progression is empty. + * + * @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.firstAndLast + */ public fun LocalDateProgression.last(): LocalDate { if (isEmpty()) throw NoSuchElementException("Progression $this is empty.") return this.last } + +/** + * Returns the first [LocalDate] of the [LocalDateProgression], or null if the progression is empty. + * + * @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.firstAndLast + */ public fun LocalDateProgression.firstOrNull(): LocalDate? = if (isEmpty()) null else this.first +/** + * Returns the last [LocalDate] of the [LocalDateProgression], or null if the progression is empty. + * + * @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.firstAndLast + */ public fun LocalDateProgression.lastOrNull(): LocalDate? = if (isEmpty()) null else this.last +/** + * Returns a reversed [LocalDateProgression], i.e. one that goes from [last] to [first]. + * The sign of the step is switched, in order to reverse the directionality of the progression. + * + * @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.reversedProgression + */ public fun LocalDateProgression.reversed(): LocalDateProgression = LocalDateProgression(longProgression.reversed()) +/** + * Returns a [LocalDateProgression] with the same start and end, but a changed step value. + * + * **Pitfall**: the value parameter represents the magnitude of the step, not the direction, and therefore must be positive. + * Its sign will be matched to the sign of the existing step, in order to maintain the directionality of the progression. + * If you wish to switch the direction of the progression, use [LocalDateProgression.reversed] + * + * @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.progressionWithStep + */ public fun LocalDateProgression.step(value: Int, unit: DateTimeUnit.DayBased) : LocalDateProgression = step(value.toLong(), unit) + +/** + * Returns a [LocalDateProgression] with the same start and end, but a changed step value. + * + * **Pitfall**: the value parameter represents the magnitude of the step, not the direction, and therefore must be positive. + * Its sign will be matched to the sign of the existing step, in order to maintain the directionality of the progression. + * If you wish to switch the direction of the progression, use [LocalDateProgression.reversed] + * + * @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.progressionWithStep + */ public fun LocalDateProgression.step(value: Long, unit: DateTimeUnit.DayBased) : LocalDateProgression = LocalDateProgression(longProgression.step(safeMultiply(value, unit.days.toLong()))) +/** + * Creates a [LocalDateProgression] from `this` down to [that], inclusive. + * + * @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.simpleRangeCreation + */ public infix fun LocalDate.downTo(that: LocalDate) : LocalDateProgression = LocalDateProgression.fromClosedRange(this, that, -1, DateTimeUnit.DAY) +/** + * Creates a [LocalDateRange] from `this` to [that], inclusive. + * + * @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.simpleRangeCreation + */ public operator fun LocalDate.rangeTo(that: LocalDate): LocalDateRange = LocalDateRange(this, that) + +/** + * Creates a [LocalDateRange] from `this` to [that], exclusive. i.e. from this to (that - 1 day) + * + * @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.simpleRangeCreation + */ public operator fun LocalDate.rangeUntil(that: LocalDate) : LocalDateRange = rangeTo(that.minus(1, DateTimeUnit.DAY)) +/** + * Returns a random [LocalDate] within the bounds of the [LocalDateProgression]. + * + * Takes the step into account; will not return any value within the range that would be skipped over by the progression. + * + * @throws IllegalArgumentException if the progression is empty. + * + * @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.random + */ public fun LocalDateProgression.random(random: Random = Random) : LocalDate = longProgression.random(random).let(LocalDate.Companion::fromEpochDays) +/** + * Returns a random [LocalDate] within the bounds of the [LocalDateProgression] or null if the progression is empty. + * + * Takes the step into account; will not return any value within the range that would be skipped over by the progression. + * + * @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.random + */ public fun LocalDateProgression.randomOrNull(random: Random = Random) : LocalDate? = longProgression.randomOrNull(random) ?.let(LocalDate.Companion::fromEpochDays) diff --git a/core/common/test/samples/LocalDateRangeSamples.kt b/core/common/test/samples/LocalDateRangeSamples.kt new file mode 100644 index 000000000..04a59f5f6 --- /dev/null +++ b/core/common/test/samples/LocalDateRangeSamples.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2019-2024 JetBrains s.r.o. and contributors. + * 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.samples + +import kotlinx.datetime.* +import kotlin.random.Random +import kotlin.test.Test + +class LocalDateRangeSamples { + val Jan_01_2000 = LocalDate(2000, 1, 1) + val Jan_05_2000 = LocalDate(2000, 1, 5) + val Jan_06_2000 = LocalDate(2000, 1, 6) + + @Test + fun simpleRangeCreation() { + // Creating LocalDateRange from LocalDate + check(Jan_01_2000..Jan_05_2000 == LocalDateRange(Jan_01_2000, Jan_05_2000)) + check(Jan_01_2000.. Date: Tue, 10 Sep 2024 11:20:43 -0400 Subject: [PATCH 22/57] Simplify LocalDateRange.EMPTY --- core/common/src/LocalDateRange.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/core/common/src/LocalDateRange.kt b/core/common/src/LocalDateRange.kt index e3c3df927..9e8174e15 100644 --- a/core/common/src/LocalDateRange.kt +++ b/core/common/src/LocalDateRange.kt @@ -139,9 +139,7 @@ public class LocalDateRange(start: LocalDate, endInclusive: LocalDate) : LocalDa override fun toString(): String = "$first..$last" public companion object { - private val DATE_ONE = LocalDate(1, 1, 1) - private val DATE_TWO = LocalDate(1, 1, 2) - public val EMPTY: LocalDateRange = LocalDateRange(DATE_TWO, DATE_ONE) + public val EMPTY: LocalDateRange = LocalDateRange(LocalDate(1970, 1, 2), LocalDate(1970, 1, 1)) } } From 7bff3cceeb49eb196ecafe43afe5d09f44b699e1 Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Tue, 10 Sep 2024 11:21:02 -0400 Subject: [PATCH 23/57] Fix logic of LongProgression.contains --- core/common/src/LocalDateRange.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/common/src/LocalDateRange.kt b/core/common/src/LocalDateRange.kt index 9e8174e15..395275682 100644 --- a/core/common/src/LocalDateRange.kt +++ b/core/common/src/LocalDateRange.kt @@ -259,7 +259,7 @@ internal fun LongProgression.random(random: Random = Random) : Long = random.nex internal fun LongProgression.randomOrNull(random: Random = Random) : Long? = if (isEmpty()) null else random(random) -internal fun LongProgression.contains(element: Long) : Boolean = element in first..last && (element - first) % step == 0L +internal fun LongProgression.contains(element: Long) : Boolean = element in (if(step > 0) first..last else last..first) && (element - first) % step == 0L internal val LongProgression.size: Int get() = if(isEmpty()) 0 else try { From c8f88ab7d55a965ed300e365cfbdec990e3e9d52 Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Tue, 10 Sep 2024 11:22:04 -0400 Subject: [PATCH 24/57] Move LocalDate range operators from extension functions to class methods --- core/common/src/LocalDate.kt | 14 ++++++++++++++ core/common/src/LocalDateRange.kt | 22 ++++++++-------------- core/commonJs/src/LocalDate.kt | 4 ++++ core/jvm/src/LocalDate.kt | 4 ++++ core/native/src/LocalDate.kt | 4 ++++ 5 files changed, 34 insertions(+), 14 deletions(-) diff --git a/core/common/src/LocalDate.kt b/core/common/src/LocalDate.kt index 57a6b67c2..97a1565fa 100644 --- a/core/common/src/LocalDate.kt +++ b/core/common/src/LocalDate.kt @@ -276,6 +276,20 @@ public expect class LocalDate : Comparable { * @sample kotlinx.datetime.test.samples.LocalDateSamples.toStringSample */ public override fun toString(): String + + /** + * Creates a [LocalDateRange] from `this` to [that], inclusive. + * + * @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.simpleRangeCreation + */ + public operator fun rangeTo(that: LocalDate): LocalDateRange + + /** + * Creates a [LocalDateRange] from `this` to [that], exclusive. i.e. from this to (that - 1 day) + * + * @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.simpleRangeCreation + */ + public operator fun rangeUntil(that: LocalDate) : LocalDateRange } /** diff --git a/core/common/src/LocalDateRange.kt b/core/common/src/LocalDateRange.kt index 395275682..7697619d3 100644 --- a/core/common/src/LocalDateRange.kt +++ b/core/common/src/LocalDateRange.kt @@ -140,6 +140,14 @@ public class LocalDateRange(start: LocalDate, endInclusive: LocalDate) : LocalDa public companion object { public val EMPTY: LocalDateRange = LocalDateRange(LocalDate(1970, 1, 2), LocalDate(1970, 1, 1)) + + internal fun fromRangeUntil(start: LocalDate, endExclusive: LocalDate) : LocalDateRange { + return fromRangeTo(start, endExclusive.minus(1, DateTimeUnit.DAY)) + } + + internal fun fromRangeTo(start: LocalDate, endInclusive: LocalDate) : LocalDateRange { + return LocalDateRange(start, endInclusive) + } } } @@ -220,20 +228,6 @@ public fun LocalDateProgression.step(value: Long, unit: DateTimeUnit.DayBased) : */ public infix fun LocalDate.downTo(that: LocalDate) : LocalDateProgression = LocalDateProgression.fromClosedRange(this, that, -1, DateTimeUnit.DAY) -/** - * Creates a [LocalDateRange] from `this` to [that], inclusive. - * - * @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.simpleRangeCreation - */ -public operator fun LocalDate.rangeTo(that: LocalDate): LocalDateRange = LocalDateRange(this, that) - -/** - * Creates a [LocalDateRange] from `this` to [that], exclusive. i.e. from this to (that - 1 day) - * - * @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.simpleRangeCreation - */ -public operator fun LocalDate.rangeUntil(that: LocalDate) : LocalDateRange = rangeTo(that.minus(1, DateTimeUnit.DAY)) - /** * Returns a random [LocalDate] within the bounds of the [LocalDateProgression]. * diff --git a/core/commonJs/src/LocalDate.kt b/core/commonJs/src/LocalDate.kt index 2c2333c8d..30595c1c9 100644 --- a/core/commonJs/src/LocalDate.kt +++ b/core/commonJs/src/LocalDate.kt @@ -89,6 +89,10 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa public actual fun toEpochDays(): Int = value.toEpochDay().toInt() internal actual fun toEpochDaysLong(): Long = value.toEpochDay().toLong() + + public actual operator fun rangeTo(that: LocalDate): LocalDateRange = LocalDateRange.fromRangeTo(this, that) + + public actual operator fun rangeUntil(that: LocalDate): LocalDateRange = LocalDateRange.fromRangeUntil(this, that) } @Deprecated("Use the plus overload with an explicit number of units", ReplaceWith("this.plus(1, unit)")) diff --git a/core/jvm/src/LocalDate.kt b/core/jvm/src/LocalDate.kt index 95bc7aabd..c4cb61112 100644 --- a/core/jvm/src/LocalDate.kt +++ b/core/jvm/src/LocalDate.kt @@ -81,6 +81,10 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa public actual fun toEpochDays(): Int = value.toEpochDay().clampToInt() internal actual fun toEpochDaysLong(): Long = value.toEpochDay() + + public actual operator fun rangeTo(that: LocalDate): LocalDateRange = LocalDateRange.fromRangeTo(this, that) + + public actual operator fun rangeUntil(that: LocalDate): LocalDateRange = LocalDateRange.fromRangeUntil(this, that) } @Deprecated("Use the plus overload with an explicit number of units", ReplaceWith("this.plus(1, unit)")) diff --git a/core/native/src/LocalDate.kt b/core/native/src/LocalDate.kt index 9abc840c4..3085f06e3 100644 --- a/core/native/src/LocalDate.kt +++ b/core/native/src/LocalDate.kt @@ -206,6 +206,10 @@ public actual class LocalDate actual constructor(public actual val year: Int, pu // org.threeten.bp.LocalDate#toString actual override fun toString(): String = format(Formats.ISO) + + public actual operator fun rangeTo(that: LocalDate): LocalDateRange = LocalDateRange.fromRangeTo(this, that) + + public actual operator fun rangeUntil(that: LocalDate): LocalDateRange = LocalDateRange.fromRangeUntil(this, that) } @Deprecated("Use the plus overload with an explicit number of units", ReplaceWith("this.plus(1, unit)")) From 22d263cacd1571fc09b4b9168af1c51fc59926c5 Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Tue, 10 Sep 2024 11:22:38 -0400 Subject: [PATCH 25/57] Add tests for LocalDateRange --- core/common/test/LocalDateRangeTest.kt | 98 ++++++++++++++++++- .../test/samples/LocalDateRangeSamples.kt | 1 + 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/core/common/test/LocalDateRangeTest.kt b/core/common/test/LocalDateRangeTest.kt index 4d623f551..2a79c384f 100644 --- a/core/common/test/LocalDateRangeTest.kt +++ b/core/common/test/LocalDateRangeTest.kt @@ -7,7 +7,6 @@ package kotlinx.datetime.test import kotlinx.datetime.* import kotlin.random.Random -import kotlin.random.nextInt import kotlin.random.nextLong import kotlin.test.* @@ -159,4 +158,101 @@ class LocalDateRangeTest { } } } + + @Test + fun first() { + assertEquals((Jan_01_2000..Jan_01_2000).first(), Jan_01_2000) + assertEquals((Jan_01_2000 downTo Jan_01_2000).first(), Jan_01_2000) + assertEquals((Jan_01_2000..Jan_05_2000).first(), Jan_01_2000) + assertEquals((Jan_05_2000 downTo Jan_01_2000).first(), Jan_05_2000) + assertFails { (Jan_02_2000..Jan_01_2000).first() } + assertFails { (Jan_01_2000 downTo Jan_02_2000).first() } + } + + @Test + fun last() { + assertEquals((Jan_01_2000..Jan_01_2000).last(), Jan_01_2000) + assertEquals((Jan_01_2000 downTo Jan_01_2000).last(), Jan_01_2000) + assertEquals((Jan_01_2000..Jan_05_2000).last(), Jan_05_2000) + assertEquals((Jan_05_2000 downTo Jan_01_2000).last(), Jan_01_2000) + assertFails { (Jan_02_2000..Jan_01_2000).last() } + assertFails { (Jan_01_2000 downTo Jan_02_2000).last() } + } + + @Test + fun firstOrNull() { + assertEquals((Jan_01_2000..Jan_01_2000).firstOrNull(), Jan_01_2000) + assertEquals((Jan_01_2000 downTo Jan_01_2000).firstOrNull(), Jan_01_2000) + assertEquals((Jan_01_2000..Jan_05_2000).firstOrNull(), Jan_01_2000) + assertEquals((Jan_05_2000 downTo Jan_01_2000).firstOrNull(), Jan_05_2000) + assertNull( (Jan_02_2000..Jan_01_2000).firstOrNull() ) + assertNull( (Jan_01_2000 downTo Jan_02_2000).firstOrNull() ) + } + + @Test + fun lastOrNull() { + assertEquals((Jan_01_2000..Jan_01_2000).lastOrNull(), Jan_01_2000) + assertEquals((Jan_01_2000 downTo Jan_01_2000).lastOrNull(), Jan_01_2000) + assertEquals((Jan_01_2000..Jan_05_2000).lastOrNull(), Jan_05_2000) + assertEquals((Jan_05_2000 downTo Jan_01_2000).lastOrNull(), Jan_01_2000) + assertNull( (Jan_02_2000..Jan_01_2000).lastOrNull() ) + assertNull( (Jan_01_2000 downTo Jan_02_2000).lastOrNull() ) + } + + @Test + fun reversed() { + assertContentEquals( + Jan_05_2000 downTo Jan_01_2000, + (Jan_01_2000..Jan_05_2000).reversed() + ) + assertContentEquals( + Jan_01_2000..Jan_05_2000, + (Jan_05_2000 downTo Jan_01_2000).reversed() + ) + assertContentEquals( + Jan_01_2000..Jan_01_2000, + (Jan_01_2000..Jan_01_2000).reversed() + ) + + } + + @Test + fun contains() { + assertTrue { Jan_01_2000 in Jan_01_2000..Jan_01_2000 } + assertTrue { Jan_02_2000 in Jan_01_2000..Jan_05_2000 } + assertTrue { Jan_01_2000 in Jan_01_2000 downTo Jan_01_2000 } + assertTrue { Jan_02_2000 in Jan_05_2000 downTo Jan_01_2000 } + + assertFalse { Jan_01_2000 in Jan_02_2000..Jan_02_2000 } + assertFalse { Jan_05_2000 in Jan_02_2000..Jan_02_2000 } + assertFalse { Jan_01_2000 in Jan_02_2000..Jan_05_2000 } + assertFalse { Jan_24_2000 in Jan_02_2000..Jan_02_2000 } + assertFalse { Jan_01_2000 in Jan_02_2000 downTo Jan_02_2000 } + assertFalse { Jan_05_2000 in Jan_02_2000 downTo Jan_02_2000 } + assertFalse { Jan_01_2000 in Jan_02_2000 downTo Jan_05_2000 } + assertFalse { Jan_24_2000 in Jan_05_2000 downTo Jan_02_2000 } + + assertTrue { (Jan_01_2000..Jan_01_2000).containsAll(listOf(Jan_01_2000)) } + assertTrue { (Jan_01_2000..Jan_05_2000).containsAll(listOf(Jan_01_2000, Jan_02_2000, Jan_05_2000)) } + + assertFalse { (Jan_01_2000..Jan_01_2000).containsAll(listOf(Jan_01_2000, Jan_02_2000)) } + assertFalse { (Jan_01_2000..Jan_05_2000).containsAll(listOf(Jan_01_2000, Jan_02_2000, Jan_05_2000, Jan_24_2000)) } + + } + + @Test + fun getSize() { + assertEquals(1, (Jan_01_2000..Jan_01_2000).size) + assertEquals(1, (Jan_01_2000 downTo Jan_01_2000).size) + assertEquals(2, (Jan_01_2000..Jan_02_2000).size) + assertEquals(2, (Jan_02_2000 downTo Jan_01_2000).size) + assertEquals(5, (Jan_01_2000..Jan_05_2000).size) + assertEquals(5, (Jan_05_2000 downTo Jan_01_2000).size) + assertEquals(4, (Jan_01_2000.. Date: Mon, 16 Sep 2024 12:28:39 -0400 Subject: [PATCH 26/57] Return EMPTY in case of rangeUntil LocalDate.MIN --- core/common/src/LocalDateRange.kt | 2 +- core/common/test/LocalDateRangeTest.kt | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/core/common/src/LocalDateRange.kt b/core/common/src/LocalDateRange.kt index 7697619d3..4fc770b29 100644 --- a/core/common/src/LocalDateRange.kt +++ b/core/common/src/LocalDateRange.kt @@ -142,7 +142,7 @@ public class LocalDateRange(start: LocalDate, endInclusive: LocalDate) : LocalDa public val EMPTY: LocalDateRange = LocalDateRange(LocalDate(1970, 1, 2), LocalDate(1970, 1, 1)) internal fun fromRangeUntil(start: LocalDate, endExclusive: LocalDate) : LocalDateRange { - return fromRangeTo(start, endExclusive.minus(1, DateTimeUnit.DAY)) + return if(endExclusive == LocalDate.MIN) EMPTY else fromRangeTo(start, endExclusive.minus(1, DateTimeUnit.DAY)) } internal fun fromRangeTo(start: LocalDate, endInclusive: LocalDate) : LocalDateRange { diff --git a/core/common/test/LocalDateRangeTest.kt b/core/common/test/LocalDateRangeTest.kt index 2a79c384f..bcd56f7f9 100644 --- a/core/common/test/LocalDateRangeTest.kt +++ b/core/common/test/LocalDateRangeTest.kt @@ -36,6 +36,10 @@ class LocalDateRangeTest { (1..<5).map { LocalDate(2000, 1, it) }, Jan_01_2000.. Date: Mon, 16 Sep 2024 12:29:30 -0400 Subject: [PATCH 27/57] Add tests for non-1 step in LocalDateProgression.getSize() --- core/common/test/LocalDateRangeTest.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/common/test/LocalDateRangeTest.kt b/core/common/test/LocalDateRangeTest.kt index bcd56f7f9..b8470ff0f 100644 --- a/core/common/test/LocalDateRangeTest.kt +++ b/core/common/test/LocalDateRangeTest.kt @@ -258,5 +258,12 @@ class LocalDateRangeTest { assertEquals (0, (Jan_01_2000 downTo Jan_02_2000).size) assertEquals(Int.MAX_VALUE, (LocalDate.MIN..LocalDate.MAX).size) assertEquals(Int.MAX_VALUE, (LocalDate.MAX downTo LocalDate.MIN).size) + + assertEquals(1, (Jan_01_2000..Jan_02_2000).step(2, DateTimeUnit.DAY).size) + assertEquals(3, (Jan_01_2000..Jan_05_2000).step(2, DateTimeUnit.DAY).size) + assertEquals(2, (Jan_02_2000..Jan_05_2000).step(2, DateTimeUnit.DAY).size) + assertEquals(1, (Jan_02_2000 downTo Jan_01_2000).step(2, DateTimeUnit.DAY).size) + assertEquals(3, (Jan_05_2000 downTo Jan_01_2000).step(2, DateTimeUnit.DAY).size) + assertEquals(2, (Jan_05_2000 downTo Jan_02_2000).step(2, DateTimeUnit.DAY).size) } } From d0265f2698381717927fe0d3c2cd20f8f35d01dc Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Mon, 16 Sep 2024 12:31:01 -0400 Subject: [PATCH 28/57] Add test for LocalDateProgression.random() --- core/common/test/LocalDateRangeTest.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/common/test/LocalDateRangeTest.kt b/core/common/test/LocalDateRangeTest.kt index b8470ff0f..3b4fc1988 100644 --- a/core/common/test/LocalDateRangeTest.kt +++ b/core/common/test/LocalDateRangeTest.kt @@ -161,6 +161,11 @@ class LocalDateRangeTest { ) } } + repeat(20) { + assertTrue { + (Jan_01_2000..Jan_24_2000).step(5, DateTimeUnit.DAY).let { it.contains(it.random()) } + } + } } @Test From 66a058bee49095b79c2a22b1b84e39145dd6a595 Mon Sep 17 00:00:00 2001 From: PeterAttardo Date: Fri, 4 Oct 2024 22:56:10 -0400 Subject: [PATCH 29/57] Update core/commonKotlin/src/LocalDate.kt Co-authored-by: Dmitry Khalanskiy <52952525+dkhalanskyjb@users.noreply.github.com> --- core/commonKotlin/src/LocalDate.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/core/commonKotlin/src/LocalDate.kt b/core/commonKotlin/src/LocalDate.kt index 3085f06e3..653cc5a4d 100644 --- a/core/commonKotlin/src/LocalDate.kt +++ b/core/commonKotlin/src/LocalDate.kt @@ -128,9 +128,7 @@ public actual class LocalDate actual constructor(public actual val year: Int, pu return total - DAYS_0000_TO_1970 } - internal actual fun toEpochDaysLong() : Long { - return toEpochDays().toLong() - } + internal actual fun toEpochDaysLong(): Long = toEpochDays().toLong() public actual val month: Month get() = Month(monthNumber) From 4255bf307f99603f7647e304e4bd5d0fcdf40f35 Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Fri, 4 Oct 2024 22:57:15 -0400 Subject: [PATCH 30/57] Reduce duplication --- core/jvm/src/LocalDate.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/jvm/src/LocalDate.kt b/core/jvm/src/LocalDate.kt index c4cb61112..05d7b78de 100644 --- a/core/jvm/src/LocalDate.kt +++ b/core/jvm/src/LocalDate.kt @@ -37,7 +37,7 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa internal actual val MAX: LocalDate = LocalDate(jtLocalDate.MAX) public actual fun fromEpochDays(epochDays: Int): LocalDate = - LocalDate(jtLocalDate.ofEpochDay(epochDays.toLong())) + fromEpochDays(epochDays.toLong()) internal actual fun fromEpochDays(epochDays: Long) : LocalDate = LocalDate(jtLocalDate.ofEpochDay(epochDays)) From 0880589a4460c8b532ae08f6b46b58a5f02386f3 Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Fri, 4 Oct 2024 23:21:33 -0400 Subject: [PATCH 31/57] Update LocalDateRangeSamples --- .../test/samples/LocalDateRangeSamples.kt | 87 ++++++++++++++----- 1 file changed, 67 insertions(+), 20 deletions(-) diff --git a/core/common/test/samples/LocalDateRangeSamples.kt b/core/common/test/samples/LocalDateRangeSamples.kt index 98a9416b2..7f0255466 100644 --- a/core/common/test/samples/LocalDateRangeSamples.kt +++ b/core/common/test/samples/LocalDateRangeSamples.kt @@ -10,49 +10,96 @@ import kotlin.random.Random import kotlin.test.Test class LocalDateRangeSamples { - val Jan_01_2000 = LocalDate(2000, 1, 1) - val Jan_05_2000 = LocalDate(2000, 1, 5) - val Jan_06_2000 = LocalDate(2000, 1, 6) @Test fun simpleRangeCreation() { // Creating LocalDateRange from LocalDate - check(Jan_01_2000..Jan_05_2000 == LocalDateRange(Jan_01_2000, Jan_05_2000)) - check(Jan_01_2000.. Date: Mon, 7 Oct 2024 10:45:49 -0400 Subject: [PATCH 32/57] Update core/commonJs/src/LocalDate.kt Co-authored-by: Dmitry Khalanskiy <52952525+dkhalanskyjb@users.noreply.github.com> --- core/commonJs/src/LocalDate.kt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/core/commonJs/src/LocalDate.kt b/core/commonJs/src/LocalDate.kt index 30595c1c9..946405431 100644 --- a/core/commonJs/src/LocalDate.kt +++ b/core/commonJs/src/LocalDate.kt @@ -42,12 +42,8 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa throw e } - internal actual fun fromEpochDays(epochDays: Long): LocalDate { - require(epochDays in Int.MIN_VALUE..Int.MAX_VALUE) { - "Invalid date: boundaries of LocalDate exceeded" - } - return fromEpochDays(epochDays.toInt()) - } + internal actual fun fromEpochDays(epochDays: Long): LocalDate = + fromEpochDays(epochDays.clampToInt()) @Suppress("FunctionName") public actual fun Format(block: DateTimeFormatBuilder.WithDate.() -> Unit): DateTimeFormat = From 63a4c42663d24aa82fa2fbccc243cee38f7506b3 Mon Sep 17 00:00:00 2001 From: PeterAttardo Date: Mon, 7 Oct 2024 10:46:06 -0400 Subject: [PATCH 33/57] Update core/commonKotlin/src/LocalDate.kt Co-authored-by: Dmitry Khalanskiy <52952525+dkhalanskyjb@users.noreply.github.com> --- core/commonKotlin/src/LocalDate.kt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/core/commonKotlin/src/LocalDate.kt b/core/commonKotlin/src/LocalDate.kt index 653cc5a4d..26fdbaa4a 100644 --- a/core/commonKotlin/src/LocalDate.kt +++ b/core/commonKotlin/src/LocalDate.kt @@ -82,12 +82,8 @@ public actual class LocalDate actual constructor(public actual val year: Int, pu return LocalDate(yearEst, month, dom) } - public actual fun fromEpochDays(epochDays: Long): LocalDate { - require(epochDays in MIN_EPOCH_DAY..MAX_EPOCH_DAY) { - "Invalid date: boundaries of LocalDate exceeded" - } - return fromEpochDays(epochDays.toInt()) - } + internal actual fun fromEpochDays(epochDays: Long): LocalDate = + fromEpochDays(epochDays.clampToInt()) internal actual val MIN = LocalDate(YEAR_MIN, 1, 1) internal actual val MAX = LocalDate(YEAR_MAX, 12, 31) From 614038dc5c0c20fbf6d5dd4406af77a351c9ce46 Mon Sep 17 00:00:00 2001 From: PeterAttardo Date: Tue, 15 Oct 2024 11:49:20 -0400 Subject: [PATCH 34/57] Update core/common/test/LocalDateRangeTest.kt Co-authored-by: Dmitry Khalanskiy <52952525+dkhalanskyjb@users.noreply.github.com> --- core/common/test/LocalDateRangeTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/common/test/LocalDateRangeTest.kt b/core/common/test/LocalDateRangeTest.kt index 3b4fc1988..dbddff95b 100644 --- a/core/common/test/LocalDateRangeTest.kt +++ b/core/common/test/LocalDateRangeTest.kt @@ -120,7 +120,7 @@ class LocalDateRangeTest { (Jan_01_2000 downTo Jan_01_2000).random() ) - assertFails { + assertFailsWith { (Jan_02_2000..Jan_01_2000).random() } From b5db45c500c99a6e2f2b1722adab5f56815f19d4 Mon Sep 17 00:00:00 2001 From: PeterAttardo Date: Tue, 15 Oct 2024 11:49:49 -0400 Subject: [PATCH 35/57] Update core/common/test/LocalDateRangeTest.kt Co-authored-by: Dmitry Khalanskiy <52952525+dkhalanskyjb@users.noreply.github.com> --- core/common/test/LocalDateRangeTest.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/core/common/test/LocalDateRangeTest.kt b/core/common/test/LocalDateRangeTest.kt index dbddff95b..8e0cf7c6e 100644 --- a/core/common/test/LocalDateRangeTest.kt +++ b/core/common/test/LocalDateRangeTest.kt @@ -162,9 +162,7 @@ class LocalDateRangeTest { } } repeat(20) { - assertTrue { - (Jan_01_2000..Jan_24_2000).step(5, DateTimeUnit.DAY).let { it.contains(it.random()) } - } + (Jan_01_2000..Jan_24_2000).step(5, DateTimeUnit.DAY).let { assertContains(it, it.random()) } } } From cc9d7a1f0f63c45b2ead8f1fa49b9d943996ad51 Mon Sep 17 00:00:00 2001 From: PeterAttardo Date: Tue, 15 Oct 2024 11:50:02 -0400 Subject: [PATCH 36/57] Update core/common/test/LocalDateRangeTest.kt Co-authored-by: Dmitry Khalanskiy <52952525+dkhalanskyjb@users.noreply.github.com> --- core/common/test/LocalDateRangeTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/common/test/LocalDateRangeTest.kt b/core/common/test/LocalDateRangeTest.kt index 8e0cf7c6e..381054534 100644 --- a/core/common/test/LocalDateRangeTest.kt +++ b/core/common/test/LocalDateRangeTest.kt @@ -172,8 +172,8 @@ class LocalDateRangeTest { assertEquals((Jan_01_2000 downTo Jan_01_2000).first(), Jan_01_2000) assertEquals((Jan_01_2000..Jan_05_2000).first(), Jan_01_2000) assertEquals((Jan_05_2000 downTo Jan_01_2000).first(), Jan_05_2000) - assertFails { (Jan_02_2000..Jan_01_2000).first() } - assertFails { (Jan_01_2000 downTo Jan_02_2000).first() } + assertFailsWith { (Jan_02_2000..Jan_01_2000).first() } + assertFailsWith { (Jan_01_2000 downTo Jan_02_2000).first() } } @Test From a4a2ead5d315823d75f07ef0259a5ee0bd0f74f7 Mon Sep 17 00:00:00 2001 From: PeterAttardo Date: Tue, 15 Oct 2024 11:50:21 -0400 Subject: [PATCH 37/57] Update core/common/test/LocalDateRangeTest.kt Co-authored-by: Dmitry Khalanskiy <52952525+dkhalanskyjb@users.noreply.github.com> --- core/common/test/LocalDateRangeTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/common/test/LocalDateRangeTest.kt b/core/common/test/LocalDateRangeTest.kt index 381054534..ce475a179 100644 --- a/core/common/test/LocalDateRangeTest.kt +++ b/core/common/test/LocalDateRangeTest.kt @@ -182,8 +182,8 @@ class LocalDateRangeTest { assertEquals((Jan_01_2000 downTo Jan_01_2000).last(), Jan_01_2000) assertEquals((Jan_01_2000..Jan_05_2000).last(), Jan_05_2000) assertEquals((Jan_05_2000 downTo Jan_01_2000).last(), Jan_01_2000) - assertFails { (Jan_02_2000..Jan_01_2000).last() } - assertFails { (Jan_01_2000 downTo Jan_02_2000).last() } + assertFailsWith { (Jan_02_2000..Jan_01_2000).last() } + assertFailsWith { (Jan_01_2000 downTo Jan_02_2000).last() } } @Test From 9ba1ac105299c447df5927b3aa9fc31c2748847e Mon Sep 17 00:00:00 2001 From: PeterAttardo Date: Tue, 15 Oct 2024 12:00:17 -0400 Subject: [PATCH 38/57] Update core/common/src/LocalDateRange.kt Co-authored-by: Dmitry Khalanskiy <52952525+dkhalanskyjb@users.noreply.github.com> --- core/common/src/LocalDateRange.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/common/src/LocalDateRange.kt b/core/common/src/LocalDateRange.kt index 4fc770b29..85927e8d1 100644 --- a/core/common/src/LocalDateRange.kt +++ b/core/common/src/LocalDateRange.kt @@ -108,7 +108,7 @@ public class LocalDateRange(start: LocalDate, endInclusive: LocalDate) : LocalDa override val endInclusive: LocalDate get() = last /** - * returns the upper bound of the range, exclusive. + * Returns the upper bound of the range, exclusive. */ @Deprecated( "This throws an exception if the exclusive end if not inside " + From 53f62272ddba96e7b1d7192f9b1f070966fc3268 Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Tue, 15 Oct 2024 12:02:25 -0400 Subject: [PATCH 39/57] Fix LocalDateRange test --- core/common/test/LocalDateRangeTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/common/test/LocalDateRangeTest.kt b/core/common/test/LocalDateRangeTest.kt index ce475a179..2d268cc3c 100644 --- a/core/common/test/LocalDateRangeTest.kt +++ b/core/common/test/LocalDateRangeTest.kt @@ -38,6 +38,8 @@ class LocalDateRangeTest { ) assertTrue { (LocalDate.MIN.. Date: Tue, 15 Oct 2024 14:03:35 -0400 Subject: [PATCH 40/57] Add tests to last() for LocalDateRangeTest --- core/common/test/LocalDateRangeTest.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/common/test/LocalDateRangeTest.kt b/core/common/test/LocalDateRangeTest.kt index 2d268cc3c..90e827f16 100644 --- a/core/common/test/LocalDateRangeTest.kt +++ b/core/common/test/LocalDateRangeTest.kt @@ -16,6 +16,7 @@ class LocalDateRangeTest { val Jan_01_2000 = LocalDate(2000, 1, 1) val Jan_02_2000 = LocalDate(2000, 1, 2) val Jan_05_2000 = LocalDate(2000, 1, 5) + val Jan_06_2000 = LocalDate(2000, 1, 6) val Jan_24_2000 = LocalDate(2000, 1, 24) val Dec_24_2000 = LocalDate(2000, 12, 24) @@ -184,6 +185,8 @@ class LocalDateRangeTest { assertEquals((Jan_01_2000 downTo Jan_01_2000).last(), Jan_01_2000) assertEquals((Jan_01_2000..Jan_05_2000).last(), Jan_05_2000) assertEquals((Jan_05_2000 downTo Jan_01_2000).last(), Jan_01_2000) + assertEquals((Jan_01_2000..Jan_06_2000).step(2, DateTimeUnit.DAY).last(), Jan_05_2000) + assertEquals((Jan_06_2000 downTo Jan_01_2000).step(2, DateTimeUnit.DAY).last(), Jan_02_2000) assertFailsWith { (Jan_02_2000..Jan_01_2000).last() } assertFailsWith { (Jan_01_2000 downTo Jan_02_2000).last() } } From 89101cbe22625080d15c503694ce3e05095ae078 Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Tue, 15 Oct 2024 14:07:47 -0400 Subject: [PATCH 41/57] Add documentation to LocalDateRange.EMPTY --- core/common/src/LocalDateRange.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/common/src/LocalDateRange.kt b/core/common/src/LocalDateRange.kt index 85927e8d1..219eb2d08 100644 --- a/core/common/src/LocalDateRange.kt +++ b/core/common/src/LocalDateRange.kt @@ -139,6 +139,7 @@ public class LocalDateRange(start: LocalDate, endInclusive: LocalDate) : LocalDa override fun toString(): String = "$first..$last" public companion object { + /** An empty range of values of type LocalDate. */ public val EMPTY: LocalDateRange = LocalDateRange(LocalDate(1970, 1, 2), LocalDate(1970, 1, 1)) internal fun fromRangeUntil(start: LocalDate, endExclusive: LocalDate) : LocalDateRange { From fef8e59c18f499b4c9242123599f3522face1ca8 Mon Sep 17 00:00:00 2001 From: PeterAttardo Date: Wed, 16 Oct 2024 16:29:57 -0400 Subject: [PATCH 42/57] Update core/common/src/LocalDateRange.kt Co-authored-by: Dmitry Khalanskiy <52952525+dkhalanskyjb@users.noreply.github.com> --- core/common/src/LocalDateRange.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/common/src/LocalDateRange.kt b/core/common/src/LocalDateRange.kt index 219eb2d08..4620985a7 100644 --- a/core/common/src/LocalDateRange.kt +++ b/core/common/src/LocalDateRange.kt @@ -256,7 +256,9 @@ internal fun LongProgression.randomOrNull(random: Random = Random) : Long? = if internal fun LongProgression.contains(element: Long) : Boolean = element in (if(step > 0) first..last else last..first) && (element - first) % step == 0L -internal val LongProgression.size: Int +// this implementation is incorrect in general (for example, `Long.MIN_VALUE..Long.MAX_VALUE` has size == 0), +// but for the range of epoch days in LocalDate it's good enough +private val LongProgression.size: Int get() = if(isEmpty()) 0 else try { (safeAdd(last, -first) / step + 1).clampToInt() } catch (e: ArithmeticException) { From 5e972177170b2d84e1c5ee44ad91af37333add2b Mon Sep 17 00:00:00 2001 From: PeterAttardo Date: Wed, 16 Oct 2024 16:30:17 -0400 Subject: [PATCH 43/57] Update core/common/src/LocalDateRange.kt Co-authored-by: Dmitry Khalanskiy <52952525+dkhalanskyjb@users.noreply.github.com> --- core/common/src/LocalDateRange.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/common/src/LocalDateRange.kt b/core/common/src/LocalDateRange.kt index 4620985a7..5463691a4 100644 --- a/core/common/src/LocalDateRange.kt +++ b/core/common/src/LocalDateRange.kt @@ -250,7 +250,9 @@ public fun LocalDateProgression.random(random: Random = Random) : LocalDate = lo public fun LocalDateProgression.randomOrNull(random: Random = Random) : LocalDate? = longProgression.randomOrNull(random) ?.let(LocalDate.Companion::fromEpochDays) -internal fun LongProgression.random(random: Random = Random) : Long = random.nextLong(0L..(last - first) / step) * step + first +// this implementation is incorrect in general (for example, `(Long.MIN_VALUE..Long.MAX_VALUE).random()` throws an exception), +// but for the range of epoch days in LocalDate it's good enough +private fun LongProgression.random(random: Random = Random) : Long = random.nextLong(0L..(last - first) / step) * step + first internal fun LongProgression.randomOrNull(random: Random = Random) : Long? = if (isEmpty()) null else random(random) From 1f2ac1838b7a2852adb39255ce14dea10ac6fb0d Mon Sep 17 00:00:00 2001 From: PeterAttardo Date: Wed, 16 Oct 2024 16:30:38 -0400 Subject: [PATCH 44/57] Update core/common/src/LocalDateRange.kt Co-authored-by: Dmitry Khalanskiy <52952525+dkhalanskyjb@users.noreply.github.com> --- core/common/src/LocalDateRange.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/common/src/LocalDateRange.kt b/core/common/src/LocalDateRange.kt index 5463691a4..e756047e7 100644 --- a/core/common/src/LocalDateRange.kt +++ b/core/common/src/LocalDateRange.kt @@ -254,7 +254,8 @@ public fun LocalDateProgression.randomOrNull(random: Random = Random) : LocalDat // but for the range of epoch days in LocalDate it's good enough private fun LongProgression.random(random: Random = Random) : Long = random.nextLong(0L..(last - first) / step) * step + first -internal fun LongProgression.randomOrNull(random: Random = Random) : Long? = if (isEmpty()) null else random(random) +// incorrect in general; see `random` just above +private fun LongProgression.randomOrNull(random: Random = Random) : Long? = if (isEmpty()) null else random(random) internal fun LongProgression.contains(element: Long) : Boolean = element in (if(step > 0) first..last else last..first) && (element - first) % step == 0L From 37a213e994c36831ffeb83d379cb7ddecda81b90 Mon Sep 17 00:00:00 2001 From: PeterAttardo Date: Wed, 16 Oct 2024 16:31:07 -0400 Subject: [PATCH 45/57] Update core/common/src/LocalDateRange.kt Co-authored-by: Dmitry Khalanskiy <52952525+dkhalanskyjb@users.noreply.github.com> --- core/common/src/LocalDateRange.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/common/src/LocalDateRange.kt b/core/common/src/LocalDateRange.kt index e756047e7..983790609 100644 --- a/core/common/src/LocalDateRange.kt +++ b/core/common/src/LocalDateRange.kt @@ -257,7 +257,10 @@ private fun LongProgression.random(random: Random = Random) : Long = random.next // incorrect in general; see `random` just above private fun LongProgression.randomOrNull(random: Random = Random) : Long? = if (isEmpty()) null else random(random) -internal fun LongProgression.contains(element: Long) : Boolean = element in (if(step > 0) first..last else last..first) && (element - first) % step == 0L +// this implementation is incorrect in general (for example, `(Long.MIN_VALUE..Long.MAX_VALUE).step(5).contains(2)` returns `false` +// incorrectly https://www.wolframalpha.com/input?i=-2%5E63+%2B+1844674407370955162+*+5), +// but for the range of epoch days in LocalDate it's good enough +private fun LongProgression.contains(element: Long) : Boolean = element in (if(step > 0) first..last else last..first) && (element - first) % step == 0L // this implementation is incorrect in general (for example, `Long.MIN_VALUE..Long.MAX_VALUE` has size == 0), // but for the range of epoch days in LocalDate it's good enough From 28e1806ec712732ebfc06d90f12c2a6de5c248a4 Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Wed, 16 Oct 2024 16:32:30 -0400 Subject: [PATCH 46/57] Word choice in comments --- core/common/src/LocalDateRange.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/common/src/LocalDateRange.kt b/core/common/src/LocalDateRange.kt index 983790609..fae59b80d 100644 --- a/core/common/src/LocalDateRange.kt +++ b/core/common/src/LocalDateRange.kt @@ -194,7 +194,7 @@ public fun LocalDateProgression.lastOrNull(): LocalDate? = if (isEmpty()) null e /** * Returns a reversed [LocalDateProgression], i.e. one that goes from [last] to [first]. - * The sign of the step is switched, in order to reverse the directionality of the progression. + * The sign of the step is switched, in order to reverse the direction of the progression. * * @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.reversedProgression */ @@ -204,7 +204,7 @@ public fun LocalDateProgression.reversed(): LocalDateProgression = LocalDateProg * Returns a [LocalDateProgression] with the same start and end, but a changed step value. * * **Pitfall**: the value parameter represents the magnitude of the step, not the direction, and therefore must be positive. - * Its sign will be matched to the sign of the existing step, in order to maintain the directionality of the progression. + * Its sign will be matched to the sign of the existing step, in order to maintain the direction of the progression. * If you wish to switch the direction of the progression, use [LocalDateProgression.reversed] * * @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.progressionWithStep @@ -215,7 +215,7 @@ public fun LocalDateProgression.step(value: Int, unit: DateTimeUnit.DayBased) : * Returns a [LocalDateProgression] with the same start and end, but a changed step value. * * **Pitfall**: the value parameter represents the magnitude of the step, not the direction, and therefore must be positive. - * Its sign will be matched to the sign of the existing step, in order to maintain the directionality of the progression. + * Its sign will be matched to the sign of the existing step, in order to maintain the direction of the progression. * If you wish to switch the direction of the progression, use [LocalDateProgression.reversed] * * @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.progressionWithStep From d161989b9b5a9b008fba6ce28cb6cff109664e4e Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Wed, 16 Oct 2024 17:23:07 -0400 Subject: [PATCH 47/57] Add safeMultiplyOrClamp --- core/common/src/internal/math.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/core/common/src/internal/math.kt b/core/common/src/internal/math.kt index d8aaf52f3..ad8d2cc95 100644 --- a/core/common/src/internal/math.kt +++ b/core/common/src/internal/math.kt @@ -5,6 +5,8 @@ package kotlinx.datetime.internal +import kotlin.math.sign + internal fun Long.clampToInt(): Int = when { this > Int.MAX_VALUE -> Int.MAX_VALUE @@ -17,6 +19,23 @@ internal expect fun safeMultiply(a: Int, b: Int): Int internal expect fun safeAdd(a: Long, b: Long): Long internal expect fun safeAdd(a: Int, b: Int): Int +internal fun safeMultiplyOrClamp(a: Long, b: Long): Long { + when (b) { + -1L -> { + if (a == Long.MIN_VALUE) { + return Long.MAX_VALUE + } + return -a + } + 1L -> return a + } + val total = a * b + if (total / b != a) { + return if (a.sign == b.sign) Long.MAX_VALUE else Long.MIN_VALUE + } + return total +} + /** Multiplies two non-zero long values. */ internal fun safeMultiplyOrZero(a: Long, b: Long): Long { when (b) { From dbac356ba9ac27357e843c0287990fab60463160 Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Wed, 16 Oct 2024 17:23:17 -0400 Subject: [PATCH 48/57] Clamp steps that overflow Long --- core/common/src/LocalDateRange.kt | 6 +++--- core/common/test/LocalDateRangeTest.kt | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/core/common/src/LocalDateRange.kt b/core/common/src/LocalDateRange.kt index fae59b80d..6272aa637 100644 --- a/core/common/src/LocalDateRange.kt +++ b/core/common/src/LocalDateRange.kt @@ -7,7 +7,7 @@ package kotlinx.datetime import kotlinx.datetime.internal.clampToInt import kotlinx.datetime.internal.safeAdd -import kotlinx.datetime.internal.safeMultiply +import kotlinx.datetime.internal.safeMultiplyOrClamp import kotlin.random.Random import kotlin.random.nextLong @@ -87,7 +87,7 @@ internal constructor(internal val longProgression: LongProgression) : Collection rangeEnd: LocalDate, stepValue: Long, stepUnit: DateTimeUnit.DayBased - ): LocalDateProgression = LocalDateProgression(rangeStart, rangeEnd, safeMultiply(stepValue, stepUnit.days.toLong())) + ): LocalDateProgression = LocalDateProgression(rangeStart, rangeEnd, safeMultiplyOrClamp(stepValue, stepUnit.days.toLong())) } } @@ -220,7 +220,7 @@ public fun LocalDateProgression.step(value: Int, unit: DateTimeUnit.DayBased) : * * @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.progressionWithStep */ -public fun LocalDateProgression.step(value: Long, unit: DateTimeUnit.DayBased) : LocalDateProgression = LocalDateProgression(longProgression.step(safeMultiply(value, unit.days.toLong()))) +public fun LocalDateProgression.step(value: Long, unit: DateTimeUnit.DayBased) : LocalDateProgression = LocalDateProgression(longProgression.step(safeMultiplyOrClamp(value, unit.days.toLong()))) /** * Creates a [LocalDateProgression] from `this` down to [that], inclusive. diff --git a/core/common/test/LocalDateRangeTest.kt b/core/common/test/LocalDateRangeTest.kt index 90e827f16..24e535740 100644 --- a/core/common/test/LocalDateRangeTest.kt +++ b/core/common/test/LocalDateRangeTest.kt @@ -89,6 +89,10 @@ class LocalDateRangeTest { (24 downTo 1 step 2).map { LocalDate(2000, 1, it) }, (Jan_24_2000 downTo Jan_01_2000).step(2, DateTimeUnit.DAY) ) + assertContentEquals( + (Jan_01_2000..Jan_24_2000).step(Long.MAX_VALUE / 2, DateTimeUnit.WEEK), + listOf(Jan_01_2000) + ) } @Test From 56c042b2b12e74805dfe07eee4e8f1fddc6ccdcb Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Wed, 16 Oct 2024 17:24:12 -0400 Subject: [PATCH 49/57] Unused variables --- core/common/test/LocalDateRangeTest.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/common/test/LocalDateRangeTest.kt b/core/common/test/LocalDateRangeTest.kt index 24e535740..389897d58 100644 --- a/core/common/test/LocalDateRangeTest.kt +++ b/core/common/test/LocalDateRangeTest.kt @@ -11,14 +11,12 @@ import kotlin.random.nextLong import kotlin.test.* class LocalDateRangeTest { - val Dec_24_1900 = LocalDate(1900, 12, 24) val Dec_30_1999 = LocalDate(1999, 12, 30) val Jan_01_2000 = LocalDate(2000, 1, 1) val Jan_02_2000 = LocalDate(2000, 1, 2) val Jan_05_2000 = LocalDate(2000, 1, 5) val Jan_06_2000 = LocalDate(2000, 1, 6) val Jan_24_2000 = LocalDate(2000, 1, 24) - val Dec_24_2000 = LocalDate(2000, 12, 24) @Test fun emptyRange() { From 163f5853d6819852ac99dfb386c49f502550911d Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Thu, 17 Oct 2024 10:18:42 -0400 Subject: [PATCH 50/57] Import clampToInt for js --- core/commonJs/src/LocalDate.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/commonJs/src/LocalDate.kt b/core/commonJs/src/LocalDate.kt index 946405431..d18aa6b95 100644 --- a/core/commonJs/src/LocalDate.kt +++ b/core/commonJs/src/LocalDate.kt @@ -6,6 +6,7 @@ package kotlinx.datetime import kotlinx.datetime.format.* +import kotlinx.datetime.internal.clampToInt import kotlinx.datetime.serializers.LocalDateIso8601Serializer import kotlinx.serialization.Serializable import kotlinx.datetime.internal.JSJoda.LocalDate as jtLocalDate From 405f034f3557603e3cca652e42dbb334242d138d Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Thu, 17 Oct 2024 12:39:36 -0400 Subject: [PATCH 51/57] Fix getSize test in LocalDateRangeTest --- core/common/test/LocalDateRangeTest.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/common/test/LocalDateRangeTest.kt b/core/common/test/LocalDateRangeTest.kt index 389897d58..df1ad4bc6 100644 --- a/core/common/test/LocalDateRangeTest.kt +++ b/core/common/test/LocalDateRangeTest.kt @@ -6,6 +6,7 @@ package kotlinx.datetime.test import kotlinx.datetime.* +import kotlinx.datetime.internal.clampToInt import kotlin.random.Random import kotlin.random.nextLong import kotlin.test.* @@ -266,8 +267,11 @@ class LocalDateRangeTest { assertEquals(0, (Jan_02_2000..Jan_01_2000).size) assertEquals (0, (Jan_01_2000 downTo Jan_02_2000).size) - assertEquals(Int.MAX_VALUE, (LocalDate.MIN..LocalDate.MAX).size) - assertEquals(Int.MAX_VALUE, (LocalDate.MAX downTo LocalDate.MIN).size) + + val maxSizeOfRange = (LocalDate.MAX.toEpochDaysLong() - LocalDate.MIN.toEpochDaysLong() + 1L).clampToInt() + assertEquals(maxSizeOfRange, (LocalDate.MIN..LocalDate.MAX).size) + assertEquals(maxSizeOfRange, (LocalDate.MAX downTo LocalDate.MIN).size) + assertEquals(1, (Jan_01_2000..Jan_02_2000).step(2, DateTimeUnit.DAY).size) assertEquals(3, (Jan_01_2000..Jan_05_2000).step(2, DateTimeUnit.DAY).size) From 179478e2496ba63e15b332503c4d805398749e89 Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Tue, 22 Oct 2024 11:07:42 -0400 Subject: [PATCH 52/57] Make LocalDateRangeTest stricter --- core/common/test/LocalDateRangeTest.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/core/common/test/LocalDateRangeTest.kt b/core/common/test/LocalDateRangeTest.kt index df1ad4bc6..1794ebc7b 100644 --- a/core/common/test/LocalDateRangeTest.kt +++ b/core/common/test/LocalDateRangeTest.kt @@ -216,19 +216,18 @@ class LocalDateRangeTest { @Test fun reversed() { - assertContentEquals( + assertEquals( Jan_05_2000 downTo Jan_01_2000, (Jan_01_2000..Jan_05_2000).reversed() ) - assertContentEquals( + assertEquals( Jan_01_2000..Jan_05_2000, (Jan_05_2000 downTo Jan_01_2000).reversed() ) - assertContentEquals( - Jan_01_2000..Jan_01_2000, + assertEquals( + Jan_01_2000 downTo Jan_01_2000, (Jan_01_2000..Jan_01_2000).reversed() ) - } @Test From b16b32971dd42d0277eee84c2bcc4e2dfd3f631d Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Tue, 22 Oct 2024 11:07:57 -0400 Subject: [PATCH 53/57] Add equals and hashcode tests to LocalDateRangeTest --- core/common/test/LocalDateRangeTest.kt | 30 ++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/core/common/test/LocalDateRangeTest.kt b/core/common/test/LocalDateRangeTest.kt index 1794ebc7b..a0b21461b 100644 --- a/core/common/test/LocalDateRangeTest.kt +++ b/core/common/test/LocalDateRangeTest.kt @@ -279,4 +279,34 @@ class LocalDateRangeTest { assertEquals(3, (Jan_05_2000 downTo Jan_01_2000).step(2, DateTimeUnit.DAY).size) assertEquals(2, (Jan_05_2000 downTo Jan_02_2000).step(2, DateTimeUnit.DAY).size) } + + @Test + fun equalityAndHashCode() { + assertEquals( + Jan_01_2000..Jan_05_2000, + (Jan_01_2000..Jan_05_2000).step(1, DateTimeUnit.DAY) + ) + assertEquals( + (Jan_01_2000..Jan_05_2000).hashCode(), + ((Jan_01_2000..Jan_05_2000).step(1, DateTimeUnit.DAY)).hashCode() + ) + + assertEquals( + (Jan_01_2000..Jan_05_2000).step(7, DateTimeUnit.DAY), + (Jan_01_2000..Jan_05_2000).step(1, DateTimeUnit.WEEK) + ) + assertEquals( + (Jan_01_2000..Jan_05_2000).step(7, DateTimeUnit.DAY).hashCode(), + (Jan_01_2000..Jan_05_2000).step(1, DateTimeUnit.WEEK).hashCode() + ) + + assertEquals( + (Jan_05_2000 downTo Jan_01_2000).step(7, DateTimeUnit.DAY), + (Jan_05_2000 downTo Jan_01_2000).step(1, DateTimeUnit.WEEK) + ) + assertEquals( + (Jan_05_2000 downTo Jan_01_2000).step(7, DateTimeUnit.DAY).hashCode(), + (Jan_05_2000 downTo Jan_01_2000).step(1, DateTimeUnit.WEEK).hashCode() + ) + } } From fe39e5cfc9a61aeb13c4d7987da82a9feaf995fe Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Fri, 1 Nov 2024 10:30:57 -0400 Subject: [PATCH 54/57] Fix whitespace --- core/common/test/samples/LocalDateRangeSamples.kt | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/core/common/test/samples/LocalDateRangeSamples.kt b/core/common/test/samples/LocalDateRangeSamples.kt index 7f0255466..cdd2457a1 100644 --- a/core/common/test/samples/LocalDateRangeSamples.kt +++ b/core/common/test/samples/LocalDateRangeSamples.kt @@ -92,13 +92,7 @@ class LocalDateRangeSamples { fun random() { // Getting a random element from a LocalDateProgression check((LocalDate(2000, 1, 1)..LocalDate(2000, 1, 5)).random() in LocalDate(2000, 1, 1)..LocalDate(2000, 1, 5)) - check( - (LocalDate(2000, 1, 1)..LocalDate(2000, 1, 5)).random(Random(123456)) in LocalDate(2000, 1, 1)..LocalDate( - 2000, - 1, - 5 - ) - ) + check((LocalDate(2000, 1, 1)..LocalDate(2000, 1, 5)).random(Random(123456)) in LocalDate(2000, 1, 1)..LocalDate(2000, 1, 5)) check((LocalDate(2000, 1, 5)..LocalDate(2000, 1, 1)).randomOrNull() == null) check((LocalDate(2000, 1, 5)..LocalDate(2000, 1, 1)).randomOrNull(Random(123456)) == null) } From 6f6732787e87d3cd0c5db2a5f943e4a90f390fd2 Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Fri, 1 Nov 2024 10:31:24 -0400 Subject: [PATCH 55/57] Unify parameter name for LocalDateRange.contains() and LocalDateProgression.contains() --- core/common/src/LocalDateRange.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/common/src/LocalDateRange.kt b/core/common/src/LocalDateRange.kt index 6272aa637..d0a566985 100644 --- a/core/common/src/LocalDateRange.kt +++ b/core/common/src/LocalDateRange.kt @@ -73,9 +73,9 @@ internal constructor(internal val longProgression: LongProgression) : Collection override fun containsAll(elements: Collection): Boolean = elements.all(::contains) /** - * Returns true iff [element] is a member of the progression. + * Returns true iff [value] is a member of the progression. */ - override fun contains(element: LocalDate): Boolean = longProgression.contains(element.toEpochDaysLong()) + override fun contains(value: LocalDate): Boolean = longProgression.contains(value.toEpochDaysLong()) override fun equals(other: Any?): Boolean = other is LocalDateProgression && longProgression == other.longProgression @@ -260,7 +260,7 @@ private fun LongProgression.randomOrNull(random: Random = Random) : Long? = if ( // this implementation is incorrect in general (for example, `(Long.MIN_VALUE..Long.MAX_VALUE).step(5).contains(2)` returns `false` // incorrectly https://www.wolframalpha.com/input?i=-2%5E63+%2B+1844674407370955162+*+5), // but for the range of epoch days in LocalDate it's good enough -private fun LongProgression.contains(element: Long) : Boolean = element in (if(step > 0) first..last else last..first) && (element - first) % step == 0L +private fun LongProgression.contains(value: Long) : Boolean = value in (if(step > 0) first..last else last..first) && (value - first) % step == 0L // this implementation is incorrect in general (for example, `Long.MIN_VALUE..Long.MAX_VALUE` has size == 0), // but for the range of epoch days in LocalDate it's good enough From b6d56ce14c46265748b3312d36513e184eabf3fb Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Fri, 1 Nov 2024 12:20:22 -0400 Subject: [PATCH 56/57] Add type checks for unexpected variance in LocalDateProgression --- core/common/src/LocalDateRange.kt | 17 ++++++++++++++--- core/common/test/LocalDateRangeTest.kt | 4 ++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/core/common/src/LocalDateRange.kt b/core/common/src/LocalDateRange.kt index d0a566985..05b49e4a7 100644 --- a/core/common/src/LocalDateRange.kt +++ b/core/common/src/LocalDateRange.kt @@ -70,12 +70,18 @@ internal constructor(internal val longProgression: LongProgression) : Collection /** * Returns true iff every element in [elements] is a member of the progression. */ - override fun containsAll(elements: Collection): Boolean = elements.all(::contains) + override fun containsAll(elements: Collection): Boolean = + (elements as Collection<*>).all { it is LocalDate && contains(it) } /** * Returns true iff [value] is a member of the progression. */ - override fun contains(value: LocalDate): Boolean = longProgression.contains(value.toEpochDaysLong()) + override fun contains(value: LocalDate): Boolean { + @Suppress("USELESS_CAST") + if ((value as Any?) !is LocalDate) return false + + return longProgression.contains(value.toEpochDaysLong()) + } override fun equals(other: Any?): Boolean = other is LocalDateProgression && longProgression == other.longProgression @@ -126,7 +132,12 @@ public class LocalDateRange(start: LocalDate, endInclusive: LocalDate) : LocalDa * i.e. [value] is between [start] and [endInclusive]. */ @Suppress("ConvertTwoComparisonsToRangeCheck") - override fun contains(value: LocalDate): Boolean = first <= value && value <= last + override fun contains(value: LocalDate): Boolean { + @Suppress("USELESS_CAST") + if ((value as Any?) !is LocalDate) return false + + return first <= value && value <= last + } /** * Returns true iff there are no dates contained within the range. diff --git a/core/common/test/LocalDateRangeTest.kt b/core/common/test/LocalDateRangeTest.kt index a0b21461b..41a62d3b3 100644 --- a/core/common/test/LocalDateRangeTest.kt +++ b/core/common/test/LocalDateRangeTest.kt @@ -246,12 +246,16 @@ class LocalDateRangeTest { assertFalse { Jan_01_2000 in Jan_02_2000 downTo Jan_05_2000 } assertFalse { Jan_24_2000 in Jan_05_2000 downTo Jan_02_2000 } + assertFalse { (Jan_01_2000..Jan_05_2000).contains(Any()) } + assertTrue { (Jan_01_2000..Jan_01_2000).containsAll(listOf(Jan_01_2000)) } assertTrue { (Jan_01_2000..Jan_05_2000).containsAll(listOf(Jan_01_2000, Jan_02_2000, Jan_05_2000)) } assertFalse { (Jan_01_2000..Jan_01_2000).containsAll(listOf(Jan_01_2000, Jan_02_2000)) } assertFalse { (Jan_01_2000..Jan_05_2000).containsAll(listOf(Jan_01_2000, Jan_02_2000, Jan_05_2000, Jan_24_2000)) } + assertFalse { ((Jan_01_2000..Jan_05_2000) as Collection<*>).containsAll(listOf(Any())) } + } @Test From c58c61f1c8c22a82b1b7273bc41bb6394c4b3516 Mon Sep 17 00:00:00 2001 From: Peter Attardo Date: Mon, 4 Nov 2024 10:46:09 -0500 Subject: [PATCH 57/57] Switch LocalDateRange.random() to throw NoSuchElementException instead of IllegalArgumentException --- core/common/src/LocalDateRange.kt | 4 +++- core/common/test/LocalDateRangeTest.kt | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core/common/src/LocalDateRange.kt b/core/common/src/LocalDateRange.kt index 05b49e4a7..50c953e29 100644 --- a/core/common/src/LocalDateRange.kt +++ b/core/common/src/LocalDateRange.kt @@ -249,7 +249,9 @@ public infix fun LocalDate.downTo(that: LocalDate) : LocalDateProgression = Loca * * @sample kotlinx.datetime.test.samples.LocalDateRangeSamples.random */ -public fun LocalDateProgression.random(random: Random = Random) : LocalDate = longProgression.random(random).let(LocalDate.Companion::fromEpochDays) +public fun LocalDateProgression.random(random: Random = Random): LocalDate = + if (isEmpty()) throw NoSuchElementException("Cannot get random in empty range: $this") + else longProgression.random(random).let(LocalDate.Companion::fromEpochDays) /** * Returns a random [LocalDate] within the bounds of the [LocalDateProgression] or null if the progression is empty. diff --git a/core/common/test/LocalDateRangeTest.kt b/core/common/test/LocalDateRangeTest.kt index 41a62d3b3..09edefe29 100644 --- a/core/common/test/LocalDateRangeTest.kt +++ b/core/common/test/LocalDateRangeTest.kt @@ -126,7 +126,7 @@ class LocalDateRangeTest { (Jan_01_2000 downTo Jan_01_2000).random() ) - assertFailsWith { + assertFailsWith { (Jan_02_2000..Jan_01_2000).random() }