Let's explore Ranges in Kotlin and understand how it can simplify development. In the end, we create a custom range for the delivery time of a restaurant base on open and close time.
Overview
We can create a Range of integers and Range of chars:
val numbers = 1..5 // 1 2 3 4 5
val characters = 'a'.rangeTo('z') // all letters from 'a' to 'z'
Both of these samples create Ranges, and we can use a different form of creating ranges. Finally, we will have two objects:
numbersis an object ofIntRangetypecharactersis an objects ofCharRangetype
A range defines a closed interval in the mathematical sense: it is defined by its two endpoint values, which are both included in the range. Ranges are defined for comparable types: having an order, you can define whether an arbitrary instance is in the range between two given instances.
Predefined Ranges:
In Kotlin defined specific types for arithmetic progressions of
Char,IntandLong.
Suppose we want to create a sequence of numbers from the largest number number to the smallest one. We need to use the downTo operator. In this case, the Progression object will be created.
val numbers = 5.downTo(1) // 5 4 3 2 1
val characters = 'c'.downTo('a') // c b a
Both of these samples create Progressions, and we can use a different form of creating ranges. Finally, we will have two objects:
numbersis an object ofIntProgressiontypecharactersis an objects ofCharProgressiontype
The IntProgression, CharProgression and LongProgression supports the custom step for progressions.
val numbers = 1..5 // 1 2 3 4 5
val numbersWithStep = 1..5 step 2 // 1 3 5
Operators
We can use the in operator with Range objects to know if a value is presented in a specific Range.
The "in" operator
Let's start with checking that a character f is a part of a specific Range.
val characters = 'a'.rangeTo('z')
val isPresent = 'f' in characters // TRUE
// or
val isPresent = 'f' in 'a'.rangeTo('c') // FALSE
However, under the hood, the contains function from CharRange will be used.
char var1 = 'a';
CharRange characters = new CharRange(var1, 'z');
boolean isPresent = characters.contains('f');
We can also use the in operator as part of for-loops:
for (i in 'a'..'f') {
print(i)
}
The next place when Range and in operator can be helpful is a when expression.
val result = 7
when (result) {
in 0..5 -> print("1..5")
in 5..10 -> print("5..10")
else -> print("error")
}
The "rangeTo" operator
The rangeTo operator allows us to create a Range of elements.
val characters = 'a'.rangeTo('z')
The following syntax is available because of the rangeTo operator.
val characters = 'a'..'z'
Properties
Any Range object has a list of properties that can help us get the information. Let's take a look at properties.
Note: Almost all properties available in the predefined Range object because of the implementation of Ranges(IntRange, CharRange, LongRange) based on Progression implementation. The start and endInclusive properties are available only for Range objects.
public class IntRange(
start: Int,
endInclusive: Int
) : IntProgression(start, endInclusive, 1), ClosedRange<Int> {
...
}
The "start" property
The start property represents the minimum value of a Range object; it equals to first property of progression in implementation of IntProgression, CharProgression, and LongProgression classes.
val intRange = 1..450
val start = intRange.start // 1
Note: This property is available only for the Range object and not available for the Progression one.
The "endInclusive" property
The endInclusive property represents the maximum value of a Range object; it equals to last property of progression in implementation of IntProgression, CharProgression, and LongProgression classes.
val charRange = 'a'.rangeTo('z')
val endInclusive = charRange.endInclusive // z
Note: This property is available only for the Range object and not available for the Progression one.
The "first" property
The first property represents the first element of progression.
val intRange = 1..450
val first = intRange.first // 1
Note: This property is available for both Range and Progression objects.
The "last" property
The last property represents the last element of progression.
val intRange = 1..450
val last = intRange.last // 450
Note: This property is available for both Range and Progression objects.
The "step" property
The step property represents the step of progression.
val intProgression = 1..450 step 10
val intStep = intRange.step // 10
val longProgression = 10L downTo 1L
val longStep = longProgression.step // -1
val charProgression = 'a'..'z'
val charStep = charProgression.step // 1
Note: This property is available only for the Progression object and not available for the Range one.
Functions
Ranges have many extension functions, which can be very helpful during development. I propose to take a look at few of them.
The "maxOrNull", "minOrNull" and "sum" functions
The maxOrNull function returns the largest element of all elements or null.
val intRange: IntRange = 1..10
val max = intRange.maxOrNull() // 10
The minOrNull function returns the smallest value of all elements or null.
val intRange: IntRange = 1..10
val min = intRange.minOrNull() // 1
The sum function returns a sum of all elements.
val intRange = 1..10
val sum = intRange.sum() // 55
Note: The sum function is not available for the CharRange objects.
The "iterator" function
The iterator function convert *Progression objects to the following types:
IntProgression->IntIteratorCharProgression->CharIteratorLongProgression->LongIterator
So, let's take a look at one of the Iterator classes: IntIterator.
/** An iterator over a sequence of values of type `Int`. */
public abstract class IntIterator : Iterator<Int> {
override final fun next() = nextInt()
/** Returns the next value in the sequence without boxing. */
public abstract fun nextInt(): Int
}
When we have access to Iterator<T> we can get values of a Range one by one.
The "filter" function
The filter function returns a List<T> of values, which are matching with a predicate.
val oddNumbers = intRange.filter { it % 2 != 0 }
println("oddNumbers: $oddNumbers") // 1 3 5 7 9
In addition to it, we have access to other extension functions of Iterator<T> relating to filtering data, like filterNot, filterNotNull, etc.
The "map" function
The map function returns a List<T> of containing the result of applying the transformation.
val intRange: IntRange = 1..3 // 1 2 3
val values = intRange.map { "$it item" } // 1 item, 2 item, 3 item
The "reversed" function
The reversed functions *Progression(IntProgression, CharProgression, LongProgression) with reversed values.
val intRange = 1..10
val reversed = intRange.reversed() // similar to 10.downTo(1)
The "random" function
The random function returns a random element of a Range.
val charRange: CharRange = 'a'..'c'
val randomChar = charRange.random() // 'a' or 'b' or 'c'
Note: The random() function is available only for the Range object and not available for the Progression one.
Creating a custom Ranges and Progression
Let's implement a custom Range that generates a Range of the restaurant's delivery time options. Assume that we have a restaurant which can deliver an order to any address.
data class Restaurant(
val id: Long,
val name: String,
val openTime: Date,
val closeTime: Date
)
I recommend to start with creating a custom Iterator which will generate a delivery time options:
class DeliveryTimeIterator(
private val start: Date,
private val endInclusive: Date,
private val stepTime: Long
): Iterator<Date> {
private var currentValue = start.time
override fun hasNext(): Boolean {
return currentValue <= endInclusive.time
}
override fun next(): Date {
val next = currentValue
currentValue += stepTime
return Date(next)
}
}
The next step is to create a Range object which supports custom interval.
class DeliveryTimeOptions(
override val start: Date,
override val endInclusive: Date,
private val stepTime: Long = 3_600_000
) : ClosedRange<Date>, Iterable<Date> {
override fun iterator(): Iterator<Date> {
return DeliveryTimeIterator(start, endInclusive, stepTime)
}
infix fun step(stepTime: Long) = DeliveryTimeOptions(start, endInclusive, stepTime)
}
Afterward, we should add a rangeTo operator for the Date type.
operator fun Date.rangeTo(other: Date) = DeliveryTimeOptions(this, other)
Finally, we update the Restaurant class to have the possibility to generate a DeliveryTimeOptions object.
data class Restaurant(
val id: Long,
val name: String,
val openTime: Date,
val closeTime: Date
) {
private val deliveryTimeStep: Long = 15 * 60 * 1000
fun getDeliveryTimeOptions(): DeliveryTimeOptions {
return openTime..closeTime step deliveryTimeStep
}
}
Right now we can play with it:
val openTime = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, 9)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
}
val closeTime = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, 17)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
}
val restaurant = Restaurant(
id = 42L,
name = "FooBar",
openTime = openTime.time,
closeTime = closeTime.time
)
println("Delivery times: ${restaurant.getDeliveryTimeOptions().toList()}")
Output:
Fri Nov 20 09:00:00 CET 2020
Fri Nov 20 09:15:00 CET 2020
Fri Nov 20 09:30:00 CET 2020
...
Fri Nov 20 16:30:00 CET 2020
Fri Nov 20 16:45:00 CET 2020
Fri Nov 20 17:00:00 CET 2020
The source code of custom Range you can find here.