Repository: rust-itertools/itertools (rust)

Issue:
multizip equivalent for unzip
It would be nice to have something similar to `izip!`/`multizip` for `unzip`, e.g. `A, B, C, D, A, B, C, D, ...` -> `([A, A, A, [...], B, B, B, [...], [...])`

Pull request description:
Implement `Itertools::multiunzip`
Simple implementation of [Iterator::unzip](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.unzip) but for tuples of sizes 0-12.

This requires adding a new public trait to be able to express the dependency between the iterator item tuple to the collection output tuple. But given that a `multiunzip` function is added to the Itertools trait the `MultiUnzip` trait does not have to be imported to make use of this.

Another option would be a macro but that would require giving it some extra syntax to declare the arity/return type which seems suboptimal, while this approach just works as a method call akin to std's iterator unzip.

Closes #362

Patch:
diff --git a/src/lib.rs b/src/lib.rs
index 7dd6241..96600f5 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -179,6 +179,7 @@ pub use crate::repeatn::repeat_n;
 #[allow(deprecated)]
 pub use crate::sources::{repeat_call, unfold, iterate};
 pub use crate::with_position::Position;
+pub use crate::unziptuple::{multiunzip, MultiUnzip};
 pub use crate::ziptuple::multizip;
 mod adaptors;
 mod either_or_both;
@@ -237,6 +238,7 @@ mod tuple_impl;
 mod duplicates_impl;
 #[cfg(feature = "use_std")]
 mod unique_impl;
+mod unziptuple;
 mod with_position;
 mod zip_eq_impl;
 mod zip_longest;
@@ -3401,6 +3403,33 @@ pub trait Itertools : Iterator {
     {
         self.map(f).counts()
     }
+
+    /// Converts an iterator of tuples into a tuple of containers.
+    ///
+    /// `unzip()` consumes an entire iterator of n-ary tuples, producing `n` collections, one for each
+    /// column.
+    ///
+    /// This function is, in some sense, the opposite of [`multizip`].
+    /// 
+    /// ```
+    /// use itertools::Itertools;
+    ///
+    /// let inputs = vec![(1, 2, 3), (4, 5, 6), (7, 8, 9)];
+    ///
+    /// let (a, b, c): (Vec<_>, Vec<_>, Vec<_>) = inputs
+    ///     .into_iter()
+    ///     .multiunzip();
+    ///
+    /// assert_eq!(a, vec![1, 4, 7]);
+    /// assert_eq!(b, vec![2, 5, 8]);
+    /// assert_eq!(c, vec![3, 6, 9]);
+    /// ```
+    fn multiunzip<FromI>(self) -> FromI
+    where
+        Self: Sized + MultiUnzip<FromI>,
+    {
+        MultiUnzip::multiunzip(self)
+    }
 }
 
 impl<T: ?Sized> Itertools for T where T: Iterator { }
