skip to content

How can you build a range over a custom Comparable type, and what does ClosedRange give you that an iterable progression does not?

level: seniorimportance: nice to knowfreq 30%

answer

  1. Any Comparable<T> gets .. via stdlib rangeTo extension
  2. Result is ClosedRange<T>: contains via compareTo, no iteration
  3. ClosedRange is NOT Iterable — no general successor
  4. Iterate by defining your own iterator/step (e.g. plusDays(1))
  5. ..< -> rangeUntil -> OpenEndRange (endExclusive)

basics

~20 s

If your type implements Comparable, you get .. for free via the rangeTo extension. That builds a closed range you can test with in, but you cannot loop over it unless you also define how to step through values.

solid answer

~50 s

Any `T : Comparable<T>` automatically supports `a..b` through the stdlib extension `operator fun <T: Comparable<T>> T.rangeTo(that: T): ClosedRange<T>`. The result is a `ClosedRange<T>` exposing `start`, `endInclusive`, `isEmpty()`, and an `operator fun contains` defined purely by comparisons (`value >= start && value <= endInclusive`). So `LocalDate.of(2026,1,1)..LocalDate.of(2026,12,31)` works and `someDate in thatRange` is valid — **membership works for any comparable**. What you do **not** get is iteration: `ClosedRange` is not `Iterable`, because Kotlin can't know how to advance from one `LocalDate` to the next. Iteration requires an `IntProgression`-style type with a defined `step` and `iterator()`; that exists only for the discrete built-ins (`Int`, `Long`, `Char`). For a custom type you can iterate, you implement `Iterable` yourself (e.g., a `ClosedRange<LocalDate>` extension that yields `Iterator` stepping by one day). There is also `..<` / `OpenEndRange` and `rangeUntil` for half-open custom ranges.

code

kotlin · 4 lines
kotlin
import java.time.LocalDate
val q = LocalDate.of(2026,1,1)..LocalDate.of(2026,3,31)
println(LocalDate.of(2026,2,14) in q)  // true (membership works)
// for (d in q) {}  // would NOT compile: ClosedRange<LocalDate> is not Iterable

go deeper

for a junior

Knows .. works on numbers and that in tests membership.

for a middle

Knows Comparable types support .. and that the result supports in.

for a senior

Explains ClosedRange-vs-progression: membership for free, iteration only with a defined step, and how to implement an iterable date range.

for a principal

Designs domain range types intentionally — when to expose only membership vs a stepped progression, and the successor-function tradeoff behind keeping ClosedRange non-Iterable.

## .. for any Comparable The standard library defines: ```kotlin public operator fun <T : Comparable<T>> T.rangeTo(that: T): ClosedRange<T> ``` So **any** type implementing `Comparable<T>` gets the `..` operator automatically: ```kotlin import java.time.LocalDate val year = LocalDate.of(2026, 1, 1)..LocalDate.of(2026, 12, 31) val inYear = LocalDate.of(2026, 6, 22) in year // true ``` `LocalDate` implements `Comparable<LocalDate>`, so this compiles with no extra code. ## What ClosedRange gives you The returned `ClosedRange<T>` interface provides: - `start: T` and `endInclusive: T` - `operator fun contains(value: T): Boolean` — implemented as `value >= start && value <= endInclusive` using `compareTo` - `fun isEmpty(): Boolean` — `start > endInclusive` That is enough for **membership** (`in`/`!in`) and emptiness, all via comparison. No iteration is implied. ## Why you can't iterate a generic ClosedRange `ClosedRange<T>` is deliberately **not** `Iterable<T>`. To iterate you must answer "what is the *next* value after this one?" — and there is no general successor for an arbitrary `Comparable`. What is the next `LocalDate`? Next day? Next second? It is undefined. So: ```kotlin for (d in year) { ... } // COMPILE ERROR: ClosedRange is not Iterable ``` Iteration is supported only by **progressions** (`IntProgression`, `LongProgression`, `CharProgression`) where a discrete `step` exists. ## Making a custom type iterable You can opt in by providing an `Iterable` (or an `iterator()` operator) that defines the step: ```kotlin class DateProgression( override val start: LocalDate, override val endInclusive: LocalDate, ) : ClosedRange<LocalDate>, Iterable<LocalDate> { override fun iterator(): Iterator<LocalDate> = object : Iterator<LocalDate> { private var current = start override fun hasNext() = current <= endInclusive override fun next(): LocalDate { val r = current current = current.plusDays(1) return r } } } operator fun LocalDate.rangeTo(other: LocalDate) = DateProgression(this, other) for (d in LocalDate.of(2026,1,1)..LocalDate.of(2026,1,3)) println(d) ``` Here your own `rangeTo` extension shadows the generic one and returns an iterable type, so both `in` (membership) and `for` (iteration by one day) work. ## Half-open custom ranges Symmetrically, `..<` maps to `rangeUntil` and yields an `OpenEndRange<T>` (with `start` and `endExclusive`). The generic comparable extension exists for it too, so `a..<b` works on custom comparables for membership. ## Key APIs/keywords - `Comparable<T>`, `compareTo` - `operator fun rangeTo` / `rangeUntil` (generic comparable extensions) - `ClosedRange<T>` (`start`, `endInclusive`, `contains`, `isEmpty`) - `OpenEndRange<T>` (`endExclusive`) - `IntProgression`/`step`/`iterator()` for discrete iteration

  • Why can't you write for (d in startDate..endDate) out of the box?
    The generic `..` returns a ClosedRange, which is not Iterable — there is no defined successor for an arbitrary Comparable. You must supply your own iterator/step.
  • What is the minimal requirement for a..b to compile?
    The type must implement Comparable<T>; the stdlib provides the rangeTo extension that builds a ClosedRange from any two comparable values.

A ClosedRange is like knowing the start and end of a road (you can ask 'am I on it?'), but to walk it you also need to know how big each step is.

saying these in an interview costs you the question

  • Assuming a date/BigDecimal range is automatically iterable
  • Confusing ClosedRange (comparison-only) with IntProgression (iterable)
  • Thinking .. requires implementing rangeTo manually for every type
  • Believing membership needs iteration
  • Not knowing OpenEndRange/rangeUntil exists for custom types

context