Skip to content

← Ground Rules: Values, Types, Control Flow step 17 of 24

Easy Primitives

Ranges: .., ..=, and range patterns

Bucket each score into a band.

pub fn bucket(scores: &[i32]) -> Vec<String>

0..=33 is "low", 34..=66 is "mid", 67..=100 is "high", and anything outside 0..=100 is "invalid".

The boundaries are the whole test: 33 is low, 34 is mid, 66 is mid, 67 is high, −1 and 101 are invalid.

The starter is correct and fails clippy three times for the same reason.

Two range syntaxes, and where the off-by-ones live

0..5     // exclusive: 0, 1, 2, 3, 4          type Range<i32>
0..=5    // inclusive: 0, 1, 2, 3, 4, 5       type RangeInclusive<i32>
..5      // RangeTo          — slicing only
5..      // RangeFrom
..       // RangeFull

a..b is the default because it composes with lengths: 0..v.len() is exactly the valid indices, and a..b has b - a elements. a..=b is what you want whenever the upper bound is a real value in the domain rather than a limit — “scores up to and including 100”, “the loop runs from 1 to n”.

Choosing wrongly here is the classic off-by-one, and Rust at least makes the choice visible in the syntax instead of hiding it in a < versus <=.

Ranges as values and ranges as patterns are different things

This distinction causes persistent confusion, so name it once:

A range value is a struct you can iterate or ask questions of.

for i in 0..5 { … }              // iterate
(0..=100).contains(&score)       // query
let slice = &v[1..4];            // index with it

A range pattern appears in a match arm and matches a value inside the interval.

match score {
    0..=33   => "low",
    34..=66  => "mid",
    67..=100 => "high",
    _        => "invalid",
}

The second one is not iterating anything and there is no range struct at runtime. It compiles to the same comparisons you would have written by hand, and it reads as a table. Exclusive range patterns (0..34) stabilised in Rust 1.80 and work here too.

match requires exhaustiveness: the arms must cover every possible value of the scrutinee, and the compiler checks it. _ is the wildcard arm that catches everything left over. Track 4 covers match in full; for this problem you only need the table shape.

The lint: manual_range_contains

The starter writes s >= 0 && s <= 33, three times over. Clippy’s manual_range_contains is on by default and pushes you to (0..=33).contains(&s) — or, better here, to a match with range patterns, which expresses the whole partition as one thing instead of three overlapping-looking conditions.

Two range traps worth carrying with you

A backwards range is empty, not reversed.

for i in 5..0 { … }    // never executes. No error. No warning at runtime.

If you want to count down, say for i in (0..5).rev(). Clippy’s reversed_empty_ranges is on by default and catches the literal case (5..0), but it cannot see a range whose ends came from variables — so this is one to keep in your head.

..= with an upper bound of MAX is fine, .. is not. 0..=u8::MAX iterates all 256 values; 0..u8::MAX quietly stops at 254.

Loading visualization…