Repository: rust-itertools/itertools
Pull request #891 (2024-03-01 11:12:27)

Add `.get(range)`
Closes #447.

So much changes happened since this work was initiated that rebase on top of master seems nothing less than a nightmare to me (I know, I tried). I therefore looked at the git differences, committed those (after formatting) and credited @wod3 and @TrolledWoods for their respective commits. Then I fixed my own review and a bit more.

---

I thought I would additionally implement `IteratorIndex` for `Either<range, range>` (and `Box<dyn range>` but I could not) but doing so would later result in a nasty breaking change if this was removed after an eventual inclusion to `Iterator`.

<details>
<summary>Details about either</summary>

```rust
    impl<L: Sealed, R: Sealed> Sealed for either::Either<L, R> {}


impl<I, L, R> IteratorIndex<I> for Either<L, R>
where
    I: Iterator,
    L: IteratorIndex<I>,
    R: IteratorIndex<I>,
{
    type Output = Either<<L as IteratorIndex<I>>::Output, <R as IteratorIndex<I>>::Output>;

    fn index(self, iter: I) -> Self::Output {
        // Could be more succinct with `map_either_with` but it would require to bump `either` version.
        match self {
            Either::Left(left) => Either::Left(left.index(iter)),
            Either::Right(right) => Either::Right(right.index(iter)),
        }
    }
}


    /// // It works nicely with either.
    /// use itertools::Either;
    ///
    /// let mut either_range = Either::Left(0..1);
    /// range = vec.iter().get(either_range).copied().collect();
    /// assert_eq!(&range, &[3]);
    ///
    /// either_range = Either::Right(2..);
    /// range = vec.iter().get(either_range).copied().collect();
    /// assert_eq!(&range, &[4, 1, 5]);
```
</details>

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/rust-itertools/itertools/pull/891?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 94.70%. Comparing base [(`6814180`)](https://app.codecov.io/gh/rust-itertools/itertools/commit/68141809aabec2816d3250820c8d6608f3b2293f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) to head [(`97c74c3`)](https://app.codecov.io/gh/rust-itertools/itertools/pull/891?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).
> Report is 30 commits behind head on master.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #891      +/-   ##
==========================================
+ Coverage   94.38%   94.70%   +0.31%     
==========================================
  Files          48       49       +1     
  Lines        6665     6802     +137     
==========================================
+ Hits         6291     6442     +151     
+ Misses        374      360      -14     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/rust-itertools/itertools/pull/891?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).

Philippe-Cholet: I rebased on master and added 3 commits: 
- Is there a reason to prefer `Take<Skip>` over `Skip<Take>`? As `Skip<Take>` is simpler defined!
- I don't see how we should really handle a range that includes `usize::MAX` other than with **panic**.
- Clarify when the resulting iterator is `ExactSizeIterator` and/or `DoubleEndedIterator`.


