Ranges & Progressions #
Kotlin has a very expressive way to represent value ranges: 1..10, 'a'..'z', "apple".."mango". This isn’t just syntactic sugar — a range is a real data type in Kotlin that can be iterated, checked for membership, and combined with various operators. Behind the scenes, there are two distinct concepts: range (a span of values) and progression (a sequence of values with a specific step). Understanding both opens up a more declarative way of thinking — instead of writing for (i = 0; i < n; i++), you just write for (i in 0 until n). This article covers the entire range and progression ecosystem in Kotlin, from basic usage to custom progressions and idiomatic patterns that make code cleaner.
Range vs Progression #
Before diving into details, it’s important to understand the fundamental difference between the two.
flowchart TD
A["Range"] --> B["A span between two values\n(start and endInclusive)\nExample: 1..10"]
A --> C["Membership can be checked\n5 in 1..10 → true"]
A --> D["Iterable if the type\nsupports it (Int, Long, Char)"]
E["Progression"] --> F["A sequence of values with a step\n(start, end, step)\nExample: 1..10 step 2"]
E --> G["Always iterable\n→ 1, 3, 5, 7, 9"]
E --> H["A subclass of Range\nwith step information"]| Range | Progression | |
|---|---|---|
| Definition | A span between two values | A sequence of values with a step |
| Example | 1..10 | 1..10 step 2 |
| Iterable | Only certain types | Always |
| Membership | in / !in | in / !in |
| Type | IntRange, CharRange, etc. | IntProgression, etc. |
Creating Ranges #
The .. Operator — Inclusive Ranges
#
The .. operator creates a range that includes both endpoints (inclusive on both sides).
val angka = 1..10 // 1, 2, 3, ..., 10 (10 included)
val huruf = 'a'..'z' // 'a', 'b', ..., 'z'
val teks = "apple".."mango" // String range (only for comparison, not iterable)
// Membership checks
println(5 in 1..10) // true
println(11 in 1..10) // false
println('e' in 'a'..'z') // true
// The resulting types
val r: IntRange = 1..10
val c: CharRange = 'a'..'z'
val l: LongRange = 1L..1000L
until — End-Exclusive Ranges
#
until creates a range that doesn’t include the end value. This is very common when working with array or list indices, because valid indices are 0 through size - 1.
val daftar = listOf("apel", "jeruk", "mangga", "durian")
// ANTI-PATTERN: using .. with size - 1, easy to get wrong
for (i in 0..daftar.size - 1) {
println(daftar[i])
}
// CORRECT: until is clearer and safer
for (i in 0 until daftar.size) {
println(daftar[i])
}
// Or even more idiomatic: use indices
for (i in daftar.indices) {
println("$i: ${daftar[i]}")
}
// until in common operations
val batasEksklusif = 0 until 100 // 0, 1, ..., 99 (100 not included)
println(99 in batasEksklusif) // true
println(100 in batasEksklusif) // false
0 until nis equivalent to0..n-1, but far safer — no underflow risk whenn = 0. With0..n-1whenn = 0, you get0..-1which produces an intuitively empty range. Always useuntilfor index-based ranges.
downTo — Descending Ranges
#
downTo creates a range that runs from a large value to a small value. A regular range (..) can’t be iterated backwards — 10..1 is a valid range but empty when iterated.
// ANTI-PATTERN: 10..1 can't be iterated (an empty range when looped)
for (i in 10..1) {
println(i) // never executed!
}
// CORRECT: use downTo for backwards iteration
for (i in 10 downTo 1) {
println(i) // 10, 9, 8, ..., 1
}
// There's no downTo with until — use downTo + manual stop
// or: (1..10).reversed()
val mundur = (1..10).reversed() // [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
// Countdown
for (i in 5 downTo 1) {
println("$i...")
}
println("Start!")
Progressions with step
#
step turns a range into a progression with a custom step — not one by one, but jumping a certain number of values.
// Even numbers from 0 to 20
for (i in 0..20 step 2) {
print("$i ") // 0 2 4 6 8 10 12 14 16 18 20
}
// Odd numbers
for (i in 1..19 step 2) {
print("$i ") // 1 3 5 7 9 11 13 15 17 19
}
// Combining downTo + step
for (i in 100 downTo 0 step 10) {
print("$i ") // 100 90 80 70 60 50 40 30 20 10 0
}
// step on a Char range
for (c in 'a'..'z' step 2) {
print("$c ") // a c e g i k m o q s u w y
}
// step must be positive — no negatives
// for going backwards with a step: downTo + step
for (i in 20 downTo 0 step 5) {
print("$i ") // 20 15 10 5 0
}
flowchart LR
A["1..10"] -->|"step 1 (default)"| B["1 2 3 4 5 6 7 8 9 10"]
A -->|"step 2"| C["1 3 5 7 9"]
A -->|"step 3"| D["1 4 7 10"]
E["10 downTo 1"] -->|"step 1"| F["10 9 8 7 6 5 4 3 2 1"]
E -->|"step 2"| G["10 8 6 4 2"]Ranges in Loops #
This is the most commonly seen use of ranges — as iteration control in for loops.
Standard Iteration #
// Simple loops
for (i in 1..5) print("$i ") // 1 2 3 4 5
for (i in 1 until 5) print("$i ") // 1 2 3 4
for (i in 5 downTo 1) print("$i ") // 5 4 3 2 1
// Loops with an index on a collection
val buah = listOf("apel", "jeruk", "mangga")
// ANTI-PATTERN: manual indexing loop
for (i in 0 until buah.size) {
println("$i: ${buah[i]}")
}
// CORRECT: use indices or withIndex
for (i in buah.indices) {
println("$i: ${buah[i]}")
}
for ((index, nama) in buah.withIndex()) {
println("$index: $nama")
}
repeat — An Alternative for Simple Loops #
When you only need to repeat something N times without caring about the index, repeat is more expressive than for (i in 0 until n).
// ANTI-PATTERN: a for loop with an unused variable
for (i in 0 until 5) {
println("Hello!")
}
// CORRECT: repeat makes the intent clearer
repeat(5) {
println("Hello!")
}
// repeat with an index if needed
repeat(5) { i ->
println("Iteration $i")
}
forEachIndexed vs Range Loops #
val produk = listOf("Laptop", "Mouse", "Keyboard", "Monitor")
// A range loop with an index
for (i in produk.indices) {
println("${i + 1}. ${produk[i]}")
}
// forEachIndexed: more idiomatic for collections
produk.forEachIndexed { index, nama ->
println("${index + 1}. $nama")
}
// Both are equivalent — choose the clearer one for the context
// forEachIndexed suits functional contexts
// range loops suit when you need flow control (break, continue)
forEachandforEachIndexeddon’t supportbreakorcontinue— they use lambdas. If you need to stop a loop mid-way or skip specific iterations, use a regularforloop with a range, or usefirst { },find { }, orany { }as appropriate.
Ranges in when
#
Ranges can be used as conditions in a when expression — one of the features that makes Kotlin’s when far more expressive than Java’s switch.
// Grade classification
fun klasifikasiNilai(nilai: Int): String = when (nilai) {
in 90..100 -> "A — Excellent"
in 80 until 90 -> "B — Good"
in 70 until 80 -> "C — Fair"
in 60 until 70 -> "D — Poor"
in 0 until 60 -> "E — Fail"
else -> "Invalid score"
}
// BMI classifier
fun kategoriBMI(bmi: Double): String = when {
bmi < 18.5 -> "Underweight"
bmi in 18.5..24.9 -> "Normal"
bmi in 25.0..29.9 -> "Overweight"
bmi >= 30.0 -> "Obese"
else -> "Invalid"
}
// Age categories
fun kategoriUsia(usia: Int): String = when (usia) {
in 0..12 -> "Child"
in 13..17 -> "Teenager"
in 18..25 -> "Young Adult"
in 26..59 -> "Adult"
in 60..Int.MAX_VALUE -> "Senior"
else -> "Invalid"
}
// Discounts based on purchase quantity
fun hitungDiskon(jumlah: Int): Double = when (jumlah) {
in 1..4 -> 0.0
in 5..9 -> 0.05
in 10..19 -> 0.10
in 20..49 -> 0.15
in 50..Int.MAX_VALUE -> 0.20
else -> 0.0
}
Ranges for Validation #
One of the most practical range uses is input validation — far cleaner than combining >= and <=.
// ANTI-PATTERN: validation with explicit comparisons
fun validasiUsia(usia: Int): Boolean {
return usia >= 0 && usia <= 150
}
fun validasiSuhu(suhu: Double): Boolean {
return suhu >= -273.15 && suhu <= 1000.0
}
// CORRECT: use ranges for validation
fun validasiUsia(usia: Int): Boolean = usia in 0..150
fun validasiSuhu(suhu: Double): Boolean = suhu in -273.15..1000.0
// Validation with !in for invalid cases
fun validasiPort(port: Int): Boolean = port in 1..65535
fun isPortInvalid(port: Int): Boolean = port !in 1..65535
// Character validation
fun isHuruf(c: Char): Boolean = c in 'a'..'z' || c in 'A'..'Z'
fun isAngka(c: Char): Boolean = c in '0'..'9'
fun isAlphanumeric(c: Char): Boolean = isHuruf(c) || isAngka(c)
// Richer validation functions
data class RentangValid(val min: Int, val max: Int) {
val range = min..max
fun valid(nilai: Int) = nilai in range
fun pesanError(nilai: Int) = "Value $nilai must be between $min and $max"
}
val rentangUsia = RentangValid(0, 120)
val rentangPort = RentangValid(1, 65535)
fun prosesInput(usia: Int, port: Int) {
require(rentangUsia.valid(usia)) { rentangUsia.pesanError(usia) }
require(rentangPort.valid(port)) { rentangPort.pesanError(port) }
// continue processing...
}
Ranges on Non-Numeric Types #
Ranges aren’t limited to numbers. Kotlin supports ranges on Char and String (for comparison), as well as any type implementing Comparable.
Char Ranges #
// Iterating letters
for (c in 'A'..'Z') {
print(c) // ABCDEFGHIJKLMNOPQRSTUVWXYZ
}
// Alphabet generators
val hurufKecil = ('a'..'z').toList()
// ['a', 'b', 'c', ..., 'z']
val hurufBesar = ('A'..'Z').toList()
// ['A', 'B', 'C', ..., 'Z']
// Digits as Chars
val digitChar = ('0'..'9').toList()
// ['0', '1', '2', ..., '9']
// A simple password generator
val karakter = ('a'..'z') + ('A'..'Z') + ('0'..'9')
fun buatPasswordAcak(panjang: Int): String {
return (1..panjang)
.map { karakter.random() }
.joinToString("")
}
// Character validation with ranges
fun isVokal(c: Char): Boolean = c.lowercaseChar() in "aeiou" // a trick with String
fun isKonsonan(c: Char): Boolean = c in 'a'..'z' && !isVokal(c)
String Ranges and Comparables #
// String ranges: membership can be checked but not iterated
val rentangBuah = "apel".."mangga"
println("jeruk" in rentangBuah) // true (lexicographic comparison)
println("semangka" in rentangBuah) // false ('s' > 'm')
// Comparable ranges: any type implementing Comparable
data class Versi(val major: Int, val minor: Int) : Comparable<Versi> {
override fun compareTo(other: Versi): Int {
return if (major != other.major) major - other.major
else minor - other.minor
}
}
val versiDidukung = Versi(2, 0)..Versi(4, 9)
println(Versi(3, 5) in versiDidukung) // true
println(Versi(1, 9) in versiDidukung) // false
println(Versi(5, 0) in versiDidukung) // false
// Dates with LocalDate (kotlinx-datetime)
// val rentangLiburan = LocalDate(2024, 12, 24)..LocalDate(2025, 1, 1)
// val hariIni = LocalDate.now()
// val sedangLibur = hariIni in rentangLiburan
Operations on Ranges and Progressions #
Ranges and progressions have several useful utility functions.
val range = 1..20
// Conversion to a List
val list = range.toList() // [1, 2, 3, ..., 20]
val listStep = (1..20 step 3).toList() // [1, 4, 7, 10, 13, 16, 19]
// Range properties
println(range.first) // 1
println(range.last) // 20
println(range.step) // 1 (IntProgression)
val prog = 1..20 step 3
println(prog.first) // 1
println(prog.last) // 19 (not 20, because 20 isn't in the progression)
println(prog.step) // 3
// isEmpty: reversed ranges are always empty
println((5..1).isEmpty()) // true
println((1..5).isEmpty()) // false
// contains: the same as `in`
println(range.contains(10)) // true
println(10 in range) // true (equivalent)
// reversed
val rangeReversed = (1..10).reversed() // [10, 9, ..., 1]
// sum, average, count on progressions
val jumlah = (1..100).sum() // 5050
val rata = (1..10).average() // 5.5
val banyak = (1..20 step 2).count() // 10
// any, all, none
val adaYangBesar = (1..100).any { it > 90 } // true
val semuaPositif = (1..100).all { it > 0 } // true
val tidakAdaNol = (1..100).none { it == 0 } // true
Custom Progressions #
Kotlin lets you create your own progression types for custom types by implementing Iterable and the rangeTo operator.
// Example: a progression for a simple date
data class Tanggal(val hari: Int) : Comparable<Tanggal> {
override fun compareTo(other: Tanggal) = hari - other.hari
operator fun plus(n: Int) = Tanggal(hari + n)
}
class TanggalProgression(
override val start: Tanggal,
override val endInclusive: Tanggal,
val langkah: Int = 1
) : Iterable<Tanggal>, ClosedRange<Tanggal> {
override fun iterator(): Iterator<Tanggal> = object : Iterator<Tanggal> {
var current = start
override fun hasNext() = current <= endInclusive
override fun next(): Tanggal {
val result = current
current = current + langkah
return result
}
}
}
// The rangeTo operator for Tanggal
operator fun Tanggal.rangeTo(lain: Tanggal) = TanggalProgression(this, lain)
// An infix extension for step
infix fun TanggalProgression.langkah(n: Int) =
TanggalProgression(start, endInclusive, n)
// Usage
val awal = Tanggal(1)
val akhir = Tanggal(31)
for (tgl in awal..akhir) {
println("Day ${tgl.hari}")
}
for (tgl in awal..akhir langkah 7) {
println("Week ${(tgl.hari - 1) / 7 + 1}: day ${tgl.hari}")
}
Idiomatic Patterns #
A few summarized range usage patterns frequently appearing in production code.
Sampling and Data Splitting #
val data = (1..1000).toList()
// Take a sample of every N elements
val sampel = data.filterIndexed { index, _ -> index % 10 == 0 }
// [1, 11, 21, 31, ..., 991] (every 10th element)
// Split data into batches
val batch = data.chunked(100)
// [[1..100], [101..200], ..., [901..1000]]
// Take the first 10% and the last 10%
val awal10persen = data.take(data.size / 10)
val akhir10persen = data.takeLast(data.size / 10)
Fibonacci with Ranges #
// A Fibonacci generator using a progression
fun fibonacci(n: Int): List<Long> {
if (n <= 0) return emptyList()
if (n == 1) return listOf(1L)
val hasil = mutableListOf(1L, 1L)
for (i in 2 until n) {
hasil.add(hasil[i - 1] + hasil[i - 2])
}
return hasil
}
println(fibonacci(10))
// [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
Clamping — Limiting a Value Within a Range #
// Clamp: make sure a value is within bounds
fun Int.coerceIn(min: Int, max: Int) = when {
this < min -> min
this > max -> max
else -> this
}
// Kotlin already has a built-in coerceIn!
val nilai = 150
val dibatasi = nilai.coerceIn(0, 100) // 100
val normal = 75.coerceIn(0, 100) // 75
val negatif = (-5).coerceIn(0, 100) // 0
// coerceIn also accepts a range directly
val range = 0..100
val hasilClamp = nilai.coerceIn(range) // 100
// coerceAtLeast and coerceAtMost
val minSaja = (-5).coerceAtLeast(0) // 0
val maxSaja = 150.coerceAtMost(100) // 100
Multiplication Tables #
// Multiplication tables with nested ranges
fun cetakTabelPerkalian(batas: Int = 10) {
for (i in 1..batas) {
for (j in 1..batas) {
print("${(i * j).toString().padStart(4)}")
}
println()
}
}
// A functional version
val tabel = (1..10).map { i ->
(1..10).map { j -> i * j }
}
Comparison with the Java Approach #
Kotlin ranges are far more expressive than conventional Java loops.
// Java style (valid in Kotlin but not idiomatic)
// ANTI-PATTERN:
var i = 0
while (i < 10) {
println(i)
i++
}
// ANTI-PATTERN:
for (i in 0..9) { // but the intent is 0 to 9
println(i)
}
// CORRECT — idiomatic Kotlin:
for (i in 0 until 10) { // clear: 0 up to and excluding 10
println(i)
}
repeat(10) { i -> // if it's just iteration without complex logic
println(i)
}
// Range-based conditions — Java needs verbose &&
// ANTI-PATTERN:
if (nilai >= 80 && nilai <= 100) println("Good")
// CORRECT:
if (nilai in 80..100) println("Good")
flowchart TD
A{What needs to\nbe done?} --> B["Forward iteration\n1 at a time"]
A --> C["Backward iteration"]
A --> D["Iteration with\njumps"]
A --> E["Membership check\nvalue within a range"]
A --> F["Value classification\nin when"]
A --> G["Bound validation\nof values"]
B --> B1["for (i in start..end)\nor\nfor (i in start until end)"]
C --> C1["for (i in end downTo start)"]
D --> D1["for (i in start..end step n)\nor\nfor (i in end downTo start step n)"]
E --> E1["nilai in start..end\nnilai !in start..end"]
F --> F1["when (x) { in a..b -> ... }"]
G --> G1["nilai.coerceIn(min, max)\nrequire(nilai in min..max)"]Summary #
..creates an inclusive range on both ends (1..10→ 1 through 10 included).untilis end-exclusive (0 until 10→ 0 through 9). Useuntilfor index-based ranges — safer than0..size-1.downTofor backward iteration (10 downTo 1). A regular range10..1doesn’t produce an error but produces an empty range when iterated — a common trap.stepturns a range into a progression with a custom step (1..20 step 3→ 1, 4, 7, …, 19). Can be combined withdownTo.inand!infor membership checks. Far cleaner thanx >= a && x <= b— use these for validation and conditions.when+ ranges is a very expressive replacement for switch-case value classification:in 80..100 -> "Good".repeat(n) { }for iteration without needing an index — clearer intent thanfor (i in 0 until n)that never usesi.coerceInclamps a value within a range;coerceAtLeastandcoerceAtMostfor one-sided limits. Use these instead of manual if-else clamping.- Ranges can be created on any
Comparabletype —Char,String, or a custom class implementingComparable. Custom progressions can be created by implementingIterableand therangeTooperator.indiceson a List/Array is a shortcut for0 until size— always use this instead of calculating ranges manually.- Progressions support collection operations like
sum(),average(),count(),any {},all {},toList()— no manual conversion to a List needed first.