Skip to content

is_first/is_last to ease handling of Position::Only #1042

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 7, 2025
Merged
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
23 changes: 15 additions & 8 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1945,8 +1945,8 @@ pub trait Itertools: Iterator {
pad_tail::pad_using(self, min, f)
}

/// Return an iterator adaptor that combines each element with a `Position` to
/// ease special-case handling of the first, last, or [singular](Position::Only) elements.
/// Return an iterator adaptor that combines each element with a `Position`
/// to ease special-case handling of the first or last elements.
///
/// Iterator element type is
/// [`(Position, Self::Item)`](Position)
Expand All @@ -1955,14 +1955,21 @@ pub trait Itertools: Iterator {
/// use itertools::{Itertools, Position};
///
/// let it = (0..4).with_position();
/// itertools::assert_equal(it,
/// vec![(Position::First, 0),
/// (Position::Middle, 1),
/// (Position::Middle, 2),
/// (Position::Last, 3)]);
/// itertools::assert_equal(
/// it,
/// vec![
/// (Position { is_first: true, is_last: false }, 0),
/// (Position { is_first: false, is_last: false }, 1),
/// (Position { is_first: false, is_last: false }, 2),
/// (Position { is_first: false, is_last: true }, 3),
/// ],
/// );
///
/// let it = (0..1).with_position();
/// itertools::assert_equal(it, vec![(Position::Only, 0)]);
/// itertools::assert_equal(
/// it,
/// vec![(Position { is_first: true, is_last: true }, 0)],
/// );
/// ```
fn with_position(self) -> WithPosition<Self>
where
Expand Down
95 changes: 56 additions & 39 deletions src/with_position.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,49 +45,48 @@
/// The first component of the value yielded by `WithPosition`.
/// Indicates the position of this element in the iterator results.
///
/// When handling the first or last position,
/// remember to consider the special case of [`Position::Only`].
///
/// See [`.with_position()`](crate::Itertools::with_position) for more information.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Position {
/// This is the first element, and there is more than one element.
First,
/// This is neither the first nor the last element.
Middle,
/// This is the last element, and there was more than one element.
Last,
/// This is the only element.
Only,
pub struct Position {
/// This is the initial element (also true if there's exactly one element)
pub is_first: bool,
/// This is the final element (also true if there's exactly one element)
pub is_last: bool,
}

impl Position {
/// This is the first and the last element at the same time, and there are no more elements
pub fn is_exactly_one(self) -> bool {
self.is_first && self.is_last
}

Check warning on line 61 in src/with_position.rs

View check run for this annotation

Codecov / codecov/patch

src/with_position.rs#L59-L61

Added lines #L59 - L61 were not covered by tests

/// This is neither first nor last element, and there will be more elements
pub fn is_middle(self) -> bool {
!self.is_first && !self.is_last
}

Check warning on line 66 in src/with_position.rs

View check run for this annotation

Codecov / codecov/patch

src/with_position.rs#L64-L66

Added lines #L64 - L66 were not covered by tests

/// This is the initial element (also true if there's exactly one element)
pub fn is_first(self) -> bool {
self.is_first
}

Check warning on line 71 in src/with_position.rs

View check run for this annotation

Codecov / codecov/patch

src/with_position.rs#L69-L71

Added lines #L69 - L71 were not covered by tests

/// This is the final element (also true if there's exactly one element)
pub fn is_last(self) -> bool {
self.is_last
}

Check warning on line 76 in src/with_position.rs

View check run for this annotation

Codecov / codecov/patch

src/with_position.rs#L74-L76

Added lines #L74 - L76 were not covered by tests
}

impl<I: Iterator> Iterator for WithPosition<I> {
type Item = (Position, I::Item);

fn next(&mut self) -> Option<Self::Item> {
match self.peekable.next() {
Some(item) => {
if !self.handled_first {
// Haven't seen the first item yet, and there is one to give.
self.handled_first = true;
// Peek to see if this is also the last item,
// in which case tag it as `Only`.
match self.peekable.peek() {
Some(_) => Some((Position::First, item)),
None => Some((Position::Only, item)),
}
} else {
// Have seen the first item, and there's something left.
// Peek to see if this is the last item.
match self.peekable.peek() {
Some(_) => Some((Position::Middle, item)),
None => Some((Position::Last, item)),
}
}
}
// Iterator is finished.
None => None,
}
let item = self.peekable.next()?;

let is_last = self.peekable.peek().is_none();
let is_first = !self.handled_first;
self.handled_first = true;

Some((Position { is_first, is_last }, item))
}

fn size_hint(&self) -> (usize, Option<usize>) {
Expand All @@ -105,18 +104,36 @@
match self.peekable.next() {
Some(second) => {
let first = std::mem::replace(&mut head, second);
init = f(init, (Position::First, first));
let position = Position {
is_first: true,
is_last: false,
};
init = f(init, (position, first));
}
None => {
let position = Position {
is_first: true,
is_last: true,
};
return f(init, (position, head));
}
None => return f(init, (Position::Only, head)),
}
}
// Have seen the first item, and there's something left.
init = self.peekable.fold(init, |acc, mut item| {
std::mem::swap(&mut head, &mut item);
f(acc, (Position::Middle, item))
let position = Position {
is_first: false,
is_last: false,
};
f(acc, (position, item))
});
let position = Position {
is_first: false,
is_last: true,
};
// The "head" is now the last item.
init = f(init, (Position::Last, head));
init = f(init, (position, head));
}
init
}
Expand Down
Loading