Skip to content

Commit b2a978a

Browse files
SAY-5phimuemue
authored andcommitted
feat(Itertools): add strip_prefix and strip_prefix_by methods
1 parent 12b6ec6 commit b2a978a

2 files changed

Lines changed: 129 additions & 0 deletions

File tree

src/lib.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5122,6 +5122,97 @@ pub trait Itertools: Iterator {
51225122
_ => Err(sh),
51235123
}
51245124
}
5125+
5126+
/// Removes a prefix from the iterator, returning the rest.
5127+
///
5128+
/// If `self` begins with all the items yielded by `prefix` (in order), this
5129+
/// returns `Ok` of the iterator advanced past that prefix. Otherwise it
5130+
/// returns `Err(StripPrefixError { .. })` exposing the partially-consumed
5131+
/// iterator, the remaining prefix, and the items that failed to match, so
5132+
/// callers can recover progress made before the mismatch.
5133+
///
5134+
/// See [`strip_prefix_by`](Itertools::strip_prefix_by) for a variant
5135+
/// taking an explicit equality predicate.
5136+
///
5137+
/// ```
5138+
/// use itertools::Itertools;
5139+
///
5140+
/// let ok = (1..6).strip_prefix([1, 2]).map(Itertools::collect_vec).ok();
5141+
/// assert_eq!(ok, Some(vec![3, 4, 5]));
5142+
/// assert!((1..6).strip_prefix([1, 9]).is_err());
5143+
/// let empty = (1..6).strip_prefix(std::iter::empty::<i32>()).map(Itertools::collect_vec).ok();
5144+
/// assert_eq!(empty, Some(vec![1, 2, 3, 4, 5]));
5145+
/// ```
5146+
fn strip_prefix<Prefix>(
5147+
self,
5148+
prefix: Prefix,
5149+
) -> Result<Self, StripPrefixError<Self, Prefix::IntoIter, Self::Item>>
5150+
where
5151+
Self: Sized,
5152+
Prefix: IntoIterator,
5153+
Self::Item: PartialEq<Prefix::Item>,
5154+
{
5155+
self.strip_prefix_by(prefix, |a, b| a == b)
5156+
}
5157+
5158+
/// Removes a prefix from the iterator using `eq` to compare items.
5159+
///
5160+
/// If `self` begins with all the items yielded by `prefix` (in order, as
5161+
/// judged by `eq`), this returns `Ok` of the iterator advanced past that
5162+
/// prefix. Otherwise it returns `Err(StripPrefixError { .. })`, allowing
5163+
/// the prefix items to have a different type than `Self::Item`.
5164+
///
5165+
/// ```
5166+
/// use itertools::Itertools;
5167+
///
5168+
/// let path = ["home", "user", "file"];
5169+
/// let stripped = path.iter().strip_prefix_by(["home", "user"], |a, b| **a == *b);
5170+
/// assert_eq!(stripped.map(Itertools::collect_vec).ok(), Some(vec![&"file"]));
5171+
/// ```
5172+
fn strip_prefix_by<Prefix, F>(
5173+
mut self,
5174+
prefix: Prefix,
5175+
mut eq: F,
5176+
) -> Result<Self, StripPrefixError<Self, Prefix::IntoIter, Self::Item>>
5177+
where
5178+
Self: Sized,
5179+
Prefix: IntoIterator,
5180+
F: FnMut(&Self::Item, &Prefix::Item) -> bool,
5181+
{
5182+
let mut prefix = prefix.into_iter();
5183+
while let Some(wanted) = prefix.next() {
5184+
match self.next() {
5185+
Some(got) if eq(&got, &wanted) => continue,
5186+
got => {
5187+
return Err(StripPrefixError {
5188+
iterator: self,
5189+
prefix,
5190+
mismatch: (got, wanted),
5191+
});
5192+
}
5193+
}
5194+
}
5195+
Ok(self)
5196+
}
5197+
}
5198+
5199+
/// The error returned by [`Itertools::strip_prefix`] and
5200+
/// [`Itertools::strip_prefix_by`] when the iterator does not start with the
5201+
/// requested prefix.
5202+
///
5203+
/// All fields are public so callers can recover the partially-consumed
5204+
/// iterators and the mismatched items.
5205+
#[derive(Debug, Clone, PartialEq, Eq)]
5206+
pub struct StripPrefixError<I, Prefix: Iterator, T> {
5207+
/// The remainder of the original iterator, advanced past the matched
5208+
/// prefix items but stopped at the position of the mismatch.
5209+
pub iterator: I,
5210+
/// The remainder of the prefix iterator, starting just after the prefix
5211+
/// item that failed to match.
5212+
pub prefix: Prefix,
5213+
/// The pair of items that failed to compare equal. The first element is
5214+
/// `None` if `iterator` was exhausted before the prefix was fully matched.
5215+
pub mismatch: (Option<T>, Prefix::Item),
51255216
}
51265217

51275218
impl<T> Itertools for T where T: Iterator + ?Sized {}

tests/quick.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2098,4 +2098,42 @@ quickcheck! {
20982098
itertools::equal(v.iter().tail(n), result)
20992099
&& itertools::equal(v.iter().filter(|_| true).tail(n), result)
21002100
}
2101+
2102+
fn strip_prefix_matches_str(haystack: String, needle: String) -> bool {
2103+
let expected = haystack.strip_prefix(&needle);
2104+
let got: Option<String> = haystack
2105+
.chars()
2106+
.strip_prefix(needle.chars())
2107+
.ok()
2108+
.map(Iterator::collect);
2109+
got.as_deref() == expected
2110+
}
2111+
2112+
fn strip_prefix_by_matches_strip_prefix(v: Vec<i32>, n: u8) -> bool {
2113+
let prefix: Vec<i32> = v.iter().take(n as usize).copied().collect();
2114+
let by_eq = v.iter().strip_prefix_by(&prefix, |a, b| **a == **b).ok();
2115+
let plain = v.iter().strip_prefix(prefix.iter()).ok();
2116+
match (by_eq, plain) {
2117+
(Some(a), Some(b)) => itertools::equal(a, b),
2118+
(None, None) => true,
2119+
_ => false,
2120+
}
2121+
}
2122+
}
2123+
2124+
#[test]
2125+
fn strip_prefix_error_exposes_mismatch_and_remainders() {
2126+
let err = (1..6)
2127+
.strip_prefix([1, 2, 9, 4])
2128+
.expect_err("third item mismatches");
2129+
assert_eq!(err.mismatch, (Some(3), 9));
2130+
assert_eq!(err.prefix.collect_vec(), vec![4]);
2131+
assert_eq!(err.iterator.collect_vec(), vec![4, 5]);
2132+
}
2133+
2134+
#[test]
2135+
fn strip_prefix_error_signals_self_exhausted() {
2136+
let err = (1..3).strip_prefix([1, 2, 3]).expect_err("self exhausted");
2137+
assert_eq!(err.mismatch, (None, 3));
2138+
assert!(err.iterator.collect_vec().is_empty());
21012139
}

0 commit comments

Comments
 (0)