Skip to content

← Closures and Iterators step 12 of 28

Easy Primitives

zip and enumerate: pair two lists and report the leftovers

Pair names with scores by position, then report how many entries did not get a partner.

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

The contract

  • Pair names[0] with scores[0], names[1] with scores[1], and so on, stopping as soon as either list has no more entries. The number of pairs is the length of the shorter list.
  • For each pair, in input order, emit one line formatted "idx:name=score". idx is the pair's position counted from 1, name is copied unchanged (it may be empty or contain any Unicode text), and score is written in ordinary decimal, with a leading - when negative.
  • After the pair lines, emit exactly one more line, "unmatched:K". K is the number of entries in the longer list that were not paired, which is the difference between the two lengths. This line is present in every result, including when K is 0 and when both lists are empty.
names  = ["a", "b", "c"]
scores = [10]
  -> ["1:a=10", "unmatched:2"]

names  = ["a"]
scores = [10, 20, 30]
  -> ["1:a=10", "unmatched:2"]

names  = []
scores = []
  -> ["unmatched:0"]

In the first example only one pair exists, and "b" and "c" are left over. In the second, the longer list is scores, and 20 and 30 are left over.

zip stops at the shorter input without reporting it

An iterator adapter is a method that wraps an iterator and produces a new one. zip combines two iterators into one that yields pairs:

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

Each call to the zipped iterator's next first advances the left iterator. If that returns None, the zipped iterator returns None without touching the right one. Otherwise it advances the right iterator, and returns Some((left, right)) only if that also produced a value. So iteration ends at the shorter input. zip returns no error and adds no padding, and the zipped iterator has no method that reports how many items the longer side still had.

Often that truncation is what you want. When the mismatch matters, as it does here, compute it separately. The lengths are enough:

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

abs_diff returns the absolute difference of two integers, so the argument order does not matter. Compare the starter's names.len() - scores.len(). len() returns usize, an unsigned type, so that subtraction has no correct result when scores is longer. What happens then depends on how the program is compiled. In a debug build, overflow checks are on and the program panics with attempt to subtract with overflow. The grading runtime compiles with rustc -O, which turns overflow checks off unless they are requested explicitly, so the subtraction wraps around instead. On a 64-bit target, 1 - 3 becomes usize::MAX - 1, and the starter reports:

unmatched:18446744073709551614

When subtracting two lengths, either know which one is larger or use abs_diff.

enumerate numbers the items it receives

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

enumerate pairs each item with a counter that starts at 0. It counts the items that reach it, not positions in the original data. If you place it after a filter, it numbers the items that passed the filter; if you place it before, it numbers the original positions. Both can be correct, so choose the order on purpose.

The counter cannot be told to start at 1. This exercise needs 1-based numbers, so either add one where you format (i + 1), or zip with an unbounded range instead of enumerating: (1..).zip(...) yields (1, item), (2, item), and so on.

Where you put enumerate relative to zip changes the shape of each item:

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

Both yield the same values. The second shape destructures as |(i, (name, score))|, which reads naturally for this exercise.

What Clippy reports on the starter

Clippy is Rust's linter. This exercise is graded by compiling, running the tests, and running Clippy with -D warnings, which turns every warning into an error. On the starter, Clippy 1.98 reports:

error: the variable `idx` is used as a loop counter
  help: consider using: `for (idx, i) in (1..).zip((0..pairs))`
  = note: `-D clippy::explicit-counter-loop` implied by `-D warnings`

explicit_counter_loop fires when a loop increments its own counter alongside the loop variable. A related lint, needless_range_loop, fires on for i in 0..v.len() when i is used only to index v, and suggests iterating over v directly. Both lints are warnings by default. They do not forbid index loops in general, but zip and enumerate cover the common case of walking sequences in step. Iterating also removes the explicit indexing, so there is no names[i] that could panic on an out-of-range index. Fixing the lint alone does not fix the subtraction, which Clippy does not report here.

Related adapters

  • chain(other) yields every item of the first iterator, then every item of the second. Both must produce the same Item type.
  • rev() iterates in reverse. It requires DoubleEndedIterator, which slice and Vec iterators and bounded integer ranges such as 0..n implement. HashMap iterators and unbounded ranges such as 0.. do not.
  • cycle() repeats an iterator from the start forever. It requires the iterator to be Clone. Unless the underlying iterator is empty, the result never ends, so limit it with take or by zipping it with a finite iterator. Clippy's infinite_iter lint, which is an error by default, catches some obviously unbounded uses, such as collecting a cycle().

If you need padding instead of truncation, chain a repeated filler onto the shorter side, for example scores.iter().copied().chain(std::iter::repeat(0)), and zip the longer side with that. cycle would repeat the shorter list's own values instead, which is a different result.