Skip to content

← Closures and Iterators step 14 of 28

Medium End-to-End

flat_map: expand a range spec like "1-3,7,10-12"

Expand comma-separated numbers and inclusive ranges into one vector.

pub fn expand(spec: Vec<String>) -> Vec<i64>

Process the strings in input order, then each string's comma-separated fragments from left to right. Preserve duplicates. Empty input produces an empty vector.

Fragment rules

Trim Unicode whitespace around each fragment. A valid fragment is either a single non-negative i64 or two such bounds separated by a hyphen. Trim whitespace around each bound too. A range includes both endpoints and must have its lower bound less than or equal to its upper bound.

Use standard decimal i64 parsing: leading zeros and a leading + are accepted. Spaces within a number, decimal points, underscores and values above i64::MAX are invalid. A leading minus sign is interpreted as a separator with a missing lower bound, so negative numbers are not supported. The supplied parser accepts "0--0": the upper bound -0 parses as zero.

Skip invalid fragments, including empty ones and reversed ranges. An invalid fragment does not discard the rest of its input string. Inputs used for this task have a total expansion small enough to fit in memory.

Input Output
["1-3,7,10-12"] [1, 2, 3, 7, 10, 11, 12]
["5-1"] []
["3-5,x-y,9"] [3, 4, 5, 9]
[" +02 - 4 , 4", "7"] [2, 3, 4, 4, 7]
["-3,7-,1 2,,6"] [6]

A single value or range endpoint can equal i64::MAX. An inclusive range such as 9223372036854775806-9223372036854775807 contains those two values; do not convert the upper endpoint to an exclusive bound by adding one.

What flat_map does

flat_map maps each input to something implementing IntoIterator, then yields its items. This has the same flattening behavior as map(...).flatten(). It removes one level of nesting, not every level recursively.

Here is a complete example that shows the intermediate item type:

fn main() {
    let nested = vec![vec![vec![1, 2], vec![3]], vec![vec![4]]];
    let one_level: Vec<Vec<i32>> = nested.into_iter().flatten().collect();
    assert_eq!(one_level, vec![vec![1, 2], vec![3], vec![4]]);

    let numbers: Vec<i32> = one_level.into_iter().flatten().collect();
    assert_eq!(numbers, vec![1, 2, 3, 4]);
}

The mapping closure may return a vector, a range or another IntoIterator value. An empty vector contributes no items. Option also implements IntoIterator: Some contributes one item and None contributes none. Option itself is not an Iterator. Result similarly yields its Ok value or no items for Err, discarding the error.

When a closure returns Option, filter_map expresses keeping only the present values directly:

fn main() {
    let words = ["7", "bad", "12"];
    let numbers: Vec<i64> = words.iter()
        .filter_map(|word| word.parse().ok())
        .collect();
    assert_eq!(numbers, vec![7, 12]);
}

That example handles single numbers. This exercise also needs ranges, which can contribute several numbers per fragment.

Why the starter does not compile

The starter creates a local String named cleaned, then returns cleaned.split(',') from a closure. The split iterator borrows cleaned, but that string is dropped when the closure returns. Rust rejects the escaping borrow with E0515.

You can sometimes solve this kind of problem by collecting owned fragments inside the closure. Here the temporary is unnecessary: spec.iter() gives references to strings already owned by expand, and each can be split directly. The spec vector stays alive while the iterator pipeline is consumed.

There are also two behavior problems to address:

  • Removing all spaces changes the grammar: "1 2" would become the valid number "12". Trim only at the fragment and bound edges.
  • Parsing each fragment as one number cannot expand ranges. Add that parsing step.

Build the pipeline in two stages

First flatten the comma-separated fragments from each string. Then map each fragment to the numbers it represents and flatten those numbers into the output. A helper returning Vec<i64> can use an empty vector for invalid fragments, a one-element vector for a single number, and a collected inclusive range for valid bounds.

split_once('-') separates a range at the first hyphen. Missing bounds fail to parse. An upper bound with a minus sign can parse, but an actually negative value makes the range reversed. Negative zero is the exception described above. (lo..=hi) includes hi; (lo..hi) does not.

Your solution must compile, match the outputs and pass Clippy with warnings treated as errors.