0
# Ranges and Progressions
1
2
Range and progression support for date types, enabling iteration, membership testing, and range operations with step control. The library provides specialized range types for LocalDate and YearMonth with comprehensive progression capabilities.
3
4
## Capabilities
5
6
### LocalDateRange
7
8
Range implementation for LocalDate values, supporting inclusive ranges and iteration.
9
10
```kotlin { .api }
11
/**
12
* Range of LocalDate values with inclusive bounds
13
* Extends LocalDateProgression with range semantics
14
*/
15
class LocalDateRange : LocalDateProgression, ClosedRange<LocalDate> {
16
/** Lower bound of the range (inclusive) */
17
override val start: LocalDate
18
19
/** Upper bound of the range (inclusive) */
20
override val endInclusive: LocalDate
21
22
/**
23
* Check if the range contains no elements
24
* @returns true if start > endInclusive
25
*/
26
override fun isEmpty(): Boolean
27
28
companion object {
29
/** Empty range constant */
30
val EMPTY: LocalDateRange
31
}
32
}
33
```
34
35
**Creation:**
36
37
```kotlin { .api }
38
/**
39
* Create inclusive range from start to end
40
* @param that End date (inclusive)
41
* @returns LocalDateRange from this to that
42
*/
43
infix fun LocalDate.rangeTo(that: LocalDate): LocalDateRange
44
45
/**
46
* Create exclusive range from start to end
47
* @param that End date (exclusive)
48
* @returns LocalDateRange from this to that-1
49
*/
50
infix fun LocalDate.rangeUntil(that: LocalDate): LocalDateRange
51
```
52
53
**Usage Examples:**
54
55
```kotlin
56
import kotlinx.datetime.*
57
58
val start = LocalDate(2023, 12, 1)
59
val end = LocalDate(2023, 12, 31)
60
61
// Create ranges
62
val inclusiveRange = start..end // December 1-31, 2023
63
val exclusiveRange = start.rangeUntil(end) // December 1-30, 2023
64
val usingRangeTo = start.rangeTo(end) // Same as start..end
65
66
// Properties
67
println("Start: ${inclusiveRange.start}") // 2023-12-01
68
println("End: ${inclusiveRange.endInclusive}") // 2023-12-31
69
println("Empty: ${inclusiveRange.isEmpty()}") // false
70
71
// Empty range
72
val emptyRange = LocalDateRange.EMPTY
73
println("Empty: ${emptyRange.isEmpty()}") // true
74
75
// Membership testing
76
val christmas = LocalDate(2023, 12, 25)
77
val newYear = LocalDate(2024, 1, 1)
78
79
println("Christmas in range: ${christmas in inclusiveRange}") // true
80
println("New Year in range: ${newYear in inclusiveRange}") // false
81
82
// Iteration
83
println("First week of December:")
84
for (date in start..LocalDate(2023, 12, 7)) {
85
println(" $date (${date.dayOfWeek})")
86
}
87
```
88
89
### LocalDateProgression
90
91
Progression implementation that supports step-based iteration over LocalDate values.
92
93
```kotlin { .api }
94
/**
95
* Progression of LocalDate values with configurable step
96
* Supports forward and backward iteration with date-based steps
97
*/
98
open class LocalDateProgression : Iterable<LocalDate> {
99
/** First date in the progression */
100
val first: LocalDate
101
102
/** Last date that would be included (may not be reached if step doesn't align) */
103
val last: LocalDate
104
105
/** Number of elements in the progression */
106
val size: Int
107
108
/**
109
* Create reversed progression
110
* @returns LocalDateProgression in opposite direction
111
*/
112
fun reversed(): LocalDateProgression
113
114
/**
115
* Change the step of the progression
116
* @param value Step amount
117
* @param unit Date-based unit for the step
118
* @returns New progression with specified step
119
*/
120
fun step(value: Int, unit: DateTimeUnit.DayBased): LocalDateProgression
121
fun step(value: Long, unit: DateTimeUnit.DayBased): LocalDateProgression
122
123
/**
124
* Get a random element from the progression
125
* @param random Random number generator
126
* @returns Random LocalDate from the progression
127
*/
128
fun random(random: Random = Random.Default): LocalDate
129
130
/**
131
* Check if a date is contained in this progression
132
* @param value Date to check
133
* @returns true if the date is part of this progression
134
*/
135
operator fun contains(value: LocalDate): Boolean
136
137
/**
138
* Iterator for stepping through the progression
139
* @returns Iterator over LocalDate values
140
*/
141
override fun iterator(): Iterator<LocalDate>
142
}
143
```
144
145
**Creation Functions:**
146
147
```kotlin { .api }
148
/**
149
* Create descending progression from this date to that date
150
* @param that End date
151
* @returns LocalDateProgression stepping backward by days
152
*/
153
infix fun LocalDate.downTo(that: LocalDate): LocalDateProgression
154
```
155
156
**Usage Examples:**
157
158
```kotlin
159
import kotlinx.datetime.*
160
161
val start = LocalDate(2023, 12, 1)
162
val end = LocalDate(2023, 12, 31)
163
164
// Basic progression (daily steps)
165
val dailyProgression = start..end
166
println("Size: ${dailyProgression.size}") // 31 days
167
168
// Reverse progression
169
val reverseProgression = dailyProgression.reversed()
170
println("Reverse first: ${reverseProgression.first}") // 2023-12-31
171
println("Reverse last: ${reverseProgression.last}") // 2023-12-01
172
173
// Descending progression
174
val countdown = end.downTo(start)
175
println("Countdown from ${countdown.first} to ${countdown.last}")
176
177
// Weekly progression (every 7 days)
178
val weeklyProgression = (start..end).step(7, DateTimeUnit.DAY)
179
println("Weekly progression:")
180
for (date in weeklyProgression) {
181
println(" $date") // Every Sunday in December 2023
182
}
183
184
// Bi-weekly progression (every 14 days)
185
val biWeeklyProgression = (start..end).step(14, DateTimeUnit.DAY)
186
187
// Random selection
188
val randomDate = dailyProgression.random()
189
println("Random date in December: $randomDate")
190
191
// Membership testing
192
println("Christmas in progression: ${LocalDate(2023, 12, 25) in dailyProgression}") // true
193
194
// Custom step with different units (if extending to month-based)
195
val monthlyDates = LocalDate(2023, 1, 1)..LocalDate(2023, 12, 1)
196
// Note: Monthly stepping would require DateTimeUnit.MonthBased support in actual implementation
197
```
198
199
### YearMonthRange
200
201
Range implementation for YearMonth values, supporting month-based ranges.
202
203
```kotlin { .api }
204
/**
205
* Range of YearMonth values with inclusive bounds
206
* Extends YearMonthProgression with range semantics
207
*/
208
class YearMonthRange : YearMonthProgression, ClosedRange<YearMonth> {
209
/** Lower bound of the range (inclusive) */
210
override val start: YearMonth
211
212
/** Upper bound of the range (inclusive) */
213
override val endInclusive: YearMonth
214
215
/**
216
* Check if the range contains no elements
217
* @returns true if start > endInclusive
218
*/
219
override fun isEmpty(): Boolean
220
221
companion object {
222
/** Empty range constant */
223
val EMPTY: YearMonthRange
224
}
225
}
226
```
227
228
**Creation:**
229
230
```kotlin { .api }
231
/**
232
* Create inclusive range from start to end
233
* @param that End YearMonth (inclusive)
234
* @returns YearMonthRange from this to that
235
*/
236
infix fun YearMonth.rangeTo(that: YearMonth): YearMonthRange
237
238
/**
239
* Create exclusive range from start to end
240
* @param that End YearMonth (exclusive)
241
* @returns YearMonthRange from this to that-1month
242
*/
243
infix fun YearMonth.rangeUntil(that: YearMonth): YearMonthRange
244
```
245
246
**Usage Examples:**
247
248
```kotlin
249
import kotlinx.datetime.*
250
251
val startMonth = YearMonth(2023, 1) // January 2023
252
val endMonth = YearMonth(2023, 12) // December 2023
253
254
// Create ranges
255
val yearRange = startMonth..endMonth // All of 2023
256
val firstHalf = startMonth.rangeUntil(YearMonth(2023, 7)) // Jan-June 2023
257
258
// Properties
259
println("Start: ${yearRange.start}") // 2023-01
260
println("End: ${yearRange.endInclusive}") // 2023-12
261
println("Size: ${yearRange.size}") // 12
262
263
// Membership testing
264
val summer = YearMonth(2023, 7)
265
val nextYear = YearMonth(2024, 1)
266
267
println("July in range: ${summer in yearRange}") // true
268
println("Next year in range: ${nextYear in yearRange}") // false
269
270
// Iteration
271
println("First quarter:")
272
for (month in YearMonth(2023, 1)..YearMonth(2023, 3)) {
273
println(" $month - ${month.numberOfDays} days")
274
}
275
```
276
277
### YearMonthProgression
278
279
Progression implementation for YearMonth values with month-based stepping.
280
281
```kotlin { .api }
282
/**
283
* Progression of YearMonth values with configurable step
284
* Supports forward and backward iteration with month-based steps
285
*/
286
open class YearMonthProgression : Iterable<YearMonth> {
287
/** First month in the progression */
288
val first: YearMonth
289
290
/** Last month that would be included */
291
val last: YearMonth
292
293
/** Number of elements in the progression */
294
val size: Int
295
296
/**
297
* Create reversed progression
298
* @returns YearMonthProgression in opposite direction
299
*/
300
fun reversed(): YearMonthProgression
301
302
/**
303
* Change the step of the progression
304
* @param value Step amount
305
* @param unit Month-based unit for the step
306
* @returns New progression with specified step
307
*/
308
fun step(value: Int, unit: DateTimeUnit.MonthBased): YearMonthProgression
309
fun step(value: Long, unit: DateTimeUnit.MonthBased): YearMonthProgression
310
311
/**
312
* Get a random element from the progression
313
* @param random Random number generator
314
* @returns Random YearMonth from the progression
315
*/
316
fun random(random: Random = Random.Default): YearMonth
317
318
/**
319
* Check if a YearMonth is contained in this progression
320
* @param value YearMonth to check
321
* @returns true if the YearMonth is part of this progression
322
*/
323
operator fun contains(value: YearMonth): Boolean
324
325
/**
326
* Iterator for stepping through the progression
327
* @returns Iterator over YearMonth values
328
*/
329
override fun iterator(): Iterator<YearMonth>
330
}
331
```
332
333
**Creation Functions:**
334
335
```kotlin { .api }
336
/**
337
* Create descending progression from this month to that month
338
* @param that End month
339
* @returns YearMonthProgression stepping backward by months
340
*/
341
infix fun YearMonth.downTo(that: YearMonth): YearMonthProgression
342
```
343
344
**Usage Examples:**
345
346
```kotlin
347
import kotlinx.datetime.*
348
349
val startMonth = YearMonth(2023, 1)
350
val endMonth = YearMonth(2025, 12)
351
352
// Basic progression (monthly steps)
353
val monthlyProgression = startMonth..endMonth
354
println("Total months: ${monthlyProgression.size}") // 36 months
355
356
// Reverse progression
357
val reverseMonths = monthlyProgression.reversed()
358
println("Reverse: ${reverseMonths.first} to ${reverseMonths.last}")
359
360
// Descending progression
361
val countdown = endMonth.downTo(startMonth)
362
363
// Quarterly progression (every 3 months)
364
val quarterlyProgression = (startMonth..endMonth).step(3, DateTimeUnit.MONTH)
365
println("Quarterly progression:")
366
for (month in quarterlyProgression) {
367
println(" $month (Q${(month.month.number - 1) / 3 + 1} ${month.year})")
368
}
369
370
// Yearly progression (every 12 months)
371
val yearlyProgression = (startMonth..endMonth).step(12, DateTimeUnit.MONTH)
372
println("Yearly progression:")
373
for (month in yearlyProgression) {
374
println(" $month") // January of each year
375
}
376
377
// Semi-annual progression (every 6 months)
378
val semiAnnualProgression = (startMonth..endMonth).step(6, DateTimeUnit.MONTH)
379
380
// Random selection
381
val randomMonth = monthlyProgression.random()
382
println("Random month: $randomMonth")
383
```
384
385
## Advanced Range Operations
386
387
### Range Extensions and Utilities
388
389
```kotlin
390
import kotlinx.datetime.*
391
392
// Utility functions for working with date ranges
393
fun LocalDateRange.weekends(): List<LocalDate> {
394
return this.filter { it.dayOfWeek == DayOfWeek.SATURDAY || it.dayOfWeek == DayOfWeek.SUNDAY }
395
}
396
397
fun LocalDateRange.weekdays(): List<LocalDate> {
398
return this.filter { it.dayOfWeek !in listOf(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY) }
399
}
400
401
fun LocalDateRange.monthBoundaries(): List<LocalDate> {
402
return this.filter { it.day == 1 || it == it.yearMonth.lastDay }
403
}
404
405
// Usage
406
val december = LocalDate(2023, 12, 1)..LocalDate(2023, 12, 31)
407
408
val weekendDays = december.weekends()
409
println("Weekend days in December: ${weekendDays.size}")
410
411
val workDays = december.weekdays()
412
println("Work days in December: ${workDays.size}")
413
414
// Month boundaries across a longer range
415
val yearRange = LocalDate(2023, 1, 1)..LocalDate(2023, 12, 31)
416
val monthBounds = yearRange.monthBoundaries()
417
println("Month boundaries in 2023: ${monthBounds.size}") // 24 (first and last day of each month)
418
```
419
420
### Custom Step Functions
421
422
```kotlin
423
import kotlinx.datetime.*
424
425
// Extension function for business day progression (Mon-Fri only)
426
fun LocalDate.businessDaysTo(endDate: LocalDate): Sequence<LocalDate> {
427
return generateSequence(this) { current ->
428
val next = current.plus(1, DateTimeUnit.DAY)
429
if (next <= endDate) {
430
// Skip to next weekday if next is weekend
431
when (next.dayOfWeek) {
432
DayOfWeek.SATURDAY -> next.plus(2, DateTimeUnit.DAY)
433
DayOfWeek.SUNDAY -> next.plus(1, DateTimeUnit.DAY)
434
else -> next
435
}
436
} else null
437
}.takeWhile { it <= endDate }
438
}
439
440
// Usage
441
val startDate = LocalDate(2023, 12, 1) // Friday
442
val endDate = LocalDate(2023, 12, 15) // Friday
443
444
val businessDays = startDate.businessDaysTo(endDate).toList()
445
println("Business days:")
446
businessDays.forEach { date ->
447
println(" $date (${date.dayOfWeek})")
448
}
449
450
// Extension for nth day of month progression
451
fun YearMonth.nthDayProgression(dayOfMonth: Int, months: Int): Sequence<LocalDate> {
452
return generateSequence(0) { it + 1 }
453
.take(months)
454
.map { this.plus(it, DateTimeUnit.MONTH) }
455
.mapNotNull { yearMonth ->
456
try {
457
LocalDate(yearMonth.year, yearMonth.month, dayOfMonth)
458
} catch (e: Exception) {
459
// Handle cases where day doesn't exist (e.g., Feb 30)
460
null
461
}
462
}
463
}
464
465
// Usage: Every 15th of the month for a year
466
val base = YearMonth(2023, 1)
467
val fifteenthOfMonths = base.nthDayProgression(15, 12).toList()
468
println("15th of each month in 2023:")
469
fifteenthOfMonths.forEach { println(" $it") }
470
```
471
472
### Range Set Operations
473
474
```kotlin
475
import kotlinx.datetime.*
476
477
// Extension functions for range operations
478
fun LocalDateRange.intersect(other: LocalDateRange): LocalDateRange? {
479
val start = maxOf(this.start, other.start)
480
val end = minOf(this.endInclusive, other.endInclusive)
481
return if (start <= end) start..end else null
482
}
483
484
fun LocalDateRange.union(other: LocalDateRange): LocalDateRange? {
485
// Only works for overlapping or adjacent ranges
486
val gap = when {
487
this.endInclusive < other.start -> other.start.minus(this.endInclusive.plus(1, DateTimeUnit.DAY))
488
other.endInclusive < this.start -> this.start.minus(other.endInclusive.plus(1, DateTimeUnit.DAY))
489
else -> DatePeriod() // Overlapping or adjacent
490
}
491
492
return if (gap.days <= 1) { // Adjacent or overlapping
493
minOf(this.start, other.start)..maxOf(this.endInclusive, other.endInclusive)
494
} else null
495
}
496
497
fun LocalDateRange.subtract(other: LocalDateRange): List<LocalDateRange> {
498
val intersection = this.intersect(other) ?: return listOf(this)
499
500
val ranges = mutableListOf<LocalDateRange>()
501
502
// Before intersection
503
if (this.start < intersection.start) {
504
ranges.add(this.start.rangeUntil(intersection.start))
505
}
506
507
// After intersection
508
if (intersection.endInclusive < this.endInclusive) {
509
ranges.add(intersection.endInclusive.plus(1, DateTimeUnit.DAY)..this.endInclusive)
510
}
511
512
return ranges
513
}
514
515
// Usage
516
val range1 = LocalDate(2023, 12, 1)..LocalDate(2023, 12, 15) // Dec 1-15
517
val range2 = LocalDate(2023, 12, 10)..LocalDate(2023, 12, 25) // Dec 10-25
518
519
val intersection = range1.intersect(range2)
520
println("Intersection: $intersection") // Dec 10-15
521
522
val union = range1.union(range2)
523
println("Union: $union") // Dec 1-25
524
525
val subtraction = range1.subtract(range2)
526
println("Range1 - Range2: $subtraction") // [Dec 1-9]
527
```
528
529
### Performance Considerations
530
531
```kotlin
532
import kotlinx.datetime.*
533
534
// For large ranges, prefer sequences over lists to avoid memory issues
535
fun LocalDateRange.asSequence(): Sequence<LocalDate> {
536
return generateSequence(this.start) { current ->
537
val next = current.plus(1, DateTimeUnit.DAY)
538
if (next <= this.endInclusive) next else null
539
}
540
}
541
542
// Efficient counting without iteration
543
fun LocalDateRange.count(): Int {
544
return if (isEmpty()) 0 else this.start.daysUntil(this.endInclusive) + 1
545
}
546
547
// Efficient nth element access
548
fun LocalDateRange.elementAt(index: Int): LocalDate? {
549
return if (index in 0 until count()) {
550
this.start.plus(index, DateTimeUnit.DAY)
551
} else null
552
}
553
554
// Usage for large ranges
555
val largeRange = LocalDate(2020, 1, 1)..LocalDate(2030, 12, 31) // 11 years
556
println("Large range size: ${largeRange.count()}") // ~4000 days
557
558
// Use sequence for memory-efficient processing
559
val weekendsCount = largeRange.asSequence()
560
.filter { it.dayOfWeek in listOf(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY) }
561
.count()
562
563
println("Weekends in large range: $weekendsCount")
564
565
// Direct access without iteration
566
val midpoint = largeRange.elementAt(largeRange.count() / 2)
567
println("Midpoint date: $midpoint")
568
```
569
570
## Range Validation and Error Handling
571
572
```kotlin
573
import kotlinx.datetime.*
574
575
// Safe range creation with validation
576
fun createSafeRange(start: LocalDate, end: LocalDate): LocalDateRange? {
577
return if (start <= end) {
578
start..end
579
} else {
580
null // Invalid range
581
}
582
}
583
584
// Range bounds checking
585
fun LocalDateRange.isValidRange(): Boolean {
586
return !isEmpty()
587
}
588
589
fun LocalDateRange.clampToRange(bounds: LocalDateRange): LocalDateRange? {
590
val clampedStart = maxOf(this.start, bounds.start)
591
val clampedEnd = minOf(this.endInclusive, bounds.endInclusive)
592
593
return if (clampedStart <= clampedEnd) {
594
clampedStart..clampedEnd
595
} else {
596
null // No overlap
597
}
598
}
599
600
// Usage
601
val validRange = createSafeRange(
602
LocalDate(2023, 12, 1),
603
LocalDate(2023, 12, 31)
604
) // Valid range
605
606
val invalidRange = createSafeRange(
607
LocalDate(2023, 12, 31),
608
LocalDate(2023, 12, 1)
609
) // null - invalid
610
611
// Bounds checking
612
val bounds = LocalDate(2023, 1, 1)..LocalDate(2023, 12, 31) // Year 2023
613
val testRange = LocalDate(2022, 6, 1)..LocalDate(2024, 6, 1) // Spans multiple years
614
615
val clamped = testRange.clampToRange(bounds)
616
println("Clamped range: $clamped") // 2023-01-01..2023-12-31
617
```