Building custom progressions to traverse dates
Kotlin provides built-in support for ranges of primitive types. In the previous recipes, we worked with the IntRange
and CharRange
types, which are included in the Kotlin standard library. However, it is possible to implement a custom progression for any type by implementing the Comparable
interface. In this recipe, we will learn how to create a progression of the LocalDate
type and discover how to traverse the dates the easy way.
Getting ready
In order to accomplish the task, we need to start by getting familiar with the ClosedRange
and Iterator
interfaces. We need to use them to declare a custom progression for the LocalDate
class:
public interface ClosedRange<T: Comparable<T>> { public val start: T public val endInclusive: T public operator fun contains(value: T): Boolean { return value >= start && value <= endInclusive } public fun isEmpty(): Boolean = start > endInclusive }
The Iterator
interface provides information about the subsequent values and their availability:
public interface Iterator<out T> { public operator fun next(): T public operator fun hasNext(): Boolean }
The ClosedRange
interface provides the minimum and maximum values of the range. It also provides the contains(value: T): Boolean
and isEmpty(): Boolean
functions, which check whether a given value belongs to the range and whether the range is empty respectively. Those two functions have default implementations provided in the ClosedRange
interface. As the result, we don't need to override them in our custom implementation of the ClosedRange
interface.
How to do it...
- Let's start with implementing the
Iterator
interface for theLocalDate
type. We are going to create a customLocalDateIterator
class, which implements theIterator<LocalDate>
interface:
class DateIterator(startDate: LocalDate, val endDateInclusive: LocalDate, val stepDays: Long) : Iterator<LocalDate> { private var currentDate = startDate override fun hasNext() = currentDate <= endDateInclusive override fun next(): LocalDate { val next = currentDate currentDate = currentDate.plusDays(stepDays) return next } }
- Now, we can implement the progression for the
LocalDate
type. Let's create a new class calledDateProgression
, which is going to implement theIterable<LocalDate>
andClosedRange<LocalDate>
interfaces:
class DateProgression(override val start: LocalDate, override val endInclusive: LocalDate, val stepDays: Long = 1) : Iterable<LocalDate>, ClosedRange<LocalDate> { override fun iterator(): Iterator<LocalDate> { return DateIterator(start, endInclusive, stepDays) } infix fun step(days: Long) = DateProgression(start, endInclusive, days) }
- Finally, declare a custom
rangeTo
operator for theLocalDate
class:
operator fun LocalDate.rangeTo(other: LocalDate) = DateProgression(this, other)
How it works...
Now, we are able to declare range expressions for the LocalDate
type. Let's see how to use our implementation. In the following example, we will use our custom LocalDate.rangeTo
operator implementation in order to create a range of dates and iterate its elements:
val startDate = LocalDate.of(2020, 1, 1) val endDate = LocalDate.of(2020, 12, 31) for (date in startDate..endDate step 7) { println("${date.dayOfWeek} $date ") }
As a result, we are going to have the dates printed out to the console with a week-long interval:
WEDNESDAY 2020-01-01 WEDNESDAY 2020-01-08 WEDNESDAY 2020-01-15 WEDNESDAY 2020-01-22 WEDNESDAY 2020-01-29 WEDNESDAY 2020-02-05 ... WEDNESDAY 2020-12-16 WEDNESDAY 2020-12-23 WEDNESDAY 2020-12-30
TheDateIterator
class holds three properties—currentDate: LocalDate
,endDateInclusive: LocalDate
, andstepDays: Long
. In the beginning, the currentDate
property is initialized with the startDate
value passed in the constructor. Inside thenext()
function, we are returning thecurrentDate
value and updating it to the next date value using a givenstepDays
property interval.
The DateProgression
class combines the functionalities of theIterable<LocalDate>
andClosedRange<LocalDate>
interfaces. It provides the Iterator
object required by the Iterable
interface by returning the DateIterator
instance. It also overrides the start
and endInclusive
properties of the ClosedRange
interface. There is also the stepDays
property with a default value equal to 1
. Note that every time the step
function is called, a new instance of the DateProgression
class is being created.
You can follow the same pattern to implement custom progressions for any class that implements the Comparable
interface.