Skip to content

Commit 740434f

Browse files
committed
Add LocalDateRange class and associated tests. Exists to enable Kotlin
range and progression semantics with the LocalDate and DatePeriod classes.
1 parent a962bde commit 740434f

File tree

2 files changed

+450
-0
lines changed

2 files changed

+450
-0
lines changed

core/common/src/LocalDateRange.kt

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright 2019-2022 JetBrains s.r.o.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package kotlinx.datetime
7+
8+
public abstract class LocalDateIterator : Iterator<LocalDate> {
9+
final override fun next(): LocalDate = nextLocalDate()
10+
public abstract fun nextLocalDate(): LocalDate
11+
}
12+
13+
internal class LocalDateProgressionIterator(first: LocalDate, last: LocalDate, val step: DatePeriod) : LocalDateIterator() {
14+
private val finalElement: LocalDate = last
15+
private val increasing = step.positive()
16+
private var hasNext: Boolean = if (increasing) first <= last else first >= last
17+
private var next: LocalDate = if (hasNext) first else finalElement
18+
19+
override fun hasNext(): Boolean = hasNext
20+
21+
override fun nextLocalDate(): LocalDate {
22+
if(!hasNext) throw NoSuchElementException()
23+
val value = next
24+
next += step
25+
/**
26+
* 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.
27+
* A period of P1M-28D for example, when added to any date in February, will return that same date, thus leading to a loop.
28+
*/
29+
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).")
30+
if ((increasing && next > finalElement) || (!increasing && next < finalElement)) {
31+
hasNext = false
32+
}
33+
return value
34+
}
35+
}
36+
37+
public open class LocalDateProgression
38+
internal constructor
39+
(
40+
start: LocalDate,
41+
endInclusive: LocalDate,
42+
public val step: DatePeriod
43+
) : Iterable<LocalDate> {
44+
init {
45+
if(!step.positive() && !step.negative()) throw IllegalArgumentException("Provided step DatePeriod is of size zero (or equivalent over an arbitrarily long timeline)")
46+
}
47+
public val first: LocalDate = start
48+
public val last: LocalDate = endInclusive
49+
50+
override fun iterator(): LocalDateIterator = LocalDateProgressionIterator(first, last, step)
51+
52+
public open fun isEmpty(): Boolean = if (step.positive()) first > last else first < last
53+
54+
55+
override fun toString(): String = if (step.positive()) "$first..$last step $step" else "$first downTo $last step $step"
56+
57+
override fun equals(other: Any?): Boolean {
58+
if (this === other) return true
59+
if (other == null || this::class != other::class) return false
60+
61+
other as LocalDateProgression
62+
63+
if (first != other.first) return false
64+
if (last != other.last) return false
65+
if (step != other.step) return false
66+
67+
return true
68+
}
69+
70+
override fun hashCode(): Int {
71+
var result = first.hashCode()
72+
result = 31 * result + last.hashCode()
73+
result = 31 * result + step.hashCode()
74+
return result
75+
}
76+
77+
public companion object {
78+
public fun fromClosedRange(rangeStart: LocalDate, rangeEnd: LocalDate, step: DatePeriod): LocalDateProgression = LocalDateProgression(rangeStart, rangeEnd, step)
79+
}
80+
}
81+
82+
public class LocalDateRange(start: LocalDate, endInclusive: LocalDate) : LocalDateProgression(start, endInclusive, DatePeriod(days = 1)), ClosedRange<LocalDate> {
83+
override val start: LocalDate get() = first
84+
override val endInclusive: LocalDate get() = last
85+
86+
@Suppress("ConvertTwoComparisonsToRangeCheck")
87+
override fun contains(value: LocalDate): Boolean = first <= value && value <= last
88+
89+
override fun isEmpty(): Boolean = first > last
90+
91+
override fun toString(): String = "$first..$last"
92+
93+
public companion object {
94+
private val DATE_ONE = LocalDate(1, 1, 1)
95+
private val DATE_TWO = LocalDate(1, 1, 2)
96+
public val EMPTY: LocalDateRange = LocalDateRange(DATE_TWO, DATE_ONE)
97+
}
98+
}
99+
100+
/**
101+
* On an arbitrarily long timeline, the average month will be 30.436875 days long (146097 days over 400 years).
102+
*/
103+
public fun DatePeriod.positive() : Boolean = totalMonths * 30.436875 + days > 0
104+
public fun DatePeriod.negative() : Boolean = totalMonths * 30.436875 + days < 0
105+
106+
public infix fun LocalDateProgression.step(step: DatePeriod) : LocalDateProgression = LocalDateProgression.fromClosedRange(first, last, step)
107+
public infix fun LocalDate.downTo(that: LocalDate) : LocalDateProgression = LocalDateProgression.fromClosedRange(this, that, DatePeriod(days = -1))
108+
109+
public operator fun LocalDate.rangeTo(that: LocalDate): LocalDateRange = LocalDateRange(this, that)

0 commit comments

Comments
 (0)