Traversing through dates with Kotlin range expressions

Photo of Samuel Urbanowicz

Samuel Urbanowicz

Updated Jan 4, 2023 • 9 min read
estee-janssens-396889-unsplash

In order to make the syntax for loop iteration and control flow statements safe and natural to read, Kotlin standard library provides a concept of ranges. In this post we are going to explore how to use range expressions in action.

First, we are going to discover a built-in ranges implementations for the integral types like Char, Int or Long. Next, we are going to implement a custom progression for the LocalDate class.

Iterating through primitive types

The idea of a range itself can be seen as an abstract data type that models a closed scope of values or a set of objects that can be iterated through, in a concise way. An interface from kotlin.ranges package called ClosedRange is a base declaration that models the idea of range. It stores handles to first and last elements of the range and also provides contains(value: T): Boolean and isEmpty(): Boolean functions that check two conditions: if the given value belongs to the range and if the range is empty. There are built in implementations of ranges for the integral primitive types, like Int, Long and Char. To define a range for the primitive type we use rangeTo() function. Let’s take a look at the following example:

val range: IntRange = 0.rangeTo(1000)

The declaration above defines a range of integers of values from 0 to 1000. The rangeTo() function has also its very own operator equivalent .. that allows to declare a range in a more natural way:

val range: IntRange = 0..1000

Under the hood, the IntRange implementation contains IntProgression class that provides Iterator interface implementation required for iteration. Here is an example of using the progression of integers with the for loop:

val progression: IntProgression = 0..1000 step 100

for (i in progression) {

println(i)

}

As a result of executing the code above we would get the following text printed to the console:

0

100

200

300

400

500

600

700

800

900

1000

We could also achieve the same result implementing the iteration using while loop in the more imperative manner:

var i = 0

while (i <= 1000) {

println(i)

i += 100

}


Traversing through non-primitive types

It turns out it’s also quite easy to implement a custom progression for any type implementing Comparable interface. In this part we will explore how to create a progression of LocalDate type objects and discover how traverse dates the easy way.

To accomplish this task, first we need to get familiar with the ClosedRange and Iterator interfaces. We will need to implement them in order to declare a progression for LocalDate type.

public interface ClosedRange<T: Comparable<T>> {

public val start: T

public val endInclusive: T

public operator fun contains(value: T): Boolean =
value >= start && value <= endInclusive

public fun isEmpty(): Boolean = start > endInclusive

}

The ClosedRange interface exposes the minimum and maximum values of the range. It also provides contains(value: T): Boolean and isEmpty(): Boolean functions implementation that check if the given value belongs to the range and if the range is empty. On the other hand, the Iterator interface just provides an information about a next value and its availability, as follows:

public interface Iterator<out T> {

public operator fun next(): T

public operator fun hasNext(): Boolean

}

Let’s start with implementing the Iterator instance for the LocalDate type. We are going to create a custom class called DateIterator that implements Iterator<LocalDate> interface.

class DateIterator(val 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

}

}

The DateIterator class has 3 properties - currentDate, endDateInclusive and stepDays. Inside the next() function, we are returning the currentDate value and updating it to the date value using a given stepDays property interval.

Now, let’s move forward and implement the progression for LocalDate type. We are going to create a new class called DateProgression which is going to implement Iterable<LocalDate> and ClosedRange<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> =
DateIterator(start, endInclusive, stepDays)

infix fun step(days: Long) = DateProgression(start, endInclusive, days)

}

The DateProgression class merges functionalities declared by both Iterable<LocalDate> and ClosedRange<LocalDate> interfaces. To allow the iteration we need to provide an instance of DateIterator class as the Iterator value required by Iterable interface. We also need to override start and endInclusive properties of ClosedRange interface directly in the constructor. There is also stepDays property with a default value 1 assigned. As you can see, every time we are invoking the step function, a new instance of DateProgression class is being created.

The last step needed to use LocalDate type in for of range expression is to declare a custom rangeTo operator for the LocalDate class.

operator fun LocalDate.rangeTo(other: LocalDate) = DateProgression(this, other)

That's it! Now, we can work with LocalDate type using range expressions. Let’s see how to use our implementation in action. In the example below, we will use our rangeTo operator declaration to iterate through the given dates range:

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 the result, the lines of code in the example above would print the next dates with a week long interval, as follows:

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


There is more...

Thanks to implementing a simple DateProgression class we have managed to achieve an effective way of iterating through LocalDate instances. However, our implementation provides a neat way of using ranges of LocalDate objects in flow control conditions as well:

val startDate = LocalDate.of(2018, 1, 1)
val endDate = LocalDate.of(2018, 12, 31)
if(LocalDate.now() in startDate..endDate) print("Welcome in 2018!")

Another powerful feature of Kotlin that we have used in the LocalDate iteration example is operator overloading possibility. It allows to replace functions calls with a concise syntax and helps to write code that is less boilerplate and natural to read at the same time.

I hope you’ve enjoyed this post. Please leave your comment if you have any questions and share how you're using range expressions in your projects.

Photo of Samuel Urbanowicz

More posts by this author

Samuel Urbanowicz

Samuel is a pragmatic software engineer skilled in Android app development. A fan of modern...
Lost with AI?  Get the most important news weekly, straight to your inbox, curated by our CEO  Subscribe to AI'm Informed

Read more on our Blog

Check out the knowledge base collected and distilled by experienced professionals.

We're Netguru

At Netguru we specialize in designing, building, shipping and scaling beautiful, usable products with blazing-fast efficiency.

Let's talk business