Skip to content

← Errors Are Values step 22 of 24

Medium Primitives

Collecting an iterator of Results

Parse a list of strings in two ways: stop at the first error in one pass, and keep every successful value in the other.

pub fn strict_and_lenient(raw: Vec<String>) -> (Result<Vec<i32>, String>, Vec<i32>, usize)

Return these three results:

  1. Strict: all parsed values in Ok, or the first failure as Err("bad input: {original}").
  2. Lenient: all successfully parsed values, including those after the strict pass's first failure, in input order. Preserve duplicates.
  3. Calls: the number of strings visited by the strict parsing closure, including the string that failed. Do not count the lenient pass.

Both passes use s.trim().parse::<i32>(). trim removes leading and trailing Unicode whitespace recognized by Rust. The remaining text must be an optional ASCII + or - followed by ASCII decimal digits, with a value in the i32 range. Leading zeros are valid; internal spaces, underscores and non-ASCII digits are not. Empty or whitespace-only strings fail.

The error message includes the original, untrimmed string, exactly as received. For " x ", the message is "bad input: x ", with two spaces after the colon and one at the end.

For ["1", "2", "x", "4", "5"], return Err("bad input: x"), [1, 2, 4, 5], and 3. If every string parses, calls equals the input length. Empty input returns (Ok(vec![]), vec![], 0).

Collect successes into one Result

An iterator yielding Result<T, E> can be collected into Result<Vec<T>, E>. Collection stops requesting items when it encounters an Err. If all items succeed, the result contains their values in order.

This complete example counts the actual calls made to its mapping closure:

fn main() {
    let raw = ["1", "x", "3"];
    let mut calls = 0;
    let parsed: Result<Vec<i32>, _> = raw.iter().map(|s| {
        calls += 1;
        s.parse::<i32>()
    }).collect();
    assert!(parsed.is_err());
    assert_eq!(calls, 2);
}

The mapping closure runs as the iterator is consumed, not when it is created. In your implementation, increment calls inside that closure and use map_err to replace a parse error with the required message.

The grader checks the returned count. That catches the starter's behavior, but a self-reported number is not independent instrumentation of arbitrary code. Keep the count tied to actual parsing visits rather than calculating the expected number after parsing everything.

A loop that exits at the first failure could also implement this behavior. Here, practice expressing it through collect::<Result<Vec<_>, _>>().

Keep only successes in the lenient pass

Convert each parse result to an Option with .ok(), then use filter_map to keep the successful values:

fn main() {
    let raw = [" 1 ", "bad", "-2", "1"];
    let values: Vec<i32> = raw.iter()
        .filter_map(|s| s.trim().parse::<i32>().ok())
        .collect();
    assert_eq!(values, [1, -2, 1]);
}

This visits the full input. It deliberately discards error details, while the strict pass preserves the first error message. There is no need to filter by is_ok() and then unwrap the same results.

The destination changes the behavior

Vec<Result<T, E>> retains each result, including errors. Result<Vec<T>, E> stops at the first error and does not return the partial success vector. If context does not choose a destination, add a type annotation or turbofish.

Option has a similar collection rule: an Option<Vec<T>> becomes None at the first missing item. By contrast, flattening an iterator of options skips missing items and continues. Option and Result implement IntoIterator; they are not themselves Iterator implementations.

fn main() {
    let items = [Some(1), None, Some(3)];
    let strict: Option<Vec<i32>> = items.into_iter().collect();
    let lenient: Vec<i32> = items.into_iter().flatten().collect();
    assert_eq!(strict, None);
    assert_eq!(lenient, [1, 3]);
}

What the starter gets wrong

The starter records the first error but continues parsing the rest of the input. Its strict result and lenient values are correct, but its strict call count is too large when a failure occurs before the final item. If the last item fails, or no item fails, that count alone does not expose the problem.

Move the strict work into a lazy mapping iterator and collect it into a Result. Then perform the lenient pass separately, without incrementing the strict counter.