What Are Kotlin Progressions and Why Should You Care?
When writing for-loops over ranges in Kotlin, Progressions can help you incorporate types that aren't supported by default. Here's a guide tailored for Java devs.
Join the DZone community and get the full member experience.
Join For FreeLife Is Great
One of the first things that you notice when you start learning Kotlin is the "nice" for loop syntax that it has:
for (i in 1..5) { // mm, so fancy!
println(i)
}
Cool, isn't it? An old, boring Java loop does not stand a chance.
for (int i = 0; i < 5; ++i) { // ugh, so boring!
System.out.println(i);
}
As long as you stay in the world of trivial code samples or you iterate only over collections and ranges like the ones above, things are great. Kotlin is great, life is great, even politics don't bother you anymore.
Sad Times Arrive
Then, one day, you need to write a more sophisticated loop, say an equivalent of Java's:
LocalDate start = ...
LocalDate end = ...
for (LocalDate ld = start, !ld.isAfter(end); ld.plusDays(1)) {
// stuff
}
"Ha, easy. I'll just switch to the Java-ish syntax and I'm fine", I thought in exactly such a case:
for (val ld = start; ld <= end; ld.plusDays(1)) {
// stuff
}
An unpleasant surprise: There's no such construct in Kotlin! If you try to paste the snippet above to IntelliJ, it will automatically convert it to a while loop. Woops!
My next thought was: "Hey, there's this range syntax for dates that I used before. Maybe I can iterate over that..."
for (ld in start..end) {
// stuff
}
Nope! That does not work either. At least not right away. As I learned a short while later, the secret of these nice Kotlin loops lies in the rangeTo operator and a bunch of base classes called Progressions.
RangeTo Operator
Whenever you type the double dot operator to create a nice range like "1..5" or "start..end", Kotlin translates the two dots in there into a call to the operator function called rangeTo.
As it turns out, Kotlin contains a few convenient implementations of the rangeTo operator for primitives and comparable types. The difference between the rangeTo for Int's and the rangeTo for LocalDate's is that the former returns an IntRange, while the latter returns a ClosedRange.
If you dig deeper into the implementation, you'll notice that IntRange extends a class called IntProgression, which in turn implements Iterable<Int>. That's the very reason why you can iterate over a range of Ints, while you cannot iterate over some other valid ranges of values.
Progressions
IntProgression is one of a few classes ending with the word Progression. The term denotes in Kotlin an Iterable that can use an arbitrary value as a step. You've probably seen already that in Kotlin you can do things like this:
for (i in 1..31 step 2) {
println(i)
}
The step word here is nothing else than a call to an infix function called step, present in the IntProgression class. When you call this method, you get a new IntProgression with a step of requested size.
Our Very Own Progression
Now, equipped with all this valuable knowledge, we can finally solve our problem of iterating dates by implementing a progression ourselves!
class LocalDateProgression(override val start: LocalDate,
override val endInclusive: LocalDate,
val stepDays: Long = 1) : Iterable<LocalDate>, ClosedRange<LocalDate> {
override fun iterator(): Iterator<LocalDate> = LocalDateProgressionIterator(start, endInclusive, stepDays)
infix fun step(days: Long) = LocalDateProgression(start, endInclusive, days)
}
As you can see, there's no rocket science in there. Three parameters and trivial, one-line implementations and we have our very own, working progression. Obviously, we also need to implement the iterator that I've used in there:
internal class LocalDateProgressionIterator(start: LocalDate, val endInclusive: LocalDate, val stepDays: Long) : Iterator<LocalDate> {
var current = start
override fun hasNext() = current <= endInclusive
override fun next(): LocalDate {
val next = current
current = current.plusDays(stepDays)
return next
}
}
And the last thing, we need to override the rangeTo operator for LocalDate, so that it uses our brand new progression:
operator fun LocalDate.rangeTo(other: LocalDate) = LocalDateProgression(this, other)
Tada!
for (ld in start..end step 2) { // mm, so fancy!
println(ld)
}
Summary
As you can see, the for-loop over ranges in Kotlin is actually nothing magical. If your type is not supported by default, you can simply implement a new Progression, override the rangeTo operator, and your for-loops are fancy and shiny again. As for me, I'd actually prefer to be able to write a "normal" Java-ish for loop instead. "Ugh, so boring!"
Opinions expressed by DZone contributors are their own.
Comments