Ranges & Progressions

.., ..<, downTo, step, membership checks, ranges over any Comparable, and the Progression underneath.

.. / ..< downTo step in checks Comparable ranges Progression
๐Ÿ“

Range Basics: .. and ..<

A range is a genuine object โ€” an instance of IntRange, CharRange, and so on โ€” not just loop syntax. 1..5 is sugar for 1.rangeTo(5), an operator function (see Functions & Lambdas) that constructs the range object; you can hold it in a variable, pass it to a function, or query it, entirely separately from ever looping over it.

Inclusive vs exclusive end .. / ..<
// .. is inclusive on both ends: 1, 2, 3, 4, 5
val inclusive = 1..5
println(inclusive.toList())  // [1, 2, 3, 4, 5]

// ..< excludes the end (Kotlin 1.7+) โ€” reads like a half-open interval
val exclusive = 0..<5
println(exclusive.toList())  // [0, 1, 2, 3, 4]

// The range is a real object: query it without iterating at all
println(inclusive.first)   // 1
println(inclusive.last)    // 5
println(inclusive.isEmpty())  // false
๐Ÿ’ก
Prefer 0..<n over the older 0 until n in new Kotlin 2.x code. Both behave identically โ€” until is still available and still idiomatic in existing codebases โ€” but ..< is shorter and visually mirrors the [0, n) half-open notation directly.
๐Ÿ”ฝ

downTo & step

.. and ..< only ever count upward by 1. downTo builds a descending range, and step โ€” an infix function, same family as to from Functions & Lambdas โ€” changes the increment on any range, ascending or descending.

Counting backward and by custom increments downTo / step
// downTo: descending range
for (i in 5 downTo 1) print("$i ")  // 5 4 3 2 1
println()

// step: changes the increment on an ascending range
for (i in 0..10 step 2) print("$i ")  // 0 2 4 6 8 10
println()

// step also works combined with downTo
for (i in 10 downTo 0 step 3) print("$i ")  // 10 7 4 1
โš ๏ธ
step always requires a positive value, even when used with downTo โ€” the direction comes entirely from whether you wrote ../..< or downTo, not from the sign of the step. 10 downTo 0 step -3 throws IllegalArgumentException at runtime, not a compile error, so it's easy to miss until it's actually executed.
โœ…

in Checks

Testing whether a value falls inside a range doesn't iterate anything โ€” x in range desugars to range.contains(x), which for a numeric range is a single pair of comparisons (x >= first && x <= last), regardless of how large the range is. This is what makes ranges a natural fit for when branches and bounds checks, covered from the control-flow side in Basics.

Constant-time membership, any range size in
val validAges = 0..120
val hugeRange = 1..1_000_000_000

println(45 in validAges)     // true
println(150 in validAges)    // false
println(999_999_999 in hugeRange)  // true โ€” instant, not a billion-step scan

// Reads naturally as a when branch condition
fun classify(age: Int) = when (age) {
    in 0..12  -> "child"
    in 13..19 -> "teenager"
    else       -> "adult"
}
๐Ÿ”ค

Ranges over Chars & Comparables

Ranges aren't limited to numbers. 'a'..'z' builds a CharRange, iterable exactly like an IntRange. More generally, .. works for any type implementing Comparable<T> โ€” the result is a ClosedRange<T> that supports membership checks and bounds, though only types with a defined "next value" (like Int or Char) can actually be iterated with a for loop.

CharRange and a custom Comparable range Comparable<T>
// CharRange: iterable, just like IntRange
for (c in 'a'..'e') print(c)  // abcde
println()

println('m' in 'a'..'z')  // true

// Any Comparable can form a ClosedRange, even without iteration support
data class Version(val major: Int, val minor: Int) : Comparable<Version> {
    override fun compareTo(other: Version): Int =
        compareValuesBy(this, other, { it.major }, { it.minor })
}

val supportedRange = Version(1, 0)..Version(2, 5)
println(Version(1, 8) in supportedRange)  // true: membership works, no loop needed
// for (v in supportedRange) { }  // ERROR: Version has no defined "next" step
โ„น๏ธ
The distinction that matters: any Comparable<T> gets range construction and membership checks for free, but only types the standard library specifically supports for stepping (Int, Long, Char) can be iterated with for. A custom type's range is genuinely useful for bounds checking even without ever being looped over.
๐Ÿ”ข

Progressions

Under the hood, 1..10, 10 downTo 1, and 1..10 step 2 are all instances of IntProgression โ€” a range is really a special case of a progression with a step of exactly 1. Every progression implements Iterable<Int>, which is the whole reason for (i in 1..10) works at all: it's the same iteration mechanism covered generally in Collections, just specialized for a lazily-computed sequence of numbers instead of a stored list.

first, last, step โ€” the three defining fields IntProgression
val progression = 1..11 step 3

println(progression.first)  // 1
println(progression.last)   // 10 โ€” NOT 11: last is the final value the step actually lands
                             // on (1, 4, 7, 10), even though the range's written end is 11
println(progression.step)   // 3
println(progression.toList())  // [1, 4, 7, 10]

// A progression allocates no backing array โ€” each element is
// computed on demand from first + step, exactly like generateSequence
๐Ÿ’ก
Because a progression computes elements on the fly rather than storing them, iterating 1..1_000_000_000 in a for loop allocates essentially nothing โ€” no billion-element array exists anywhere. This is what makes ranges safe to use freely for loop bounds no matter how large, unlike materializing the same span as a List with .toList() would be.
๐Ÿ“‹

Quick Reference

Concept Syntax Notes
Inclusive range1..51, 2, 3, 4, 5
Exclusive end0..<50, 1, 2, 3, 4 โ€” Kotlin 1.7+, prefer over `until`
Descending range5 downTo 15, 4, 3, 2, 1
Custom increment0..10 step 2step is always positive, even with downTo
Membership checkx in rangeConstant time for numeric ranges, no iteration
Char range'a'..'z'Iterable, same as IntRange
Comparable rangea..b for any Comparable<T>Supports membership; iterable only if T supports stepping
Underlying typeIntProgression / CharProgressionDefined by first, last, step; range is step = 1
No backing storagefor (i in 1..1_000_000_000)Elements computed on demand, not materialized