Skip to content

← Shape Your Data: Structs, Enums, Pattern Matching step 10 of 26

Medium Primitives

Binding values with @ and or-patterns

Repair the starter so it labels each number with its bucket and value.

pub fn bucket(values: Vec<i64>) -> Vec<String>
Value Label
At most 9, including all negative numbers "small:{value}"
10 through 99, inclusive "mid:{value}"
At least 100 "large:{value}"

Preserve input order and duplicates. An empty vector produces an empty vector. Every i64 is valid, including i64::MIN and i64::MAX. Format the integer in decimal without spaces or extra punctuation.

For example, [0, 50, 1000, -5] becomes ["small:0", "mid:50", "large:1000", "small:-5"].

The starter already classifies and formats the values. Fix the or-pattern in payload, keeping the supplied Bucket enum and helper functions.

Testing and binding with @

name @ pattern tests a pattern and gives the matched value a name. The starter uses n @ ..=9 to match any integer at most nine and bind it to n. This is an upper-bounded inclusive range: nine is included, and no explicit lower bound is needed.

Here is a complete example with a different set of ranges:

fn describe(value: i64) -> String {
    match value {
        n @ 1..=5 => format!("in range: {n}"),
        n => format!("outside: {n}"),
    }
}

fn main() {
    assert_eq!(describe(3), "in range: 3");
    assert_eq!(describe(8), "outside: 8");
}

The final n binds any remaining value, making the match exhaustive. Use @ when the inner pattern adds a useful test or destructuring step. n @ _ adds nothing over a plain n.

One binding across several alternatives

An or-pattern joins alternatives with |. They share one arm body, so each alternative must bind the same names with compatible types and the same binding modes. The starter mixes n and m: whichever alternative matches, the arm body needs one consistently named binding.

This example combines two variants that both contain an integer:

enum Reading {
    Sensor(i64),
    Manual(i64),
}

fn value(reading: &Reading) -> i64 {
    match reading {
        Reading::Sensor(n) | Reading::Manual(n) => *n,
    }
}

fn main() {
    assert_eq!(value(&Reading::Sensor(12)), 12);
    assert_eq!(value(&Reading::Manual(-3)), -3);
}

Matching &Reading this way borrows its contents, so n has type &i64. Dereferencing it copies the integer. The same reasoning applies to the starter's payload(bucket: &Bucket).

Reading the compiler's messages

Start with the earliest binding error, then compile again after fixing it. One mistake can cause additional messages about later uses of a binding.

Problem Typical diagnostic
A name is absent from an alternative E0408: variable is not bound in all patterns
The same name has different types E0308: mismatched types
The same name uses different modes, such as ref n and n E0409: variable is bound inconsistently
A name appears twice within one pattern, such as (n, n) E0416: identifier is bound more than once

Repeating a name within one pattern is not an equality test. If two fields must be equal, bind them separately and compare them in a guard.

Borrowing still depends on context

@ does not force a move. Like other bindings, it can copy, move or borrow according to the type and binding mode. An i64 bound by value is copied. A String bound by value is moved, while a reference pattern or an explicit ref binding can borrow it. Adding @ does not allow moving owned data out through a shared reference.

Alternatives can also appear inside another pattern. For example, Some(1 | 4 | 9) matches the same values as Some(1) | Some(4) | Some(9). Choose the form that makes the shared structure easiest to read.

Submissions must compile, pass the tests and pass clippy -D warnings.