Kotlin

Guide: Ranges in Kotlin

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.
Alex Zhukovich 6 min read
Guide: Ranges in Kotlin
Table of Contents

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:

  • numbers is an object of IntRange type
  • characters is an objects of CharRange 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.

Read more

Predefined Ranges:

In Kotlin defined specific types for arithmetic progressions of Char, Int and Long.

Read more

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:

  • numbers is an object of IntProgression type
  • characters is an objects of CharProgression 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.

Resources


Mobile development with Alex

A blog about Android development & testing, Best Practices, Tips and Tricks

Share
More from Mobile development with Alex

Great! You’ve successfully signed up.

Welcome back! You've successfully signed in.

You've successfully subscribed to Mobile development with Alex.

Success! Check your email for magic link to sign-in.

Success! Your billing info has been updated.

Your billing was not updated.