Patch:
diff --git a/src/iter_index.rs b/src/iter_index.rs
new file mode 100644
index 0000000..aadaa72
--- /dev/null
+++ b/src/iter_index.rs
@@ -0,0 +1,116 @@
+use core::iter::{Skip, Take};
+use core::ops::{Range, RangeFrom, RangeFull, RangeInclusive, RangeTo, RangeToInclusive};
+
+#[cfg(doc)]
+use crate::Itertools;
+
+mod private_iter_index {
+    use core::ops;
+
+    pub trait Sealed {}
+
+    impl Sealed for ops::Range<usize> {}
+    impl Sealed for ops::RangeInclusive<usize> {}
+    impl Sealed for ops::RangeTo<usize> {}
+    impl Sealed for ops::RangeToInclusive<usize> {}
+    impl Sealed for ops::RangeFrom<usize> {}
+    impl Sealed for ops::RangeFull {}
+}
+
+/// Used by [`Itertools::get`] to know which iterator
+/// to turn different ranges into.
+pub trait IteratorIndex<I>: private_iter_index::Sealed
+where
+    I: Iterator,
+{
+    /// The type returned for this type of index.
+    type Output: Iterator<Item = I::Item>;
+
+    /// Returns an adapted iterator for the current index.
+    ///
+    /// Prefer calling [`Itertools::get`] instead
+    /// of calling this directly.
+    fn index(self, from: I) -> Self::Output;
+}
+
+impl<I> IteratorIndex<I> for Range<usize>
+where
+    I: Iterator,
+{
+    type Output = Skip<Take<I>>;
+
+    fn index(self, iter: I) -> Self::Output {
+        iter.take(self.end).skip(self.start)
+    }
+}
+
+impl<I> IteratorIndex<I> for RangeInclusive<usize>
+where
+    I: Iterator,
+{
+    type Output = Take<Skip<I>>;
+
+    fn index(self, iter: I) -> Self::Output {
+        // end - start + 1 without overflowing if possible
+        let length = if *self.end() == usize::MAX {
+            assert_ne!(*self.start(), 0);
+            self.end() - self.start() + 1
+        } else {
+            (self.end() + 1).saturating_sub(*self.start())
+        };
+        iter.skip(*self.start()).take(length)
+    }
+}
+
+impl<I> IteratorIndex<I> for RangeTo<usize>
+where
+    I: Iterator,
+{
+    type Output = Take<I>;
+
+    fn index(self, iter: I) -> Self::Output {
+        iter.take(self.end)
+    }
+}
+
+impl<I> IteratorIndex<I> for RangeToInclusive<usize>
+where
+    I: Iterator,
+{
+    type Output = Take<I>;
+
+    fn index(self, iter: I) -> Self::Output {
+        assert_ne!(self.end, usize::MAX);
+        iter.take(self.end + 1)
+    }
+}
+
+impl<I> IteratorIndex<I> for RangeFrom<usize>
+where
+    I: Iterator,
+{
+    type Output = Skip<I>;
+
+    fn index(self, iter: I) -> Self::Output {
+        iter.skip(self.start)
+    }
+}
+
+impl<I> IteratorIndex<I> for RangeFull
+where
+    I: Iterator,
+{
+    type Output = I;
+
+    fn index(self, iter: I) -> Self::Output {
+        iter
+    }
+}
+
+pub fn get<I, R>(iter: I, index: R) -> R::Output
+where
+    I: IntoIterator,
+    R: IteratorIndex<I::IntoIter>,
+{
+    index.index(iter.into_iter())
+}
diff --git a/src/lib.rs b/src/lib.rs
index 931a28f..c439fcf 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -146,6 +146,7 @@ pub mod structs {
 
 /// Traits helpful for using certain `Itertools` methods in generic contexts.
 pub mod traits {
+    pub use crate::iter_index::IteratorIndex;
     pub use crate::tuple_impl::HomogeneousTuple;
 }
 
@@ -194,6 +195,7 @@ mod groupbylazy;
 #[cfg(feature = "use_std")]
 mod grouping_map;
 mod intersperse;
+mod iter_index;
 #[cfg(feature = "use_alloc")]
 mod k_smallest;
 #[cfg(feature = "use_alloc")]
@@ -504,6 +506,56 @@ pub trait Itertools: Iterator {
         intersperse::intersperse_with(self, element)
     }
 
+    /// Returns an iterator over a subsection of the iterator.
+    ///
+    /// Works similarly to [`slice::get`](https://doc.rust-lang.org/std/primitive.slice.html#method.get).
+    ///
+    /// **Panics** for ranges `..=usize::MAX` and `0..=usize::MAX`.
+    ///
+    /// It's a generalisation of [`Iterator::take`] and [`Iterator::skip`],
+    /// and uses these under the hood.
+    /// Therefore, the resulting iterator is:
+    /// - [`ExactSizeIterator`] if the adapted iterator is [`ExactSizeIterator`].
+    /// - [`DoubleEndedIterator`] if the adapted iterator is [`DoubleEndedIterator`] and [`ExactSizeIterator`].
+    ///
+    /// # Unspecified Behavior
+    /// The result of indexing with an exhausted [`core::ops::RangeInclusive`] is unspecified.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// use itertools::Itertools;
+    ///
+    /// let vec = vec![3, 1, 4, 1, 5];
+    ///
+    /// let mut range: Vec<_> =
+    ///         vec.iter().get(1..=3).copied().collect();
+    /// assert_eq!(&range, &[1, 4, 1]);
+    ///
+    /// // It works with other types of ranges, too
+    /// range = vec.iter().get(..2).copied().collect();
+    /// assert_eq!(&range, &[3, 1]);
+    ///
+    /// range = vec.iter().get(0..1).copied().collect();
+    /// assert_eq!(&range, &[3]);
+    ///
+    /// range = vec.iter().get(2..).copied().collect();
+    /// assert_eq!(&range, &[4, 1, 5]);
+    ///
+    /// range = vec.iter().get(..=2).copied().collect();
+    /// assert_eq!(&range, &[3, 1, 4]);
+    ///
+    /// range = vec.iter().get(..).copied().collect();
+    /// assert_eq!(range, vec);
+    /// ```
+    fn get<R>(self, index: R) -> R::Output
+    where
+        Self: Sized,
+        R: traits::IteratorIndex<Self>,
+    {
+        iter_index::get(self, index)
+    }
+
     /// Create an iterator which iterates over both this and the specified
     /// iterator simultaneously, yielding pairs of two optional elements.
     ///


Test patch:
diff --git a/tests/laziness.rs b/tests/laziness.rs
index 2ce29cb..f936dec 100644
--- a/tests/laziness.rs
+++ b/tests/laziness.rs
@@ -63,6 +63,14 @@ must_use_tests! {
     intersperse_with {
         let _ = Panicking.intersperse_with(|| 0);
     }
+    get {
+        let _ = Panicking.get(1..4);
+        let _ = Panicking.get(1..=4);
+        let _ = Panicking.get(1..);
+        let _ = Panicking.get(..4);
+        let _ = Panicking.get(..=4);
+        let _ = Panicking.get(..);
+    }
     zip_longest {
         let _ = Panicking.zip_longest(Panicking);
     }
diff --git a/tests/test_core.rs b/tests/test_core.rs
index b5826ef..32af246 100644
--- a/tests/test_core.rs
+++ b/tests/test_core.rs
@@ -18,6 +18,41 @@ use crate::it::Itertools;
 use core::iter;
 use itertools as it;
 
+#[allow(dead_code)]
+fn get_esi_then_esi<I: ExactSizeIterator + Clone>(it: I) {
+    fn is_esi(_: impl ExactSizeIterator) {}
+    is_esi(it.clone().get(1..4));
+    is_esi(it.clone().get(1..=4));
+    is_esi(it.clone().get(1..));
+    is_esi(it.clone().get(..4));
+    is_esi(it.clone().get(..=4));
+    is_esi(it.get(..));
+}
+
+#[allow(dead_code)]
+fn get_dei_esi_then_dei_esi<I: DoubleEndedIterator + ExactSizeIterator + Clone>(it: I) {
+    fn is_dei_esi(_: impl DoubleEndedIterator + ExactSizeIterator) {}
+    is_dei_esi(it.clone().get(1..4));
+    is_dei_esi(it.clone().get(1..=4));
+    is_dei_esi(it.clone().get(1..));
+    is_dei_esi(it.clone().get(..4));
+    is_dei_esi(it.clone().get(..=4));
+    is_dei_esi(it.get(..));
+}
+
+#[test]
+fn get_1_max() {
+    let mut it = (0..5).get(1..=usize::MAX);
+    assert_eq!(it.next(), Some(1));
+    assert_eq!(it.next_back(), Some(4));
+}
+
+#[test]
+#[should_panic]
+fn get_full_range_inclusive() {
+    let _it = (0..5).get(0..=usize::MAX);
+}
+
 #[test]
 fn product0() {
     let mut prod = iproduct!();