Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 230 additions & 0 deletions src/array_impl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
use crate::Itertools;
use std::iter::Fuse;

/// An iterator over all contiguous windows of the input iterator,
/// producing arrays of a specific size.
///
/// See [`.array_windows()`](crate::Itertools::array_windows) for more
/// information.
#[derive(Debug, Clone)]
pub struct ArrayWindows<I, const N: usize>
where
I: Iterator + Sized,
I::Item: Clone,
{
iter: Fuse<I>,
inner: Option<ArrayWindowsInner<I::Item, N>>,
}

#[derive(Debug, Clone)]
struct ArrayWindowsInner<T: Clone, const N: usize> {
// `window` stores the `N` items delivered in the most
// recent output window. It is stored in the form of a ring
// buffer, with `window_start` identifying the element
// that logically comes first.
window: [T; N],
window_start: usize,
}

impl<T: Clone, const N: usize> ArrayWindowsInner<T, N> {
/// Replace the least recent item in `window` with a new
/// item.
fn add_to_buffer(&mut self, item: T) {
if N > 0 {
self.window[self.window_start] = item;
self.window_start = (self.window_start + 1) % N;
}
}

/// Construct an array window to return.
fn make_window(&self) -> [T; N] {
std::array::from_fn(|i| self.window[(i + self.window_start) % N].clone())
}
}

impl<I, const N: usize> Iterator for ArrayWindows<I, N>
where
I: Iterator + Sized,
I::Item: Clone,
{
type Item = [I::Item; N];

fn next(&mut self) -> Option<[I::Item; N]> {
match &mut self.inner {
// Initialisation code, when next() is called for the first time
None => match self.iter.next_array() {
None => {
// The input iterator was completely empty
None
}
Some(buf) => {
let inner = ArrayWindowsInner {
window: buf.clone(),
window_start: 0,
};
let window = inner.make_window();
self.inner = Some(inner);
Some(window)
}
},
Some(inner) => match self.iter.next() {
Some(item) => {
inner.add_to_buffer(item);
Some(inner.make_window())
}
None => None,
},
}
}
}

pub fn array_windows<I, const N: usize>(iter: I) -> ArrayWindows<I, N>
where
I: Iterator + Sized,
I::Item: Clone,
{
ArrayWindows {
iter: iter.fuse(),
inner: None,
}
}

/// An iterator over all windows, wrapping back to the first elements when the
/// window would otherwise exceed the length of the iterator, producing arrays
/// of a specific size.
///
/// See [`.circular_array_windows()`](crate::Itertools::circular_array_windows)
/// for more information.
#[derive(Debug, Clone)]
pub struct CircularArrayWindows<I, const N: usize>
where
I: Iterator + Sized,
I::Item: Clone,
{
iter: Fuse<I>,
inner: Option<CircularArrayWindowsInner<I::Item, N>>,
}

#[derive(Debug, Clone)]
struct CircularArrayWindowsInner<T: Clone, const N: usize> {
// `prefix` stores the first `N` items output from this iterator.
// If the input contained fewer than `N` items, then it is filled
// with clones of the previous items in a cycle.
//
// `prefix_pos` tracks the number of items that have been _used_
// from `prefix`. It begins counting up from 0 once the input runs
// out. (So in the case where the input iterator is shorter than
// `N`, it will begin counting up before `prefix` has even been
// populated during setup.)
prefix: [T; N],
prefix_pos: usize,

// For delivering the output arrays, we reuse `ArrayWindowsInner`
// unchanged.
arraywin: ArrayWindowsInner<T, N>,
}

