What are the operator-precedence and edge-case pitfalls when writing range expressions like `0..n-1`, `1..count step 2`, or negative steps?
answer
- Precedence: arithmetic > range > infix(step/downTo)
- `0..n-1` == `0..(n-1)` (safe)
- step <= 0 throws at RUNTIME
- `(1..5).reversed()` == 5..1 step -1
- 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 sPrecedence 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 linesval 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 Longgo deeper
Writes 0..n-1 correctly and knows it includes 0 through n-1.
Explains arithmetic binds tighter than .., and .. tighter than step, so common expressions parse as intended.
Knows step validation is runtime, reversed() vs downTo, and the no-implicit-widening rule.
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