Skip to content

← Closures and Iterators step 12 of 28

Easy Primitives

zip and enumerate: pair two lists and report the leftovers

Pair up names with scores and report what did not pair.

pub fn interleave_report(names: Vec<String>, scores: Vec<i64>) -> Vec<String>

Emit one line per pair, formatted "idx:name=score" with idx 1-based, stopping when either list runs out. Then emit exactly one final line, "unmatched:K", where K is how many entries the longer list had left over.

names  = ["a", "b", "c"]
scores = [10]
  -> ["1:a=10", "unmatched:2"]

The "unmatched:K" line is always present, even when K is 0 and even when both lists are empty.

zip truncates, and never tells you

fn zip<U: IntoIterator>(self, other: U) -> Zip<Self, U::IntoIter>

Zip::next calls next on both sides and yields Some((a, b)) only if both produced a value. The first None on either side ends the whole iterator. So zip silently stops at the shorter input — no error, no padding, no warning.

That is usually what you want, and it is the number-one source of “why did my output come out short”. If the mismatch matters, you have to detect it yourself, before or beside the zip — which is exactly what this problem asks for. The iterator cannot tell you afterwards; it has already forgotten.

Compute the leftover count from the lengths, and use usize::abs_diff:

let leftover = names.len().abs_diff(scores.len());

Not names.len() - scores.len(). len() is usize, which is unsigned, so when scores is longer that subtraction underflows. This binary is built with -O, so it does not panic — it wraps. The starter does exactly this and reports:

unmatched:18446744073709551614

That is usize::MAX - 1. A debug build would have panicked with “attempt to subtract with overflow”; a release build ships the nonsense. Whenever you subtract two lengths, ask which one is bigger — and if you do not know, abs_diff does.

enumerate is the replacement for the index loop

fn enumerate(self) -> Enumerate<Self>   // Item = (usize, Self::Item)

The counter is always 0-based and always counts what reaches it, not positions in the original source. That second part matters: put enumerate after a filter and you number the survivors; put it before and you number the originals. Both are useful, and picking the wrong one is a silent bug.

Here the index must be 1-based, so i + 1 at the format site. Do not try to start the counter at one; there is no such option, and reaching for .skip(1) or a manual counter to fake it is how the starter got into trouble.

Nesting matters too:

a.iter().enumerate().zip(b)   // Item = ((usize, &A), B)
a.iter().zip(b).enumerate()   // Item = (usize, (&A, B))

Same elements, different tuple shape. The second is what you want here, and it destructures as |(i, (name, score))|.

Two default-on lints are doing the teaching

The starter uses a range loop with a hand-rolled counter, and clippy says:

error: the variable `idx` is used as a loop counter
       help: consider using: `for (idx, ...) in ... .enumerate()`
       [clippy::explicit_counter_loop]

Its sibling needless_range_loop fires on for i in 0..v.len() when i is only used to index one slice, and tells you to iterate the slice directly. Between them, clippy essentially bans the C-style index loop in idiomatic Rust — which is fine, because zip and enumerate cover the cases it was used for, and they do it without a bounds check.

The rest of the family, briefly

  • chain(other) — run one iterator then the next. Both must have the same Item.
  • rev() — requires DoubleEndedIterator. Slices and ranges have it; HashMap iteration does not.
  • cycle() — repeat forever. Requires Clone, and is infinite: pair it with zip or take or you have written a hang. Clippy’s infinite_iter is deny-by-default and catches the obvious cases.

zip with cycle is the idiomatic “pad the shorter side”, if you ever need the opposite of truncation.

Grade is compile + tests + clippy -D warnings.

Loading visualization…