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 Range
s, and we can use a different form of creating ranges. Finally, we will have two objects:
numbers
is an object ofIntRange
typecharacters
is an objects ofCharRange
type
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
,Int
andLong
.
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 Progression
s, and we can use a different form of creating ranges. Finally, we will have two objects:
numbers
is an object ofIntProgression
typecharacters
is an objects ofCharProgression
type
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
->IntIterator
CharProgression
->CharIterator
LongProgression
->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.