From 4312f49d8b6af072419ec128c5b4336c2ab80b38 Mon Sep 17 00:00:00 2001 From: pickx Date: Mon, 26 Jun 2023 09:05:47 +0300 Subject: [PATCH 1/3] change return type of Itertools::at_most_one to AtMostOneResult --- src/at_most_one.rs | 76 ++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 59 ++++++++++++++++++++--------------- tests/quick.rs | 32 +++++++++++++++---- 3 files changed, 137 insertions(+), 30 deletions(-) create mode 100644 src/at_most_one.rs diff --git a/src/at_most_one.rs b/src/at_most_one.rs new file mode 100644 index 000000000..7d33cd7e8 --- /dev/null +++ b/src/at_most_one.rs @@ -0,0 +1,76 @@ +use crate::size_hint; +#[cfg(feature = "use_std")] +use std::iter::ExactSizeIterator; +use std::mem; + +/// The enum returned by `at_most_one`, depending on the number of remaining +/// elements in the iterator. +/// +/// See [`.at_most_one()`](crate::Itertools::at_most_one) for more detail. +#[derive(PartialEq, Eq, Clone)] +pub enum AtMostOneResult { + /// The iterator was empty and therefore had zero elements. + Zero, + + /// The iterator had exactly one element. + One(I::Item), + + /// The iterator had more than one element. + /// `MoreThanOne` is an iterator which yields the same elements as the original iterator. + MoreThanOne(MoreThanOne), +} + +#[derive(PartialEq, Eq, Clone)] +enum IterSource { + FirstElement(T, T), + SecondElement(T), + InnerIter, +} + +/// The iterator returned by [`.at_most_one()`](crate::Itertools::at_most_one), if the original iterator +/// had at least two elements remaining. Yields the same elements as the original iterator. +#[derive(PartialEq, Eq, Clone)] +pub struct MoreThanOne { + next_source: IterSource, + inner: I, +} + +impl MoreThanOne { + pub(crate) fn new(first_two: [I::Item; 2], inner: I) -> Self { + let [first, second] = first_two; + let next_source = IterSource::FirstElement(first, second); + + Self { next_source, inner } + } + + fn additional_len(&self) -> usize { + match self.next_source { + IterSource::FirstElement(_, _) => 2, + IterSource::SecondElement(_) => 1, + IterSource::InnerIter => 0, + } + } +} + +impl Iterator for MoreThanOne { + type Item = I::Item; + + fn next(&mut self) -> Option { + let source = mem::replace(&mut self.next_source, IterSource::InnerIter); + + match source { + IterSource::FirstElement(first, second) => { + self.next_source = IterSource::SecondElement(second); + Some(first) + } + IterSource::SecondElement(second) => Some(second), + IterSource::InnerIter => self.inner.next(), + } + } + + fn size_hint(&self) -> (usize, Option) { + size_hint::add_scalar(self.inner.size_hint(), self.additional_len()) + } +} + +impl ExactSizeIterator for MoreThanOne where I: ExactSizeIterator {} diff --git a/src/lib.rs b/src/lib.rs index c23a65db5..fde96fbd8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -112,6 +112,7 @@ pub mod structs { pub use crate::adaptors::{MapResults, Step}; #[cfg(feature = "use_alloc")] pub use crate::adaptors::MultiProduct; + pub use crate::at_most_one::{AtMostOneResult, MoreThanOne}; #[cfg(feature = "use_alloc")] pub use crate::combinations::Combinations; #[cfg(feature = "use_alloc")] @@ -183,6 +184,7 @@ pub use crate::with_position::Position; pub use crate::unziptuple::{multiunzip, MultiUnzip}; pub use crate::ziptuple::multizip; mod adaptors; +mod at_most_one; mod either_or_both; pub use crate::either_or_both::EitherOrBoth; #[doc(hidden)] @@ -3690,39 +3692,48 @@ pub trait Itertools : Iterator { } } - /// If the iterator yields no elements, Ok(None) will be returned. If the iterator yields - /// exactly one element, that element will be returned, otherwise an error will be returned - /// containing an iterator that has the same output as the input iterator. - /// - /// This provides an additional layer of validation over just calling `Iterator::next()`. - /// If your assumption that there should be at most one element yielded is false this provides - /// the opportunity to detect and handle that, preventing errors at a distance. + /// If the iterator yields no elements, [`AtMostOneResult::Zero`] will be returned. + /// If the iterator yields exactly one element, that element will be returned as [`AtMostOneResult::One`]. + /// Otherwise [`AtMostOneResult::MoreThanOne`] will be returned, containing an iterator that has the same + /// output as the input iterator. /// + /// This function is especially useful in `match` statements, to clearly assert the number of elements + /// in an iterator or take different paths depending on this number. + /// For example, using [`.find()`](std::iter::Iterator::find) will stop at the first element, + /// but you may additionally want to validate that no more than a single element satisfies the predicate. + /// /// # Examples /// ``` /// use itertools::Itertools; - /// - /// assert_eq!((0..10).filter(|&x| x == 2).at_most_one().unwrap(), Some(2)); - /// assert!((0..10).filter(|&x| x > 1 && x < 4).at_most_one().unwrap_err().eq(2..4)); - /// assert!((0..10).filter(|&x| x > 1 && x < 5).at_most_one().unwrap_err().eq(2..5)); - /// assert_eq!((0..10).filter(|&_| false).at_most_one().unwrap(), None); + /// + /// let numbers = [-9, -1, -7, 4, -38, -21].iter().copied(); + /// let positive_number: Option = match numbers.filter(|&num| num >= 0).at_most_one() { + /// AtMostOneResult::Zero => None, + /// AtMostOneResult::One(n) => Some(n), + /// AtMostOneResult::MoreThanOne(_) => panic!("expected no more than one positive number"), + /// }; + /// assert_eq!(positive_number, Some(4)); + /// + /// let zero_elements = empty().at_most_one(); + /// assert_eq!(zero_elements, AtMostOneResult::Zero); + /// + /// assert_eq!(one_element, AtMostOneResult::One(5)); + /// + /// let many_elements = (1..=10).at_most_one(); + /// let AtMostOneResult::MoreThanOne(more_than_one) = many_elements else { panic!() }; + /// assert!(more_than_one.eq(1..=10)); /// ``` - fn at_most_one(mut self) -> Result, ExactlyOneError> + fn at_most_one(mut self) -> AtMostOneResult where Self: Sized, { - match self.next() { - Some(first) => { - match self.next() { - Some(second) => { - Err(ExactlyOneError::new(Some(Either::Left([first, second])), self)) - } - None => { - Ok(Some(first)) - } - } + match (self.next(), self.next()) { + (None, _) => AtMostOneResult::Zero, + (Some(first), None) => AtMostOneResult::One(first), + (Some(first), Some(second)) => { + let more_than_one = MoreThanOne::new([first, second], self); + AtMostOneResult::MoreThanOne(more_than_one) } - None => Ok(None), } } diff --git a/tests/quick.rs b/tests/quick.rs index c19af6c1e..20de3fca4 100644 --- a/tests/quick.rs +++ b/tests/quick.rs @@ -1300,12 +1300,32 @@ quickcheck! { quickcheck! { fn at_most_one_i32(a: Vec) -> TestResult { - let ret = a.iter().cloned().at_most_one(); - match a.len() { - 0 => TestResult::from_bool(ret.unwrap() == None), - 1 => TestResult::from_bool(ret.unwrap() == Some(a[0])), - _ => TestResult::from_bool(ret.unwrap_err().eq(a.iter().cloned())), - } + use itertools::AtMostOneResult; + + let iter = a.iter().copied(); + + let (first, second, tail) = { + let mut iter = iter.clone(); + (iter.next(), iter.next(), iter) + }; + + let amo_result = iter.at_most_one(); + let expected = match a.len() { + 0 => matches!(amo_result, AtMostOneResult::Zero), + 1 => matches!(amo_result, AtMostOneResult::One(n) if first == Some(n)), + _ => { + let AtMostOneResult::MoreThanOne(mut more_than_one) = amo_result else { + return TestResult::failed(); + }; + + let actual_first = more_than_one.next(); + let actual_second = more_than_one.next(); + + first == actual_first && second == actual_second && more_than_one.eq(tail) + }, + }; + + TestResult::from_bool(expected) } } From c798a7697ae5fd36860dfaa1fffeb470c997bb90 Mon Sep 17 00:00:00 2001 From: pickx Date: Mon, 26 Jun 2023 09:52:15 +0300 Subject: [PATCH 2/3] make it no_std compatible --- src/at_most_one.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/at_most_one.rs b/src/at_most_one.rs index 7d33cd7e8..e3547174b 100644 --- a/src/at_most_one.rs +++ b/src/at_most_one.rs @@ -1,7 +1,6 @@ use crate::size_hint; -#[cfg(feature = "use_std")] -use std::iter::ExactSizeIterator; -use std::mem; +use core::iter::ExactSizeIterator; +use core::mem; /// The enum returned by `at_most_one`, depending on the number of remaining /// elements in the iterator. From dd1bf325b3132470697913e8e0febc1707ea8a5e Mon Sep 17 00:00:00 2001 From: pickx Date: Mon, 26 Jun 2023 10:42:19 +0300 Subject: [PATCH 3/3] fix doctest --- src/at_most_one.rs | 8 ++++---- src/lib.rs | 12 +++++++----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/at_most_one.rs b/src/at_most_one.rs index e3547174b..f08a223e8 100644 --- a/src/at_most_one.rs +++ b/src/at_most_one.rs @@ -6,7 +6,7 @@ use core::mem; /// elements in the iterator. /// /// See [`.at_most_one()`](crate::Itertools::at_most_one) for more detail. -#[derive(PartialEq, Eq, Clone)] +#[derive(PartialEq, Eq, Clone, Debug)] pub enum AtMostOneResult { /// The iterator was empty and therefore had zero elements. Zero, @@ -15,11 +15,11 @@ pub enum AtMostOneResult { One(I::Item), /// The iterator had more than one element. - /// `MoreThanOne` is an iterator which yields the same elements as the original iterator. + /// [`MoreThanOne`](crate::MoreThanOne) is an iterator which yields the same elements as the original iterator. MoreThanOne(MoreThanOne), } -#[derive(PartialEq, Eq, Clone)] +#[derive(PartialEq, Eq, Clone, Debug)] enum IterSource { FirstElement(T, T), SecondElement(T), @@ -28,7 +28,7 @@ enum IterSource { /// The iterator returned by [`.at_most_one()`](crate::Itertools::at_most_one), if the original iterator /// had at least two elements remaining. Yields the same elements as the original iterator. -#[derive(PartialEq, Eq, Clone)] +#[derive(PartialEq, Eq, Clone, Debug)] pub struct MoreThanOne { next_source: IterSource, inner: I, diff --git a/src/lib.rs b/src/lib.rs index fde96fbd8..ed0596e7c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3699,12 +3699,13 @@ pub trait Itertools : Iterator { /// /// This function is especially useful in `match` statements, to clearly assert the number of elements /// in an iterator or take different paths depending on this number. - /// For example, using [`.find()`](std::iter::Iterator::find) will stop at the first element, + /// For example, using [`.find()`](std::iter::Iterator::find) will stop at the first matched element, /// but you may additionally want to validate that no more than a single element satisfies the predicate. /// /// # Examples /// ``` - /// use itertools::Itertools; + /// use itertools::{Itertools, AtMostOneResult}; + /// use core::iter::{empty, once}; /// /// let numbers = [-9, -1, -7, 4, -38, -21].iter().copied(); /// let positive_number: Option = match numbers.filter(|&num| num >= 0).at_most_one() { @@ -3714,10 +3715,11 @@ pub trait Itertools : Iterator { /// }; /// assert_eq!(positive_number, Some(4)); /// - /// let zero_elements = empty().at_most_one(); - /// assert_eq!(zero_elements, AtMostOneResult::Zero); + /// let zero_elements = empty::().at_most_one(); + /// assert!(matches!(zero_elements, AtMostOneResult::Zero)); /// - /// assert_eq!(one_element, AtMostOneResult::One(5)); + /// let one_element = once(5).at_most_one(); + /// assert!(matches!(one_element, AtMostOneResult::One(5))); /// /// let many_elements = (1..=10).at_most_one(); /// let AtMostOneResult::MoreThanOne(more_than_one) = many_elements else { panic!() };