impl<I, const N: usize> Iterator for CircularArrayWindows<I, N>
where
I: Iterator + Sized,
I::Item: Clone,
{
type Item = [I::Item; N];

fn next(&mut self) -> Option<[I::Item; N]> {
match &mut self.inner {
// Initialisation code, when next() is called for the first time
None => match self.iter.next() {
None => {
// The input iterator was completely empty
None
}
Comment on lines +137 to +141
Copy link
Member

@phimuemue phimuemue Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Behavior for unfused iterators (iterators that yield Some(...) after they already yielded None) is questionable here - it would create inner on a non-first-call to next.

Let's fuse the underlying iterator. windows for unfused iterators are confusing anyway.

Some(first) => {
// We have at least one item, so we can definitely
// populate `prefix` (even if we have to make N
// copies of this element).

// Construct [Option<T>; N] and convert to [T; N]
// once it's full. TODO: can this be improved?
let mut items = std::array::from_fn(|_| None);
let mut prefix_pos = 0;
if N > 0 {
// The first item stored is the one passed to
// us from our caller.
items[0] = Some(first);
}
for i in 1..N {
// Populate the remaining slots in `items`
// from the input iterator.
items[i] = self.iter.next();
if items[i].is_none() {
// If the input iterator runs out early,
// populate the rest of `items` by
// recycling from the beginning, and set
// `prefix_pos` to indicate that we have
// already consumed those items.
for j in i..N {
items[j] = items[j - i].clone();
}
prefix_pos = N - i;
break;
}
}
let items = items.map(Option::unwrap);

let inner = CircularArrayWindowsInner {
prefix: items.clone(),
prefix_pos,
arraywin: ArrayWindowsInner {
window: items,
window_start: 0,
},
};

let window = inner.arraywin.make_window();
self.inner = Some(inner);
Some(window)
}
},
Some(inner) => {
// Normal case. Read the next item in the logical
// input sequence (consisting of the contents of the
// input iterator followed by N-1 items recycling from
// the beginning), and add it to the ring buffer.
let item = if let Some(item) = self.iter.next() {
// Read from the input iterator.
item
} else if N == 0 {
return None;
} else {
assert!(N == 0 || inner.prefix_pos < N);
if inner.prefix_pos + 1 == N {
// The input iterator has run out, and we've
// emitted as many windows as we read items,
// so we've finished.
return None;
}
let item = inner.prefix[inner.prefix_pos].clone();
inner.prefix_pos += 1;
item
};

if N > 0 {
inner.arraywin.add_to_buffer(item);
}
Some(inner.arraywin.make_window())
}
}
}
}

pub fn circular_array_windows<I, const N: usize>(iter: I) -> CircularArrayWindows<I, N>
where
I: Iterator + Sized,
I::Item: Clone,
{
CircularArrayWindows {
iter: iter.fuse(),
inner: None,
}
}
117 changes: 117 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ pub mod structs {
FilterOk, Interleave, InterleaveShortest, MapInto, MapOk, Positions, Product, PutBack,
TakeWhileRef, TupleCombinations, Update, WhileSome,
};
pub use crate::array_impl::{ArrayWindows, CircularArrayWindows};
#[cfg(feature = "use_alloc")]
pub use crate::combinations::{ArrayCombinations, Combinations};
#[cfg(feature = "use_alloc")]
Expand Down Expand Up @@ -174,6 +175,7 @@ pub use crate::unziptuple::{multiunzip, MultiUnzip};
pub use crate::with_position::Position;
pub use crate::ziptuple::multizip;
mod adaptors;
mod array_impl;
mod either_or_both;
pub use crate::either_or_both::EitherOrBoth;
#[doc(hidden)]
Expand Down Expand Up @@ -900,6 +902,121 @@ pub trait Itertools: Iterator {
tuple_impl::tuples(self)
}

/// Return an iterator over all contiguous windows, producing
/// arrays of size `N`.
///
/// `array_windows` clones the iterator elements so that they can be
/// part of successive windows. This makes it most suited for iterators
/// of references and other values that are cheap to copy.
///
/// If the input iterator contains fewer than `N` items, no
/// windows are returned. Otherwise, if the input iterator
/// contains `k` items, exactly `k+N-1` windows are returned.
///
/// ```
/// use itertools::Itertools;
///
/// // Three-element windows from the items [1, 2, 3, 4, 5].
/// itertools::assert_equal(
/// (1..6).array_windows::<3>(),
/// vec![[1, 2, 3], [2, 3, 4], [3, 4, 5]]
/// );
///
/// // When the input list is shorter than the window size, no windows
/// // are returned at all.
/// let mut windows = (1..6).array_windows::<10>();
/// assert_eq!(None, windows.next());
///
/// // In some cases you don't have to specify the window size
/// // explicitly with a type hint, because Rust can infer it
/// for [a, b, c] in (1..6).array_windows() {
/// println!("{a} {b} {c}");
/// }
///
/// // You can also specify the complete type.
/// use itertools::ArrayWindows;
/// use std::ops::Range;
///
/// let it: ArrayWindows<Range<u32>, 3> = (1..6).array_windows();
/// itertools::assert_equal(it, vec![[1, 2, 3], [2, 3, 4], [3, 4, 5]]);
/// ```
fn array_windows<const N: usize>(self) -> ArrayWindows<Self, N>
where
Self: Sized,
Self::Item: Clone,
{
array_impl::array_windows(self)
}

