Repository: rust-itertools/itertools (rust)

Issue:
Merge `MergeBy` and `MergeJoinBy`
It seems that `merge_join_by`/`MergeJoinBy` is actually a more generic variant of `merge`/`MergeBy`.

However, currently, the `MergeJoinBy` and `MergeBy` are implemented independently (`PutBack` vs `Peekable`, fused behavior, requiring `Ordering` vs `bool`). The past showed that uniform implementations may be better.

This change should not introduce breaking changes for users: `merge`, `merge_by` and `merge_join_by` should stay usable as they are. 

However, I'd imagine that the general `merge_join_by` could also accept a comparator returning `bool` (instead of `Ordering`) - this would free the users from having to deal with the `EitherOrBoth::Both` case. I.e. I suggest that users should be able to write:

```
use itertools; // 0.10.5
use itertools::{Either, EitherOrBoth};

fn main() {
    let numbers = [1,2,3,4,5];
    let strings = ["one", "two", "three", "four"];
    
    for x in itertools::merge_join_by(
        numbers,
        strings,
        |number, string| number.cmp(&string.len()),
    ) {
        match x {
            EitherOrBoth::Left(number) => {dbg!(number);},
            EitherOrBoth::Right(string) => {dbg!(string);},
            EitherOrBoth::Both(number, string) => {dbg!(number, string);},
        };
    }
    
    for x in itertools::merge_join_by(
        numbers,
        strings,
        |number, string| number < &string.len(), // <-- Note that we return bool instead of Ordering
    ) {
        match x {
            Either::Left(number) => {dbg!(number);},
            Either::Right(string) => {dbg!(string);},
            // <-- Note that we do not need to deal with the "Both" case, as our comparator returns bool
        };
    }
}
```

When we tackle this, we should - as a first step - probably move all the `merge`-related functions into an own module.

Pull request description:
Merge `Merge/MergeBy/MergeJoinBy` implementations (v2)
Cleaner alternative to the experimental/messy #711. Closes #701.
@phimuemue I would additionally rename `InternalMergeJoinBy` to `MergeBy`, making `MergeJoinBy` an alias of `MergeBy` instead. But I would like your opinion on this first.
I recently found a way to not add a fourth type to `MergeJoinBy` (added to the old PR) but another difference is that I do not wrap with `MergeFuncT` anymore.

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 45e399a..c412fc8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -9,11 +9,9 @@ on:
 
 jobs:
   check:
-    name: check
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        build: [msrv, stable]
         features:
           [
             "",
@@ -21,24 +19,22 @@ jobs:
             "--no-default-features --features use_alloc",
             "--all-targets --all-features",
           ]
-        include:
-          - build: msrv
-            rust: 1.62.1
-          - build: stable
-            rust: stable
-        exclude:
-          - build: msrv
-            # we only care about the MSRV with respect to the lib target
-            features: "--all-targets --all-features"
     steps:
       - uses: actions/checkout@v4
-      - uses: dtolnay/rust-toolchain@master
-        with:
-          toolchain: ${{ matrix.rust }}
+      - uses: dtolnay/rust-toolchain@stable
       - run: cargo check ${{ matrix.features }}
 
+  msrv:
+    runs-on: ubuntu-latest
+    env:
+      CARGO_NET_GIT_FETCH_WITH_CLI: true
+    steps:
+      - uses: actions/checkout@v4
+      - uses: taiki-e/install-action@cargo-no-dev-deps
+      - uses: dtolnay/rust-toolchain@1.43.1
+      - run: cargo no-dev-deps check
+
   test:
-    name: test
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v4
@@ -62,7 +58,6 @@ jobs:
     if: success()
     runs-on: ubuntu-latest
     needs: [check, test]
-
     steps:
       - name: Mark the job as successful
         run: exit 0
diff --git a/Cargo.toml b/Cargo.toml
index a6dfefb..2a023a1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -16,7 +16,7 @@ exclude = ["/bors.toml"]
 
 edition = "2018"
 
-rust-version = "1.36.0"
+rust-version = "1.43.1"
 
 [lib]
 bench = false
diff --git a/src/lib.rs b/src/lib.rs
index 7dc4037..458ffe6 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -42,7 +42,7 @@
 //!
 //! ## Rust Version
 //!
-//! This version of itertools requires Rust 1.36 or later.
+//! This version of itertools requires Rust 1.43.1 or later.
 #![doc(html_root_url = "https://docs.rs/itertools/0.11/")]
 
 #[cfg(not(feature = "use_std"))]
