Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
100 changes: 64 additions & 36 deletions src/pad_tail.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use crate::size_hint;
use std::iter::{Fuse, FusedIterator};

/// An iterator adaptor that pads a sequence to a minimum length by filling
Expand All @@ -11,28 +10,36 @@ use std::iter::{Fuse, FusedIterator};
#[must_use = "iterator adaptors are lazy and do nothing unless consumed"]
pub struct PadUsing<I, F> {
iter: Fuse<I>,
min: usize,
pos: usize,
elements_from_next: usize,
elements_from_next_back: usize,
elements_required: usize,
filler: F,
}

impl<I, F> std::fmt::Debug for PadUsing<I, F>
where
I: std::fmt::Debug,
{
debug_fmt_fields!(PadUsing, iter, min, pos);
debug_fmt_fields!(
PadUsing,
iter,
elements_from_next,
elements_from_next_back,
elements_required
);
}

/// Create a new `PadUsing` iterator.
pub fn pad_using<I, F>(iter: I, min: usize, filler: F) -> PadUsing<I, F>
pub fn pad_using<I, F>(iter: I, elements_required: usize, filler: F) -> PadUsing<I, F>
where
I: Iterator,
F: FnMut(usize) -> I::Item,
{
PadUsing {
iter: iter.fuse(),
min,
pos: 0,
elements_from_next: 0,
elements_from_next_back: 0,
elements_required,
filler,
}
}
Expand All @@ -46,38 +53,49 @@ where

#[inline]
fn next(&mut self) -> Option<Self::Item> {
match self.iter.next() {
None => {
if self.pos < self.min {
let e = Some((self.filler)(self.pos));
self.pos += 1;
e
} else {
None
}
}
e => {
self.pos += 1;
e
}
let total_consumed = self.elements_from_next + self.elements_from_next_back;

if total_consumed >= self.elements_required {
self.iter.next()
} else if let Some(e) = self.iter.next() {
self.elements_from_next += 1;
Some(e)
} else {
let e = (self.filler)(self.elements_from_next);
self.elements_from_next += 1;
Some(e)
Comment on lines +57 to +65
Copy link
Member

@phimuemue phimuemue Dec 8, 2025

Choose a reason for hiding this comment

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

Looks very nice imo. It tells

  • total_consumed does not overcount elements_required, which (presumably) avoids overflows.

}
}

fn size_hint(&self) -> (usize, Option<usize>) {
let tail = self.min.saturating_sub(self.pos);
size_hint::max(self.iter.size_hint(), (tail, Some(tail)))
let total_consumed = self.elements_from_next + self.elements_from_next_back;

if total_consumed >= self.elements_required {
return self.iter.size_hint();
}
Comment on lines +72 to +74
Copy link
Member

Choose a reason for hiding this comment

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

Nice and simple. Don't overcount elements_required, similar to next.


let elements_remaining = self.elements_required - total_consumed;
let (low, high) = self.iter.size_hint();

let lower_bound = low.max(elements_remaining);
let upper_bound = high.map(|h| h.max(elements_remaining));

(lower_bound, upper_bound)
}

fn fold<B, G>(self, mut init: B, mut f: G) -> B
where
G: FnMut(B, Self::Item) -> B,
{
let mut pos = self.pos;
let mut start = self.elements_from_next;
init = self.iter.fold(init, |acc, item| {
pos += 1;
start += 1;
Copy link
Member

Choose a reason for hiding this comment

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

Nitpick: Would this overflow if iter itself generated more than usize::MAX elements? And if so, would we end up mistakenly calling filler at the end of fold?

I'm not sure there's a straightforward formulation that would avoid the problem1. I'd be willing to accept it.

Footnotes

  1. Apart from messing with checked_add, which is sth we do not do often.

f(acc, item)
});
(pos..self.min).map(self.filler).fold(init, f)

let end = self.elements_required - self.elements_from_next_back;

(start..end).map(self.filler).fold(init, f)
}
}

