Skip to content

← Collections, Text and First Iterators step 16 of 21

Medium Primitives

windows and chunks are slice methods, not iterator adapters

Two small slice jobs.

pub fn run_lengths(data: Vec<i64>) -> Vec<(i64, usize)>
pub fn max_window_sum(data: Vec<i64>, k: usize) -> Option<i64>
  • run_lengths is run-length encoding: [1,1,2,2,1] becomes [(1,2), (2,2), (1,1)]. Note the trailing (1,1) — a run is a run of adjacent equal values, so the two 1s at either end are separate runs.
  • max_window_sum returns the largest sum of any k consecutive elements, or None when there is no such window: k == 0, k > data.len(), or an empty slice.

The error that names the boundary

The starter writes what reads like perfectly ordinary iterator code:

data.iter().windows(k).map(...)
error[E0599]: no method named `windows` found for struct
              `std::slice::Iter<'a, T>` in the current scope

Read the type in that message: std::slice::Iter. You called .iter(), which turned your slice into an iterator, and then asked the iterator for a slice method. windows, chunks, chunks_exact, chunk_by, split_at, sort, binary_search, first, last — all of these live on [T], and none of them exist on Iterator.

The fix is to delete .iter(). Vec<T> derefs to [T], so data.windows(k) works directly, and it returns an iterator — of slices — which you can then .map() over as usual. Slice method first, iterator second.

Why the boundary exists at all

It is not an oversight, and knowing the reason means you will never have to memorise the list.

An Iterator is a one-way, one-shot protocol: it has exactly one method, next, and once an item is handed out it is gone. windows(3) needs to return [a,b,c], then [b,c,d] — it must look at b and c again. There is no way to do that over a one-way stream without buffering, and buffering would mean allocating, and an adapter that silently allocates is not something std will give you. A slice, by contrast, is random-access memory you already hold, so windows is just arithmetic on indices and each yielded item is a borrow, not a copy.

The same logic explains sort (needs to move elements around), and binary_search (needs to jump).

If you genuinely have an iterator and want pairs of neighbours, the std-only trick is to zip a stream with itself, offset by one:

v.iter().zip(v.iter().skip(1))   // (1,2), (2,3), (3,4), (4,5)

That gives you windows of exactly 2, lazily, with no buffering — which is most of what people want windows for anyway.

windows(0) panics

thread 'main' panicked at 'window size must be non-zero'

Not None, not an empty iterator — a panic. There is no sensible sequence of zero-width windows, and std takes the position that asking for one is a bug in the caller rather than a runtime condition. Your guard is not defensive padding; it is part of the specification, and there is a test case for it.

This is the same design you met with BTreeMap::range earlier. Build the habit of asking, for any std method that takes a size or a range, “what does it do at the boundary?” — the answer is in the docs under Panics, and it is worth two seconds before you write the call rather than two hours after the incident.

chunks versus chunks_exact versus chunk_by

Three neighbours that do different things:

let v = [1, 2, 3, 4, 5];

v.chunks(2)        // [1,2] [3,4] [5]      ragged tail included
v.chunks_exact(2)  // [1,2] [3,4]          tail dropped...
                   //   ...and available via .remainder() -> [5]
v.windows(2)       // [1,2] [2,3] [3,4] [4,5]   overlapping

chunks partitions with a possibly-short last piece; chunks_exact gives only full pieces and hands the leftovers back separately, which lets the optimiser vectorise the body because every chunk has the same length; windows overlaps and never has a short one.

chunk_by is the fourth: it groups adjacent elements by a predicate on each neighbouring pair, so the chunk boundaries come from the data rather than from a fixed size. That is exactly run-length encoding:

data.chunk_by(|a, b| a == b)

One caution when you search for it: chunk_by was called group_by until Rust 1.77. Every blog post and Stack Overflow answer written before that says group_by, and the rename happened because group_by sounded like SQL’s arbitrary-key grouping, which — as the previous problem in this track showed — is a completely different operation needing a map.

::: question max_window_sum as written is O(n·k). Could you do better, and should you here? Yes: a sliding window keeps a running sum, adds the entering element and subtracts the leaving one, which is O(n) regardless of k. With windows, you re-add all k elements for every position.

Should you? For this problem, no — the inputs are tiny and the windows version says what it means in one line. But it is worth knowing the shape, because the O(n) version is not harder:

let mut sum: i64 = data[..k].iter().sum();
let mut best = sum;
for i in k..data.len() {
    sum += data[i] - data[i - k];
    best = best.max(sum);
}

And it is worth noticing what you gave up: that version indexes, so it can panic; the windows version cannot index out of bounds at all. Clarity and safety on one side, a factor of k on the other. Choose deliberately rather than by reflex. :::

Remember the grade is compile + tests + clippy -D warnings.

Loading visualization…