/// Return an iterator over all windows, wrapping back to the first
/// elements when the window would otherwise exceed the length of the
/// iterator, producing arrays of size `N`.
///
/// `circular_array_windows` clones the iterator elements so that
/// they can be part of successive windows, this makes it most
/// suited for iterators of references and other values that are
/// cheap to copy.
///
/// One window is returned per element of the input iterator. This
/// is true even if the input contains fewer elements than the
/// window size. In that situation, input elements are repeated
/// within each window. The results are as if the input had been
/// treated as a cyclic list, and a window of `N` items had been
/// returned for every starting point in the cycle.
///
/// ```
/// use itertools::Itertools;
///
/// // Three-element windows from [1, 2, 3, 4, 5], with two of
/// // them wrapping round from 5 to 1.
/// itertools::assert_equal(
/// (1..6).circular_array_windows::<3>(),
/// vec![[1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5, 1], [5, 1, 2]]
/// );
///
/// // If the input is shorter than the window size, input
/// // items are repeated even within the same window.
/// itertools::assert_equal(
/// (1..3).circular_array_windows::<5>(),
/// vec![[1, 2, 1, 2, 1], [2, 1, 2, 1, 2]]
/// );
///
/// // If the input contains only one item, the returned window
/// // repeats it N times.
/// let once = std::iter::once(1);
/// itertools::assert_equal(
/// once.circular_array_windows::<3>(),
/// vec![[1, 1, 1]]
/// );
///
/// // If the input is empty, no windows are returned at all.
/// let empty = std::iter::empty::<i32>();
/// let mut windows = empty.circular_array_windows::<5>();
/// assert_eq!(None, windows.next());
///
/// // In some cases you don't have to specify the window size
/// // explicitly with a type hint, because Rust can infer it.
/// for [a, b, c] in (1..10).circular_array_windows() {
/// println!("{a} {b} {c}");
/// }
///
/// // You can also specify the complete type.
/// use itertools::CircularArrayWindows;
/// use std::ops::Range;
///
/// let it: CircularArrayWindows<Range<u32>, 2> =
/// (1..6).circular_array_windows();
/// itertools::assert_equal(
/// it, vec![[1, 2], [2, 3], [3, 4], [4, 5], [5, 1]]);
/// ```
Comment on lines 968 to 1011
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make the asserts uniform: Either go with assert_eq(..., vec![...]) or with the single assert_eq. (I prefer the first one, maybe even condensed to sth like assert_eq!((1..5).windows().collect(), vec![...]).)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I should have known that copying the demo code directly from circular_tuple_windows wouldn't be good enough 🙂

Copy link
Member

@phimuemue phimuemue Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I should have known that copying the demo code directly from circular_tuple_windows wouldn't be good enough 🙂

Haha - (I'm) Guilty as charged. Sorry for being pedantic, but I think if we touch the code, we should make it (and its documentation) as good as we can.

Comment on lines 968 to 1011
Copy link
Member

@phimuemue phimuemue Feb 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this describe what happens for e.g. [1,2].circular_windows<5>()?

fn circular_array_windows<const N: usize>(self) -> CircularArrayWindows<Self, N>
where
Self: Sized,
Self::Item: Clone,
{
array_impl::circular_array_windows(self)
}

/// Split into an iterator pair that both yield all elements from
/// the original iterator.
///
Expand Down
Loading
Loading