@@ -1075,7 +1075,6 @@ pub trait Itertools: Iterator {
     where
         J: IntoIterator,
         F: FnMut(&Self::Item, &J::Item) -> T,
-        T: merge_join::OrderingOrBool<Self::Item, J::Item>,
         Self: Sized,
     {
         merge_join_by(self, other, cmp_fn)
@@ -2373,7 +2372,9 @@ pub trait Itertools: Iterator {
     /// For example the sequence *Ok(1), Ok(2), Ok(3)* will result in a
     /// computation like this:
     ///
-    /// ```ignore
+    /// ```no_run
+    /// # let start = 0;
+    /// # let f = |x, y| x + y;
     /// let mut accum = start;
     /// accum = f(accum, 1);
     /// accum = f(accum, 2);
@@ -3981,9 +3982,10 @@ where
 /// **Panics** on assertion failure with a message that shows the
 /// two iteration elements.
 ///
-/// ```ignore
+/// ```should_panic
+/// # use itertools::assert_equal;
 /// assert_equal("exceed".split('c'), "excess".split('c'));
-/// // ^PANIC: panicked at 'Failed assertion Some("eed") == Some("ess") for iteration 1',
+/// // ^PANIC: panicked at 'Failed assertion Some("eed") == Some("ess") for iteration 1'.
 /// ```
 pub fn assert_equal<I, J>(a: I, b: J)
 where
diff --git a/src/merge_join.rs b/src/merge_join.rs
index 1769aaf..c831591 100644
--- a/src/merge_join.rs
+++ b/src/merge_join.rs
@@ -1,7 +1,7 @@
 use std::cmp::Ordering;
 use std::fmt;
-use std::iter::Fuse;
-use std::iter::{FusedIterator, Peekable};
+use std::iter::{Fuse, FusedIterator};
+use std::marker::PhantomData;
 
 use either::Either;
 
@@ -11,19 +11,9 @@ use crate::size_hint::{self, SizeHint};
 #[cfg(doc)]
 use crate::Itertools;
 
-pub trait MergePredicate<T> {
-    fn merge_pred(&mut self, a: &T, b: &T) -> bool;
-}
-
 #[derive(Clone, Debug)]
 pub struct MergeLte;
 
-impl<T: PartialOrd> MergePredicate<T> for MergeLte {
-    fn merge_pred(&mut self, a: &T, b: &T) -> bool {
-        a <= b
-    }
-}
-
 /// An iterator adaptor that merges the two base iterators in ascending order.
 /// If both base iterators are sorted (ascending), the result is sorted.
 ///
@@ -62,30 +52,10 @@ where
 ///
 /// See [`.merge_by()`](crate::Itertools::merge_by) for more information.
 #[must_use = "iterator adaptors are lazy and do nothing unless consumed"]
-pub struct MergeBy<I, J, F>
-where
-    I: Iterator,
-    J: Iterator<Item = I::Item>,
-{
-    a: Peekable<I>,
-    b: Peekable<J>,
-    fused: Option<bool>,
-    cmp: F,
-}
-
-impl<I, J, F> fmt::Debug for MergeBy<I, J, F>
-where
-    I: Iterator + fmt::Debug,
-    J: Iterator<Item = I::Item> + fmt::Debug,
-    I::Item: fmt::Debug,
-{
-    debug_fmt_fields!(MergeBy, a, b);
-}
-
-impl<T, F: FnMut(&T, &T) -> bool> MergePredicate<T> for F {
-    fn merge_pred(&mut self, a: &T, b: &T) -> bool {
-        self(a, b)
-    }
+pub struct MergeBy<I: Iterator, J: Iterator, F> {
+    left: PutBack<Fuse<I>>,
+    right: PutBack<Fuse<J>>,
+    cmp_fn: F,
 }
 
 /// Create a `MergeBy` iterator.
@@ -93,72 +63,14 @@ pub fn merge_by_new<I, J, F>(a: I, b: J, cmp: F) -> MergeBy<I::IntoIter, J::Into
 where
     I: IntoIterator,
     J: IntoIterator<Item = I::Item>,
-    F: MergePredicate<I::Item>,
 {
     MergeBy {
-        a: a.into_iter().peekable(),
-        b: b.into_iter().peekable(),
-        fused: None,
-        cmp,
+        left: put_back(a.into_iter().fuse()),
+        right: put_back(b.into_iter().fuse()),
+        cmp_fn: cmp,
     }
 }
 
-impl<I, J, F> Clone for MergeBy<I, J, F>
-where
-    I: Iterator,
-    J: Iterator<Item = I::Item>,
-    Peekable<I>: Clone,
-    Peekable<J>: Clone,
-    F: Clone,
-{
-    clone_fields!(a, b, fused, cmp);
-}
-
-impl<I, J, F> Iterator for MergeBy<I, J, F>
-where
-    I: Iterator,
-    J: Iterator<Item = I::Item>,
-    F: MergePredicate<I::Item>,
-{
-    type Item = I::Item;
-
-    fn next(&mut self) -> Option<Self::Item> {
-        let less_than = match self.fused {
-            Some(lt) => lt,
-            None => match (self.a.peek(), self.b.peek()) {
-                (Some(a), Some(b)) => self.cmp.merge_pred(a, b),
-                (Some(_), None) => {
-                    self.fused = Some(true);
-                    true
-                }
-                (None, Some(_)) => {
-                    self.fused = Some(false);
-                    false
-                }
-                (None, None) => return None,
-            },
-        };
-        if less_than {
-            self.a.next()
-        } else {
-            self.b.next()
-        }
-    }
-
-    fn size_hint(&self) -> (usize, Option<usize>) {
-        // Not ExactSizeIterator because size may be larger than usize
-        size_hint::add(self.a.size_hint(), self.b.size_hint())
-    }
-}
-
-impl<I, J, F> FusedIterator for MergeBy<I, J, F>
-where
-    I: FusedIterator,
-    J: FusedIterator<Item = I::Item>,
-    F: MergePredicate<I::Item>,
-{
-}
-
 /// Return an iterator adaptor that merge-joins items from the two base iterators in ascending order.
 ///
 /// [`IntoIterator`] enabled version of [`Itertools::merge_join_by`].
@@ -171,23 +83,29 @@ where
     I: IntoIterator,
     J: IntoIterator,
     F: FnMut(&I::Item, &J::Item) -> T,
-    T: OrderingOrBool<I::Item, J::Item>,
 {
-    MergeJoinBy {
+    MergeBy {
         left: put_back(left.into_iter().fuse()),
         right: put_back(right.into_iter().fuse()),
-        cmp_fn,
+        cmp_fn: MergeFuncLR(cmp_fn, PhantomData),
     }
 }
 
 /// An iterator adaptor that merge-joins items from the two base iterators in ascending order.
 ///
 /// See [`.merge_join_by()`](crate::Itertools::merge_join_by) for more information.
-#[must_use = "iterator adaptors are lazy and do nothing unless consumed"]
-pub struct MergeJoinBy<I: Iterator, J: Iterator, F> {
-    left: PutBack<Fuse<I>>,
-    right: PutBack<Fuse<J>>,
-    cmp_fn: F,
+pub type MergeJoinBy<I, J, F> =
+    MergeBy<I, J, MergeFuncLR<F, <F as FuncLR<<I as Iterator>::Item, <J as Iterator>::Item>>::T>>;
+
+#[derive(Clone, Debug)]
+pub struct MergeFuncLR<F, T>(F, PhantomData<T>);
+
+pub trait FuncLR<L, R> {
+    type T;
+}
+
+impl<L, R, T, F: FnMut(&L, &R) -> T> FuncLR<L, R> for F {
+    type T = T;
 }
 
 pub trait OrderingOrBool<L, R> {
@@ -197,11 +115,11 @@ pub trait OrderingOrBool<L, R> {
     // "merge" never returns (Some(...), Some(...), ...) so Option<Either<I::Item, J::Item>>
     // is appealing but it is always followed by two put_backs, so we think the compiler is
     // smart enough to optimize it. Or we could move put_backs into "merge".
-    fn merge(self, left: L, right: R) -> (Option<L>, Option<R>, Self::MergeResult);
+    fn merge(&mut self, left: L, right: R) -> (Option<L>, Option<R>, Self::MergeResult);
     fn size_hint(left: SizeHint, right: SizeHint) -> SizeHint;
 }
 
-impl<L, R> OrderingOrBool<L, R> for Ordering {
+impl<L, R, F: FnMut(&L, &R) -> Ordering> OrderingOrBool<L, R> for MergeFuncLR<F, Ordering> {
     type MergeResult = EitherOrBoth<L, R>;
     fn left(left: L) -> Self::MergeResult {
         EitherOrBoth::Left(left)
@@ -209,8 +127,8 @@ impl<L, R> OrderingOrBool<L, R> for Ordering {
     fn right(right: R) -> Self::MergeResult {
         EitherOrBoth::Right(right)
     }
-    fn merge(self, left: L, right: R) -> (Option<L>, Option<R>, Self::MergeResult) {
-        match self {
+    fn merge(&mut self, left: L, right: R) -> (Option<L>, Option<R>, Self::MergeResult) {
+        match self.0(&left, &right) {
             Ordering::Equal => (None, None, EitherOrBoth::Both(left, right)),
             Ordering::Less => (None, Some(right), EitherOrBoth::Left(left)),
             Ordering::Greater => (Some(left), None, EitherOrBoth::Right(right)),
@@ -228,7 +146,7 @@ impl<L, R> OrderingOrBool<L, R> for Ordering {
     }
 }
 
-impl<L, R> OrderingOrBool<L, R> for bool {
+impl<L, R, F: FnMut(&L, &R) -> bool> OrderingOrBool<L, R> for MergeFuncLR<F, bool> {
     type MergeResult = Either<L, R>;
     fn left(left: L) -> Self::MergeResult {
         Either::Left(left)
@@ -236,8 +154,8 @@ impl<L, R> OrderingOrBool<L, R> for bool {
     fn right(right: R) -> Self::MergeResult {
         Either::Right(right)
     }
-    fn merge(self, left: L, right: R) -> (Option<L>, Option<R>, Self::MergeResult) {
-        if self {
+    fn merge(&mut self, left: L, right: R) -> (Option<L>, Option<R>, Self::MergeResult) {
+        if self.0(&left, &right) {
             (None, Some(right), Either::Left(left))
         } else {
             (Some(left), None, Either::Right(right))
@@ -249,7 +167,49 @@ impl<L, R> OrderingOrBool<L, R> for bool {
     }
 }
 
-impl<I, J, F> Clone for MergeJoinBy<I, J, F>
+impl<T, F: FnMut(&T, &T) -> bool> OrderingOrBool<T, T> for F {
+    type MergeResult = T;
+    fn left(left: T) -> Self::MergeResult {
+        left
+    }
+    fn right(right: T) -> Self::MergeResult {
+        right
+    }
+    fn merge(&mut self, left: T, right: T) -> (Option<T>, Option<T>, Self::MergeResult) {
+        if self(&left, &right) {
+            (None, Some(right), left)
+        } else {
+            (Some(left), None, right)
+        }
+    }
+    fn size_hint(left: SizeHint, right: SizeHint) -> SizeHint {
+        // Not ExactSizeIterator because size may be larger than usize
+        size_hint::add(left, right)
+    }
+}
+
+impl<T: PartialOrd> OrderingOrBool<T, T> for MergeLte {
+    type MergeResult = T;
+    fn left(left: T) -> Self::MergeResult {
+        left
+    }
+    fn right(right: T) -> Self::MergeResult {
+        right
+    }
+    fn merge(&mut self, left: T, right: T) -> (Option<T>, Option<T>, Self::MergeResult) {
+        if left <= right {
+            (None, Some(right), left)
+        } else {
+            (Some(left), None, right)
+        }
+    }
+    fn size_hint(left: SizeHint, right: SizeHint) -> SizeHint {
+        // Not ExactSizeIterator because size may be larger than usize
+        size_hint::add(left, right)
+    }
+}
+
+impl<I, J, F> Clone for MergeBy<I, J, F>
 where
     I: Iterator,
     J: Iterator,
@@ -260,32 +220,31 @@ where
     clone_fields!(left, right, cmp_fn);
 }
 
-impl<I, J, F> fmt::Debug for MergeJoinBy<I, J, F>
+impl<I, J, F> fmt::Debug for MergeBy<I, J, F>
 where
     I: Iterator + fmt::Debug,
     I::Item: fmt::Debug,
     J: Iterator + fmt::Debug,
     J::Item: fmt::Debug,
 {
-    debug_fmt_fields!(MergeJoinBy, left, right);
+    debug_fmt_fields!(MergeBy, left, right);
 }
 
-impl<I, J, F, T> Iterator for MergeJoinBy<I, J, F>
+impl<I, J, F> Iterator for MergeBy<I, J, F>
 where
     I: Iterator,
     J: Iterator,
-    F: FnMut(&I::Item, &J::Item) -> T,
-    T: OrderingOrBool<I::Item, J::Item>,
+    F: OrderingOrBool<I::Item, J::Item>,
 {
-    type Item = T::MergeResult;
+    type Item = F::MergeResult;
 
     fn next(&mut self) -> Option<Self::Item> {
         match (self.left.next(), self.right.next()) {
             (None, None) => None,
-            (Some(left), None) => Some(T::left(left)),
-            (None, Some(right)) => Some(T::right(right)),
+            (Some(left), None) => Some(F::left(left)),
+            (None, Some(right)) => Some(F::right(right)),
             (Some(left), Some(right)) => {
-                let (left, right, next) = (self.cmp_fn)(&left, &right).merge(left, right);
+                let (left, right, next) = self.cmp_fn.merge(left, right);
                 if let Some(left) = left {
                     self.left.put_back(left);
                 }
@@ -298,7 +257,7 @@ where
     }
 
     fn size_hint(&self) -> SizeHint {
-        T::size_hint(self.left.size_hint(), self.right.size_hint())
+        F::size_hint(self.left.size_hint(), self.right.size_hint())
     }
 
     fn count(mut self) -> usize {
@@ -310,7 +269,7 @@ where
                 (None, Some(_right)) => break count + 1 + self.right.into_parts().1.count(),
                 (Some(left), Some(right)) => {
                     count += 1;
-                    let (left, right, _) = (self.cmp_fn)(&left, &right).merge(left, right);
+                    let (left, right, _) = self.cmp_fn.merge(left, right);
                     if let Some(left) = left {
                         self.left.put_back(left);
                     }
@@ -328,13 +287,13 @@ where
             match (self.left.next(), self.right.next()) {
                 (None, None) => break previous_element,
                 (Some(left), None) => {
-                    break Some(T::left(self.left.into_parts().1.last().unwrap_or(left)))
+                    break Some(F::left(self.left.into_parts().1.last().unwrap_or(left)))
                 }
                 (None, Some(right)) => {
-                    break Some(T::right(self.right.into_parts().1.last().unwrap_or(right)))
+                    break Some(F::right(self.right.into_parts().1.last().unwrap_or(right)))
                 }
                 (Some(left), Some(right)) => {
-                    let (left, right, elem) = (self.cmp_fn)(&left, &right).merge(left, right);
+                    let (left, right, elem) = self.cmp_fn.merge(left, right);
                     if let Some(left) = left {
                         self.left.put_back(left);
                     }
@@ -355,10 +314,10 @@ where
             n -= 1;
             match (self.left.next(), self.right.next()) {
                 (None, None) => break None,
-                (Some(_left), None) => break self.left.nth(n).map(T::left),
-                (None, Some(_right)) => break self.right.nth(n).map(T::right),
+                (Some(_left), None) => break self.left.nth(n).map(F::left),
+                (None, Some(_right)) => break self.right.nth(n).map(F::right),
                 (Some(left), Some(right)) => {
-                    let (left, right, _) = (self.cmp_fn)(&left, &right).merge(left, right);
+                    let (left, right, _) = self.cmp_fn.merge(left, right);
                     if let Some(left) = left {
                         self.left.put_back(left);
                     }
@@ -370,3 +329,11 @@ where
         }
     }
 }
+
+impl<I, J, F> FusedIterator for MergeBy<I, J, F>
+where
+    I: Iterator,
+    J: Iterator,
+    F: OrderingOrBool<I::Item, J::Item>,
+{
+}
diff --git a/src/size_hint.rs b/src/size_hint.rs
index 7ba6f44..857e0c4 100644
--- a/src/size_hint.rs
+++ b/src/size_hint.rs
@@ -38,20 +38,6 @@ pub fn sub_scalar(sh: SizeHint, x: usize) -> SizeHint {
 }
 
 /// Multiply `SizeHint` correctly
-///
-/// ```ignore
-/// use std::usize;
-/// use itertools::size_hint;
-///
-/// assert_eq!(size_hint::mul((3, Some(4)), (3, Some(4))),
-///            (9, Some(16)));
-///
-/// assert_eq!(size_hint::mul((3, Some(4)), (usize::MAX, None)),
-///            (usize::MAX, None));
-///
-/// assert_eq!(size_hint::mul((3, None), (0, Some(0))),
-///            (0, Some(0)));
-/// ```
 #[inline]
 pub fn mul(a: SizeHint, b: SizeHint) -> SizeHint {
     let low = a.0.saturating_mul(b.0);
@@ -100,3 +86,10 @@ pub fn min(a: SizeHint, b: SizeHint) -> SizeHint {
     };
     (lower, upper)
 }
+
+#[test]
+fn mul_size_hints() {
+    assert_eq!(mul((3, Some(4)), (3, Some(4))), (9, Some(16)));
+    assert_eq!(mul((3, Some(4)), (usize::MAX, None)), (usize::MAX, None));
+    assert_eq!(mul((3, None), (0, Some(0))), (0, Some(0)));
+}
diff --git a/src/tuple_impl.rs b/src/tuple_impl.rs
index 0aea977..ba1c69d 100644
--- a/src/tuple_impl.rs
+++ b/src/tuple_impl.rs
@@ -3,7 +3,6 @@
 use std::iter::Cycle;
 use std::iter::Fuse;
 use std::iter::FusedIterator;
-use std::iter::Take;
 use std::marker::PhantomData;
 
 // `HomogeneousTuple` is a public facade for `TupleCollect`, allowing
@@ -185,6 +184,20 @@ where
         }
         None
     }
+
+    fn size_hint(&self) -> (usize, Option<usize>) {
+        // At definition, `T::num_items() - 1` are collected
+        // so each remaining item in `iter` will lead to an item.
+        self.iter.size_hint()
+    }
+}
+
+impl<I, T> ExactSizeIterator for TupleWindows<I, T>
+where
+    I: ExactSizeIterator<Item = T::Item>,
+    T: HomogeneousTuple + Clone,
+    T::Item: Clone,
+{
 }
 
 impl<I, T> FusedIterator for TupleWindows<I, T>
@@ -208,7 +221,8 @@ where
     I: Iterator<Item = T::Item> + Clone,
     T: TupleCollect + Clone,
 {
-    iter: Take<TupleWindows<Cycle<I>, T>>,
+    iter: TupleWindows<Cycle<I>, T>,
+    len: usize,
     phantom_data: PhantomData<T>,
 }
 
@@ -219,10 +233,11 @@ where
     T::Item: Clone,
 {
     let len = iter.len();
-    let iter = tuple_windows(iter.cycle()).take(len);
+    let iter = tuple_windows(iter.cycle());
 
     CircularTupleWindows {
         iter,
+        len,
         phantom_data: PhantomData {},
     }
 }
@@ -236,10 +251,35 @@ where
     type Item = T;
 
     fn next(&mut self) -> Option<Self::Item> {
-        self.iter.next()
+        if self.len != 0 {
+            self.len -= 1;
+            self.iter.next()
+        } else {
+            None
+        }
+    }
+
+    fn size_hint(&self) -> (usize, Option<usize>) {
+        (self.len, Some(self.len))
     }
 }
 
+impl<I, T> ExactSizeIterator for CircularTupleWindows<I, T>
+where
+    I: Iterator<Item = T::Item> + Clone,
+    T: TupleCollect + Clone,
+    T::Item: Clone,
+{
+}
+
+impl<I, T> FusedIterator for CircularTupleWindows<I, T>
+where
+    I: Iterator<Item = T::Item> + Clone,
+    T: TupleCollect + Clone,
+    T::Item: Clone,
+{
+}
+
 pub trait TupleCollect: Sized {
     type Item;
     type Buffer: Default + AsRef<[Option<Self::Item>]> + AsMut<[Option<Self::Item>]>;


Test patch:
diff --git a/tests/quick.rs b/tests/quick.rs
index 1a19d60..3dc23d7 100644
--- a/tests/quick.rs
+++ b/tests/quick.rs
@@ -1117,6 +1117,10 @@ quickcheck! {
         true
     }
 
+    fn circular_tuple_windows_exact_size(a: Vec<u8>) -> bool {
+        exact_size(a.iter().circular_tuple_windows::<(_, _, _, _)>())
+    }
+
     fn equal_tuple_windows_1(a: Vec<u8>) -> bool {
         let x = a.windows(1).map(|s| (&s[0], ));
         let y = a.iter().tuple_windows::<(_,)>();
@@ -1141,6 +1145,14 @@ quickcheck! {
         itertools::equal(x, y)
     }
 
+    fn tuple_windows_exact_size_1(a: Vec<u8>) -> bool {
+        exact_size(a.iter().tuple_windows::<(_,)>())
+    }
+
+    fn tuple_windows_exact_size_4(a: Vec<u8>) -> bool {
+        exact_size(a.iter().tuple_windows::<(_, _, _, _)>())
+    }
+
     fn equal_tuples_1(a: Vec<u8>) -> bool {
         let x = a.chunks(1).map(|s| (&s[0], ));
         let y = a.iter().tuples::<(_,)>();