Skip to content

Commit 5448939

Browse files
committed
Introduce ranges of YearMonth
1 parent feebb44 commit 5448939

File tree

6 files changed

+744
-2
lines changed

6 files changed

+744
-2
lines changed

core/common/src/YearMonth.kt

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,20 @@ public constructor(year: Int, month: Int) : Comparable<YearMonth> {
183183
public val ISO: DateTimeFormat<YearMonth>
184184
}
185185

186+
/**
187+
* Creates a [YearMonthRange] from `this` to [that], inclusive.
188+
*
189+
* @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.simpleRangeCreation
190+
*/
191+
public operator fun rangeTo(that: YearMonth): YearMonthRange
192+
193+
/**
194+
* Creates a [YearMonthRange] from `this` to [that], exclusive, i.e., from this to (that - 1 month)
195+
*
196+
* @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.simpleRangeCreation
197+
*/
198+
public operator fun rangeUntil(that: YearMonth): YearMonthRange
199+
186200
/**
187201
* Compares `this` date with the [other] year-month.
188202
* Returns zero if this year-month represents the same month as the other (meaning they are equal to one other),
@@ -357,9 +371,9 @@ public fun YearMonth.plus(value: Long, unit: DateTimeUnit.MonthBased): YearMonth
357371
public fun YearMonth.minus(value: Long, unit: DateTimeUnit.MonthBased): YearMonth =
358372
if (value != Long.MIN_VALUE) plus(-value, unit) else plus(Long.MAX_VALUE, unit).plus(1, unit)
359373

360-
private val YearMonth.prolepticMonth: Long get() = year * 12L + (monthNumber - 1)
374+
internal val YearMonth.prolepticMonth: Long get() = year * 12L + (monthNumber - 1)
361375

362-
private fun YearMonth.Companion.fromProlepticMonth(prolepticMonth: Long): YearMonth {
376+
internal fun YearMonth.Companion.fromProlepticMonth(prolepticMonth: Long): YearMonth {
363377
val year = prolepticMonth.floorDiv(12)
364378
require(year in LocalDate.MIN.year..LocalDate.MAX.year) {
365379
"Year $year is out of range: ${LocalDate.MIN.year}..${LocalDate.MAX.year}"
@@ -368,3 +382,6 @@ private fun YearMonth.Companion.fromProlepticMonth(prolepticMonth: Long): YearMo
368382
println("proleptic month: ${prolepticMonth}, year: $year, month: $month")
369383
return YearMonth(year.toInt(), month)
370384
}
385+
386+
internal val YearMonth.Companion.MAX get() = LocalDate.MAX.yearMonth
387+
internal val YearMonth.Companion.MIN get() = LocalDate.MIN.yearMonth

core/common/src/YearMonthRange.kt

Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
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+
import kotlinx.datetime.internal.clampToInt
9+
import kotlinx.datetime.internal.safeAdd
10+
import kotlinx.datetime.internal.safeMultiplyOrClamp
11+
import kotlin.random.Random
12+
import kotlin.random.nextLong
13+
14+
private class YearMonthProgressionIterator(private val iterator: LongIterator) : Iterator<YearMonth> {
15+
override fun hasNext(): Boolean = iterator.hasNext()
16+
override fun next(): YearMonth = YearMonth.fromProlepticMonth(iterator.next())
17+
}
18+
19+
/**
20+
* A progression of values of type [YearMonth].
21+
*
22+
* @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.progressionWithStep
23+
* @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.reversedProgression
24+
* @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.firstAndLast
25+
*/
26+
public open class YearMonthProgression
27+
internal constructor(internal val longProgression: LongProgression) : Collection<YearMonth> {
28+
29+
internal constructor(
30+
start: YearMonth,
31+
endInclusive: YearMonth,
32+
step: Long
33+
) : this(LongProgression.fromClosedRange(start.prolepticMonth, endInclusive.prolepticMonth, step))
34+
35+
/**
36+
* Returns the first [YearMonth] of the progression
37+
*/
38+
public val first: YearMonth = YearMonth.fromProlepticMonth(longProgression.first)
39+
40+
/**
41+
* Returns the last [YearMonth] of the progression
42+
*/
43+
public val last: YearMonth = YearMonth.fromProlepticMonth(longProgression.last)
44+
45+
/**
46+
* Returns an [Iterator] that traverses the progression from [first] to [last]
47+
*/
48+
override fun iterator(): Iterator<YearMonth> = YearMonthProgressionIterator(longProgression.iterator())
49+
50+
/**
51+
* Returns true iff the progression contains no values.
52+
* i.e. [first] < [last] if step is positive, or [first] > [last] if step is negative.
53+
*/
54+
public override fun isEmpty(): Boolean = longProgression.isEmpty()
55+
56+
/**
57+
* Returns a string representation of the progression.
58+
* Uses the range operator notation if the progression is increasing, and `downTo` if it is decreasing.
59+
* The step is referenced in days.
60+
*/
61+
override fun toString(): String =
62+
if (longProgression.step > 0) "$first..$last step ${longProgression.step}M"
63+
else "$first downTo $last step ${longProgression.step}M"
64+
65+
/**
66+
* Returns the number of months in the progression.
67+
* Returns [Int.MAX_VALUE] if the number of months overflows [Int]
68+
*/
69+
override val size: Int
70+
get() = longProgression.size
71+
72+
/**
73+
* Returns true iff every element in [elements] is a member of the progression.
74+
*/
75+
override fun containsAll(elements: Collection<YearMonth>): Boolean =
76+
(elements as Collection<*>).all { it is YearMonth && contains(it) }
77+
78+
/**
79+
* Returns true iff [value] is a member of the progression.
80+
*/
81+
override fun contains(value: YearMonth): Boolean {
82+
@Suppress("USELESS_CAST")
83+
if ((value as Any?) !is YearMonth) return false
84+
85+
return longProgression.contains(value.prolepticMonth)
86+
}
87+
88+
override fun equals(other: Any?): Boolean =
89+
other is YearMonthProgression && longProgression == other.longProgression
90+
91+
override fun hashCode(): Int = longProgression.hashCode()
92+
93+
public companion object {
94+
internal fun fromClosedRange(
95+
rangeStart: YearMonth,
96+
rangeEnd: YearMonth,
97+
stepValue: Long,
98+
stepUnit: DateTimeUnit.MonthBased
99+
): YearMonthProgression =
100+
YearMonthProgression(rangeStart, rangeEnd, safeMultiplyOrClamp(stepValue, stepUnit.months.toLong()))
101+
}
102+
}
103+
104+
/**
105+
* A range of values of type [YearMonth].
106+
*
107+
* @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.simpleRangeCreation
108+
*/
109+
public class YearMonthRange(start: YearMonth, endInclusive: YearMonth) : YearMonthProgression(start, endInclusive, 1),
110+
ClosedRange<YearMonth>, OpenEndRange<YearMonth> {
111+
/**
112+
* Returns the lower bound of the range, inclusive.
113+
*/
114+
override val start: YearMonth get() = first
115+
116+
/**
117+
* Returns the upper bound of the range, inclusive.
118+
*/
119+
override val endInclusive: YearMonth get() = last
120+
121+
/**
122+
* Returns the upper bound of the range, exclusive.
123+
*/
124+
@Deprecated(
125+
"This throws an exception if the exclusive end if not inside " +
126+
"the platform-specific boundaries for YearMonth. " +
127+
"The 'endInclusive' property does not throw and should be preferred.",
128+
level = DeprecationLevel.WARNING
129+
)
130+
override val endExclusive: YearMonth
131+
get() {
132+
if (last == YearMonth.MAX)
133+
error("Cannot return the exclusive upper bound of a range that includes YearMonth.MAX.")
134+
return endInclusive.plus(1, DateTimeUnit.MONTH)
135+
}
136+
137+
/**
138+
* Returns true iff [value] is contained within the range.
139+
* i.e. [value] is between [start] and [endInclusive].
140+
*/
141+
@Suppress("ConvertTwoComparisonsToRangeCheck")
142+
override fun contains(value: YearMonth): Boolean {
143+
@Suppress("USELESS_CAST")
144+
if ((value as Any?) !is YearMonth) return false
145+
146+
return first <= value && value <= last
147+
}
148+
149+
/**
150+
* Returns true iff there are no months contained within the range.
151+
*/
152+
override fun isEmpty(): Boolean = first > last
153+
154+
/**
155+
* Returns a string representation of the range using the range operator notation.
156+
*/
157+
override fun toString(): String = "$first..$last"
158+
159+
public companion object {
160+
/** An empty range of values of type YearMonth. */
161+
public val EMPTY: YearMonthRange = YearMonthRange(YearMonth(0, 2), YearMonth(0, 1))
162+
163+
internal fun fromRangeUntil(start: YearMonth, endExclusive: YearMonth): YearMonthRange {
164+
return if (endExclusive == YearMonth.MIN) EMPTY else fromRangeTo(
165+
start,
166+
endExclusive.minus(1, DateTimeUnit.MONTH)
167+
)
168+
}
169+
170+
internal fun fromRangeTo(start: YearMonth, endInclusive: YearMonth): YearMonthRange {
171+
return YearMonthRange(start, endInclusive)
172+
}
173+
}
174+
}
175+
176+
/**
177+
* Returns the first [YearMonth] of the [YearMonthProgression].
178+
*
179+
* @throws NoSuchElementException if the progression is empty.
180+
*
181+
* @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.firstAndLast
182+
*/
183+
public fun YearMonthProgression.first(): YearMonth {
184+
if (isEmpty())
185+
throw NoSuchElementException("Progression $this is empty.")
186+
return this.first
187+
}
188+
189+
/**
190+
* Returns the last [YearMonth] of the [YearMonthProgression].
191+
*
192+
* @throws NoSuchElementException if the progression is empty.
193+
*
194+
* @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.firstAndLast
195+
*/
196+
public fun YearMonthProgression.last(): YearMonth {
197+
if (isEmpty())
198+
throw NoSuchElementException("Progression $this is empty.")
199+
return this.last
200+
}
201+
202+
/**
203+
* Returns the first [YearMonth] of the [YearMonthProgression], or null if the progression is empty.
204+
*
205+
* @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.firstAndLast
206+
*/
207+
public fun YearMonthProgression.firstOrNull(): YearMonth? = if (isEmpty()) null else this.first
208+
209+
/**
210+
* Returns the last [YearMonth] of the [YearMonthProgression], or null if the progression is empty.
211+
*
212+
* @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.firstAndLast
213+
*/
214+
public fun YearMonthProgression.lastOrNull(): YearMonth? = if (isEmpty()) null else this.last
215+
216+
/**
217+
* Returns a reversed [YearMonthProgression], i.e. one that goes from [last] to [first].
218+
* The sign of the step is switched, in order to reverse the direction of the progression.
219+
*
220+
* @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.reversedProgression
221+
*/
222+
public fun YearMonthProgression.reversed(): YearMonthProgression = YearMonthProgression(longProgression.reversed())
223+
224+
/**
225+
* Returns a [YearMonthProgression] with the same start and end, but a changed step value.
226+
*
227+
* **Pitfall**: the value parameter represents the magnitude of the step,
228+
* not the direction, and therefore must be positive.
229+
* Its sign will be matched to the sign of the existing step, in order to maintain the direction of the progression.
230+
* If you wish to switch the direction of the progression, use [YearMonthProgression.reversed]
231+
*
232+
* @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.progressionWithStep
233+
*/
234+
public fun YearMonthProgression.step(value: Int, unit: DateTimeUnit.MonthBased): YearMonthProgression =
235+
step(value.toLong(), unit)
236+
237+
/**
238+
* Returns a [YearMonthProgression] with the same start and end, but a changed step value.
239+
*
240+
* **Pitfall**: the value parameter represents the magnitude of the step,
241+
* not the direction, and therefore must be positive.
242+
* Its sign will be matched to the sign of the existing step, in order to maintain the direction of the progression.
243+
* If you wish to switch the direction of the progression, use [YearMonthProgression.reversed]
244+
*
245+
* @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.progressionWithStep
246+
*/
247+
public fun YearMonthProgression.step(value: Long, unit: DateTimeUnit.MonthBased): YearMonthProgression =
248+
YearMonthProgression(longProgression.step(safeMultiplyOrClamp(value, unit.months.toLong())))
249+
250+
/**
251+
* Creates a [YearMonthProgression] from `this` down to [that], inclusive.
252+
*
253+
* @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.simpleRangeCreation
254+
*/
255+
public infix fun YearMonth.downTo(that: YearMonth): YearMonthProgression =
256+
YearMonthProgression.fromClosedRange(this, that, -1, DateTimeUnit.MONTH)
257+
258+
/**
259+
* Returns a random [YearMonth] within the bounds of the [YearMonthProgression].
260+
*
261+
* Takes the step into account;
262+
* will not return any value within the range that would be skipped over by the progression.
263+
*
264+
* @throws IllegalArgumentException if the progression is empty.
265+
*
266+
* @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.random
267+
*/
268+
public fun YearMonthProgression.random(random: Random = Random): YearMonth =
269+
if (isEmpty()) throw NoSuchElementException("Cannot get random in empty range: $this")
270+
else longProgression.random(random).let(YearMonth.Companion::fromProlepticMonth)
271+
272+
/**
273+
* Returns a random [YearMonth] within the bounds of the [YearMonthProgression] or null if the progression is empty.
274+
*
275+
* Takes the step into account;
276+
* will not return any value within the range that would be skipped over by the progression.
277+
*
278+
* @sample kotlinx.datetime.test.samples.YearMonthRangeSamples.random
279+
*/
280+
public fun YearMonthProgression.randomOrNull(random: Random = Random): YearMonth? = longProgression.randomOrNull(random)
281+
?.let(YearMonth.Companion::fromProlepticMonth)
282+
283+
// this implementation is incorrect in general
284+
// (for example, `(Long.MIN_VALUE..Long.MAX_VALUE).random()` throws an exception),
285+
// but for the range of epoch days in YearMonth it's good enough
286+
private fun LongProgression.random(random: Random = Random): Long =
287+
random.nextLong(0L..(last - first) / step) * step + first
288+
289+
// incorrect in general; see `random` just above
290+
private fun LongProgression.randomOrNull(random: Random = Random): Long? = if (isEmpty()) null else random(random)
291+
292+
// this implementation is incorrect in general (for example, `(Long.MIN_VALUE..Long.MAX_VALUE).step(5).contains(2)`
293+
// returns `false` incorrectly https://www.wolframalpha.com/input?i=-2%5E63+%2B+1844674407370955162+*+5),
294+
// but for the range of epoch days in YearMonth it's good enough
295+
private fun LongProgression.contains(value: Long): Boolean =
296+
value in (if (step > 0) first..last else last..first) && (value - first) % step == 0L
297+
298+
// this implementation is incorrect in general (for example, `Long.MIN_VALUE..Long.MAX_VALUE` has size == 0),
299+
// but for the range of epoch days in YearMonth it's good enough
300+
private val LongProgression.size: Int
301+
get() = if (isEmpty()) 0 else try {
302+
(safeAdd(last, -first) / step + 1).clampToInt()
303+
} catch (e: ArithmeticException) {
304+
Int.MAX_VALUE
305+
}

0 commit comments

Comments
 (0)