diff --git a/src/unziptuple.rs b/src/unziptuple.rs
new file mode 100644
index 0000000..f468f05
--- /dev/null
+++ b/src/unziptuple.rs
@@ -0,0 +1,80 @@
+/// Converts an iterator of tuples into a tuple of containers.
+///
+/// `unzip()` consumes an entire iterator of n-ary tuples, producing `n` collections, one for each
+/// column.
+///
+/// This function is, in some sense, the opposite of [`multizip`].
+///
+/// ```
+/// use itertools::multiunzip;
+///
+/// let inputs = vec![(1, 2, 3), (4, 5, 6), (7, 8, 9)];
+///
+/// let (a, b, c): (Vec<_>, Vec<_>, Vec<_>) = multiunzip(inputs);
+///
+/// assert_eq!(a, vec![1, 4, 7]);
+/// assert_eq!(b, vec![2, 5, 8]);
+/// assert_eq!(c, vec![3, 6, 9]);
+/// ```
+///
+/// [`multizip`]: crate::multizip
+pub fn multiunzip<FromI, I>(i: I) -> FromI
+where
+    I: IntoIterator,
+    I::IntoIter: MultiUnzip<FromI>,
+{
+    i.into_iter().multiunzip()
+}
+
+/// An iterator that can be unzipped into multiple collections.
+///
+/// See [`.multiunzip()`](crate::Itertools::multiunzip) for more information.
+pub trait MultiUnzip<FromI>: Iterator {
+    /// Unzip this iterator into multiple collections.
+    fn multiunzip(self) -> FromI;
+}
+
+macro_rules! impl_unzip_iter {
+    ($($T:ident => $FromT:ident),*) => (
+        #[allow(non_snake_case)]
+        impl<IT: Iterator<Item = ($($T,)*)>, $($T, $FromT: Default + Extend<$T>),* > MultiUnzip<($($FromT,)*)> for IT {
+            fn multiunzip(self) -> ($($FromT,)*) {
+                // This implementation mirrors the logic of Iterator::unzip as close as possible.
+                // Unfortunately a lot of the used api there is still unstable represented by
+                // the commented out parts that follow.
+                //
+                // https://doc.rust-lang.org/src/core/iter/traits/iterator.rs.html#2816-2844
+
+                let mut res = ($($FromT::default(),)*);
+                let ($($FromT,)*) = &mut res;
+
+                // Still unstable #72631
+                // let (lower_bound, _) = self.size_hint();
+                // if lower_bound > 0 {
+                //     $($FromT.extend_reserve(lower_bound);)*
+                // }
+
+                self.fold((), |(), ($($T,)*)| {
+                    // Still unstable #72631
+                    // $( $FromT.extend_one($T); )*
+                    $( $FromT.extend(std::iter::once($T)); )*
+                });
+                res
+            }
+        }
+    );
+}
+
+impl_unzip_iter!();
+impl_unzip_iter!(A => FromA);
+impl_unzip_iter!(A => FromA, B => FromB);
+impl_unzip_iter!(A => FromA, B => FromB, C => FromC);
+impl_unzip_iter!(A => FromA, B => FromB, C => FromC, D => FromD);
+impl_unzip_iter!(A => FromA, B => FromB, C => FromC, D => FromD, E => FromE);
+impl_unzip_iter!(A => FromA, B => FromB, C => FromC, D => FromD, E => FromE, F => FromF);
+impl_unzip_iter!(A => FromA, B => FromB, C => FromC, D => FromD, E => FromE, F => FromF, G => FromG);
+impl_unzip_iter!(A => FromA, B => FromB, C => FromC, D => FromD, E => FromE, F => FromF, G => FromG, H => FromH);
+impl_unzip_iter!(A => FromA, B => FromB, C => FromC, D => FromD, E => FromE, F => FromF, G => FromG, H => FromH, I => FromI);
+impl_unzip_iter!(A => FromA, B => FromB, C => FromC, D => FromD, E => FromE, F => FromF, G => FromG, H => FromH, I => FromI, J => FromJ);
+impl_unzip_iter!(A => FromA, B => FromB, C => FromC, D => FromD, E => FromE, F => FromF, G => FromG, H => FromH, I => FromI, J => FromJ, K => FromK);
+impl_unzip_iter!(A => FromA, B => FromB, C => FromC, D => FromD, E => FromE, F => FromF, G => FromG, H => FromH, I => FromI, J => FromJ, K => FromK, L => FromL);


Test patch:
diff --git a/tests/test_std.rs b/tests/test_std.rs
index 7cda9b5..1cac30d 100644
--- a/tests/test_std.rs
+++ b/tests/test_std.rs
@@ -1080,3 +1080,12 @@ fn exactly_one_question_mark_return() -> Result<(), ExactlyOneError<std::slice::
     [].iter().exactly_one()?;
     Ok(())
 }
+
+#[test]
+fn multiunzip() {
+    let (a, b, c): (Vec<_>, Vec<_>, Vec<_>) = [(0, 1, 2), (3, 4, 5), (6, 7, 8)].iter().cloned().multiunzip();    
+    assert_eq!((a, b, c), (vec![0, 3, 6], vec![1, 4, 7], vec![2, 5, 8]));
+    let (): () = [(), (), ()].iter().cloned().multiunzip();
+    let t: (Vec<_>, Vec<_>, Vec<_>, Vec<_>, Vec<_>, Vec<_>, Vec<_>, Vec<_>, Vec<_>, Vec<_>, Vec<_>, Vec<_>) = [(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)].iter().cloned().multiunzip();    
+    assert_eq!(t, (vec![0], vec![1], vec![2], vec![3], vec![4], vec![5], vec![6], vec![7], vec![8], vec![9], vec![10], vec![11]));
+}
\ No newline at end of file