Expand All @@ -87,24 +105,34 @@ where
F: FnMut(usize) -> I::Item,
{
fn next_back(&mut self) -> Option<Self::Item> {
if self.min == 0 {
self.iter.next_back()
} else if self.iter.len() >= self.min {
self.min -= 1;
self.iter.next_back()
let total_consumed = self.elements_from_next + self.elements_from_next_back;

if total_consumed >= self.elements_required {
return self.iter.next_back();
}

Comment on lines 94 to 97
Copy link
Member

Choose a reason for hiding this comment

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

Again, nice. Similar to next.

let elements_remaining = self.elements_required - total_consumed;
self.elements_from_next_back += 1;

if self.iter.len() < elements_remaining {
Some((self.filler)(
self.elements_required - self.elements_from_next_back,
))
} else {
self.min -= 1;
Some((self.filler)(self.min))
self.iter.next_back()
Copy link
Member

Choose a reason for hiding this comment

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

Please assert that the result of next_back is_some. Otherwise incrementing elements_from_next_back does not make sense and we'd possibly overcount elements_required.

You could even think about incrementing in each branch explicitly (just as you did in next). (Or adjust next to match next_back: Aligned logic in next and next_back might be easier to understand.)

Copy link
Member

Choose a reason for hiding this comment

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

2. 'm confused about the need to assert, either in this version or the previous version of the code (since they both contain the same logic). self.iter.next_back() is always Some, so I don't know what the assert call does. incrementing self.elements_from_next_back doesn't impact the underlying iterator.

Please add debug_assert that the element obtained from next_back actually is_some. We could leave it out with the argument "garbage in, garbage out", but the debug_asserts catches the in-garbage instead of tunneling through wrong results.

}
}

fn rfold<B, G>(self, mut init: B, mut f: G) -> B
where
G: FnMut(B, Self::Item) -> B,
{
init = (self.iter.len()..self.min)
.map(self.filler)
.rfold(init, &mut f);
let start = self.iter.len() + self.elements_from_next;
let remaining = self.elements_required.max(start);
let end = remaining - self.elements_from_next_back;
Copy link
Member

@phimuemue phimuemue Dec 8, 2025

Choose a reason for hiding this comment

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

I don't understand this. I could not find counterexamples, but the logic here seems to differ from everything else you wrote.

Especially: Why is remaining = elements_required.max(start)? Trying to untangle the - implicit - case distinction incurred by max:

let start = iter.len() + elements_from_next;
if elements_required < start {
 let end = start - elements_from_next_back;
 init = (start..end).rev().map(self.filler).fold(init, &mut f);
} else {
 let end = elements_required - elements_from_next_back;
 init = (start..end).rev().map(self.filler).fold(init, &mut f);
}

If elements_required < start, doesn't this mean that the iter itself will be able to generate more elements than elements_required? If so, we don't even need to touch filler in that case, and we can simplify to:

let start = iter.len() + elements_from_next;
if start <= elements_required {
 let end = elements_required - elements_from_next_back;
 init = (start..end).rev().map(self.filler).fold(init, &mut f);
}

There's another implicit case distinction: start..end is empty if start>=end. Making this case explicit:

let start = iter.len() + elements_from_next;
if start <= elements_required {
 let end = elements_required - elements_from_next_back;
 if (start < end) {
  init = (start..end).rev().map(self.filler).fold(init, &mut f);
 }
}

Expanding and reformulating the condition start < end gives this:

  • start < end
  • iter.len() + elements_from_next < elements_required - elements_from_next_back
  • iter.len() + elements_from_next + elements_from_next_back < elements_required

Now this brings us to known territory - much closer to what you already have in next[_back]. Also note that the last line alone is already a stronger condition than the initial start <= elements_required, so start <= elements_required is not needed.


init = (start..end).rev().map(self.filler).fold(init, &mut f);

self.iter.rfold(init, f)
}
}
Expand Down
28 changes: 28 additions & 0 deletions tests/specializations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,15 @@ where
}
}
}
check_specialized!(it, |i| {
let mut parameters_from_fold = vec![];
let fold_result = i.fold(vec![], |mut acc, v: I::Item| {
parameters_from_fold.push((acc.clone(), v.clone()));
acc.push(v);
acc
});
(parameters_from_fold, fold_result)
});
check_specialized!(it, |i| {
let mut parameters_from_rfold = vec![];
let rfold_result = i.rfold(vec![], |mut acc, v: I::Item| {
Expand All @@ -131,6 +140,25 @@ where
for n in 0..size + 2 {
check_specialized!(it, |mut i| i.nth_back(n));
}

let mut fwd = it.clone();
let mut bwd = it.clone();

for _ in fwd.by_ref() {}
Copy link
Member

Choose a reason for hiding this comment

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

Plase make this while fwd.next_back.is_some (just as with bwd).


assert_eq!(
fwd.next_back(),
None,
"iterator leaks elements after consuming forwards"
);

while bwd.next_back().is_some() {}

assert_eq!(
bwd.next(),
None,
"iterator leaks elements after consuming backwards"
);
}

quickcheck! {
Expand Down
Loading