skip to content

What are the operator-precedence and edge-case pitfalls when writing range expressions like `0..n-1`, `1..count step 2`, or negative steps?

level: seniorimportance: should knowfreq 33%

answer

  1. Precedence: arithmetic > range > infix(step/downTo)
  2. `0..n-1` == `0..(n-1)` (safe)
  3. step <= 0 throws at RUNTIME
  4. `(1..5).reversed()` == 5..1 step -1
  5. No implicit Int->Long widening in ranges

basics

~20 s

.. binds tighter than +/- is a myth — actually +/- bind tighter, so 0..n-1 works as expected. But .. binds tighter than infix step/downTo calls, and a wrong or negative step throws at runtime, not compile time.

solid answer

~50 s

Precedence matters. The range operators `..`/`..<` sit **above** additive (`+`/`-`) and multiplicative operators, but **below**? No — concretely: arithmetic like `n - 1` is evaluated first, so `0..n-1` means `0..(n-1)` (correct). However `..` binds **tighter than infix functions**, so `1..10 step 2` parses as `(1..10) step 2` — which is what you want — and `1..2 + 3` parses as `1..(2 + 3)` = `1..5`. Pitfalls: (1) negative or zero `step` throws `IllegalArgumentException` at runtime. (2) `downTo` with a `step` still needs a positive step. (3) For reversed iteration, `(1..5).reversed()` gives an `IntProgression` 5..1 step -1 — different from `5 downTo 1` only in how you wrote it. (4) Mixing types (`1..5L`) won't compile; promote explicitly. (5) `Int` overflow: `Int.MAX_VALUE - 1 .. Int.MAX_VALUE` is fine, but stepping past `MAX_VALUE` is guarded by the progression's `last` computation, which avoids overflow.

code

kotlin · 5 lines
kotlin
val n = 5
println((0..n - 1).toList())   // [0, 1, 2, 3, 4]
println((1..10 step 2).toList())// [1, 3, 5, 7, 9]
println((1..5).reversed().toList()) // [5, 4, 3, 2, 1]
// val bad = 1..5L              // compile error: Int vs Long

go deeper

for a junior

Writes 0..n-1 correctly and knows it includes 0 through n-1.

for a middle

Explains arithmetic binds tighter than .., and .. tighter than step, so common expressions parse as intended.

for a senior

Knows step validation is runtime, reversed() vs downTo, and the no-implicit-widening rule.

for a principal

Reasons about overflow-safe last computation, empty-range guards, and when explicit parentheses prevent precedence surprises in larger expressions.

## Operator precedence Kotlin's precedence (high to low, relevant slice): 1. Multiplicative `* / %` 2. Additive `+ -` 3. **Range `.. ..<`** 4. Infix functions (`step`, `downTo`, `until`, `and`, custom infix) 5. Comparisons, equality, `&&`, `||` So arithmetic is evaluated **before** the range, and the range **before** infix functions: ```kotlin 0..n - 1 // == 0..(n - 1) arithmetic first 1..2 + 3 // == 1..(2 + 3) == 1..5 1..10 step 2 // == (1..10) step 2 range first, then step 0..<n + 1 // == 0..<(n + 1) ``` This is why `0..list.size - 1` and `1..count step 2` read the way you expect without parentheses. When in doubt, parenthesize — it's free. ## The `step` traps `step` must be **strictly positive**; the direction is set by `..`/`downTo`. A non-positive value throws **at runtime**, not compile time: ```kotlin 0..10 step 0 // IllegalArgumentException: Step must be positive, was: 0. 0..10 step -2 // IllegalArgumentException ``` There is no compile-time guarantee, so validate computed steps before use. ## Reversing vs `downTo` ```kotlin (1..5).reversed() // IntProgression: 5, 4, 3, 2, 1 (step -1) 5 downTo 1 // same values ``` `reversed()` on a progression returns a new progression with negated step and swapped ends — handy when you already have a range. Note `(1..10 step 2).reversed()` is `10 downTo 2 step 2`? Actually it reverses the *actual* elements: `10, 8, ... 2` — it reverses around the corrected `last` (10), so know your `last`. ## Type and overflow edges - **No implicit widening:** `1..5L` does not compile (Int vs Long). Use `1L..5L`. - **`Char` ranges:** `'a'..'z'` works; `'a'..5` does not. - **Overflow safety:** progressions compute `last` carefully to avoid Int overflow when stepping near `Int.MAX_VALUE`/`MIN_VALUE`. Iteration stops at the corrected `last` rather than wrapping around. - **Empty results:** `0..-1`, `3..<3`, `5..1` are all empty and iterate zero times — guard if you expected at least one element. ## Practical rules of thumb - Trust `0..n-1` and `0..<n` to mean what they look like. - Always pass a positive, validated `step`. - Prefer `list.indices` / `0..<list.size` over manual `0..size-1`. - Parenthesize when combining range ops with comparisons or `?:`.

  • Does `0..n-1` need parentheses around `n-1`?
    No. Additive ops bind tighter than `..`, so it already parses as `0..(n-1)`. Parens are optional for clarity.
  • When is a bad `step` caught — compile or runtime?
    Runtime: a zero/negative step throws IllegalArgumentException. The compiler does not check the value.
  • How does `(1..5).reversed()` differ from `5 downTo 1`?
    They produce the same elements; `reversed()` builds a new progression with negated step from an existing range.

saying these in an interview costs you the question

  • Wrapping `n-1` in parens believing `..` binds tighter than `-`
  • Expecting a negative `step` to be rejected at compile time
  • Assuming `1..5L` compiles via implicit widening
  • Forgetting `0..-1` / `3..<3` are empty

context