diff --git a/core/common/src/LocalDate.kt b/core/common/src/LocalDate.kt index 0146d6f9..97a1565f 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), @@ -257,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 new file mode 100644 index 00000000..50c953e2 --- /dev/null +++ b/core/common/src/LocalDateRange.kt @@ -0,0 +1,285 @@ +/* + * 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 + +import kotlinx.datetime.internal.clampToInt +import kotlinx.datetime.internal.safeAdd +import kotlinx.datetime.internal.safeMultiplyOrClamp +import kotlin.random.Random +import kotlin.random.nextLong + +private class LocalDateProgressionIterator(private val iterator: LongIterator) : Iterator { + override fun hasNext(): Boolean = iterator.hasNext() + 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 { + + internal constructor( + start: LocalDate, + endInclusive: LocalDate, + 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 as Collection<*>).all { it is LocalDate && contains(it) } + + /** + * Returns true iff [value] is a member of the progression. + */ + 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 + + override fun hashCode(): Int = longProgression.hashCode() + + public companion object { + internal fun fromClosedRange( + rangeStart: LocalDate, + rangeEnd: LocalDate, + stepValue: Long, + stepUnit: DateTimeUnit.DayBased + ): LocalDateProgression = LocalDateProgression(rangeStart, rangeEnd, safeMultiplyOrClamp(stepValue, stepUnit.days.toLong())) + } +} + +/** + * 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. " + + "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) + } + + /** + * 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 { + @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. + */ + 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 { + /** 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 { + return if(endExclusive == LocalDate.MIN) EMPTY else fromRangeTo(start, endExclusive.minus(1, DateTimeUnit.DAY)) + } + + internal fun fromRangeTo(start: LocalDate, endInclusive: LocalDate) : LocalDateRange { + return LocalDateRange(start, endInclusive) + } + } +} + +/** + * 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 direction 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 direction 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 direction 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(safeMultiplyOrClamp(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) + +/** + * 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 = + 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. + * + * 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) + +// 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 + +// incorrect in general; see `random` just above +private fun LongProgression.randomOrNull(random: Random = Random) : Long? = if (isEmpty()) null else random(random) + +// 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(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 +private 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/src/internal/math.kt b/core/common/src/internal/math.kt index d8aaf52f..ad8d2cc9 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) { diff --git a/core/common/test/LocalDateRangeTest.kt b/core/common/test/LocalDateRangeTest.kt new file mode 100644 index 00000000..09edefe2 --- /dev/null +++ b/core/common/test/LocalDateRangeTest.kt @@ -0,0 +1,316 @@ +/* + * 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 kotlinx.datetime.internal.clampToInt +import kotlin.random.Random +import kotlin.random.nextLong +import kotlin.test.* + +class LocalDateRangeTest { + 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) + + @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( + (1..<5).map { LocalDate(2000, 1, it) }, + Jan_01_2000.. { + (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.nextLong(0L..23L).let { Jan_01_2000.plus(it, DateTimeUnit.DAY) }, + (Jan_01_2000..Jan_24_2000).random(actualRand) + ) + } + + repeat(20) { + assertEquals( + expectedRand.nextLong(0L..23L).let { Jan_24_2000.minus(it, DateTimeUnit.DAY) }, + (Jan_24_2000 downTo Jan_01_2000).random(actualRand) + ) + } + + listOf(1L, 2L, 5L, 30L).forEach { step -> + repeat(20) { + val range = (0L..23L step step) + assertEquals( + 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 = (0L..23L step step) + assertEquals( + 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) + ) + } + } + repeat(20) { + (Jan_01_2000..Jan_24_2000).step(5, DateTimeUnit.DAY).let { assertContains(it, it.random()) } + } + } + + @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) + assertFailsWith { (Jan_02_2000..Jan_01_2000).first() } + assertFailsWith { (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) + 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() } + } + + @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() { + assertEquals( + Jan_05_2000 downTo Jan_01_2000, + (Jan_01_2000..Jan_05_2000).reversed() + ) + assertEquals( + Jan_01_2000..Jan_05_2000, + (Jan_05_2000 downTo Jan_01_2000).reversed() + ) + assertEquals( + Jan_01_2000 downTo 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 } + + 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 + 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.. Unit): DateTimeFormat = LocalDateFormat.build(block) @@ -82,6 +86,12 @@ 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() + + 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/commonKotlin/src/LocalDate.kt b/core/commonKotlin/src/LocalDate.kt index 14ee3a17..26fdbaa4 100644 --- a/core/commonKotlin/src/LocalDate.kt +++ b/core/commonKotlin/src/LocalDate.kt @@ -82,6 +82,9 @@ public actual class LocalDate actual constructor(public actual val year: Int, pu return LocalDate(yearEst, month, dom) } + 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) @@ -121,6 +124,8 @@ public actual class LocalDate actual constructor(public actual val year: Int, pu return total - DAYS_0000_TO_1970 } + internal actual fun toEpochDaysLong(): Long = toEpochDays().toLong() + public actual val month: Month get() = Month(monthNumber) @@ -195,6 +200,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)")) diff --git a/core/jvm/src/LocalDate.kt b/core/jvm/src/LocalDate.kt index 8d1bcc23..e7cba75d 100644 --- a/core/jvm/src/LocalDate.kt +++ b/core/jvm/src/LocalDate.kt @@ -38,7 +38,10 @@ 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)) @Suppress("FunctionName") public actual fun Format(block: DateTimeFormatBuilder.WithDate.() -> Unit): DateTimeFormat = @@ -77,6 +80,12 @@ 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() + + 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)"))