From ed13b5c5d2bf199672dd6d9128c65a01cd4301b0 Mon Sep 17 00:00:00 2001 From: Philippe-Cholet Date: Tue, 22 Aug 2023 10:06:26 +0200 Subject: [PATCH 01/11] `Combinations::n_and_count` `Powerset::count` will need both `n` and the count of combinations. --- src/combinations.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/combinations.rs b/src/combinations.rs index a5b34fc95..4b0742a0d 100644 --- a/src/combinations.rs +++ b/src/combinations.rs @@ -77,6 +77,12 @@ impl Combinations { self.pool.prefill(k); } } + + pub(crate) fn n_and_count(self) -> (usize, usize) { + let Self { indices, pool, first } = self; + let n = pool.count(); + (n, remaining_for(n, first, &indices).unwrap()) + } } impl Iterator for Combinations @@ -128,10 +134,9 @@ impl Iterator for Combinations (low, upp) } + #[inline] fn count(self) -> usize { - let Self { indices, pool, first } = self; - let n = pool.count(); - remaining_for(n, first, &indices).unwrap() + self.n_and_count().1 } } From e3b17a393a7fac44d4a4a320afd6f42c15ed45ef Mon Sep 17 00:00:00 2001 From: Philippe-Cholet Date: Tue, 22 Aug 2023 10:07:46 +0200 Subject: [PATCH 02/11] `Powerset::count` --- src/powerset.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/powerset.rs b/src/powerset.rs index 4175f013b..f5d79d807 100644 --- a/src/powerset.rs +++ b/src/powerset.rs @@ -3,7 +3,7 @@ use std::iter::FusedIterator; use std::usize; use alloc::vec::Vec; -use super::combinations::{Combinations, combinations}; +use super::combinations::{Combinations, checked_binomial, combinations}; use super::size_hint; /// An iterator to iterate through the powerset of the elements from an iterator. @@ -81,6 +81,12 @@ impl Iterator for Powerset (0, self_total.1) } } + + fn count(self) -> usize { + let k = self.combs.k(); + let (n, combs_count) = self.combs.n_and_count(); + combs_count + (k + 1..=n).map(|i| checked_binomial(n, i).unwrap()).sum::() + } } impl FusedIterator for Powerset From 516d24ff2673f7132018bbc06a8a1e8c2a1b7863 Mon Sep 17 00:00:00 2001 From: Philippe-Cholet Date: Tue, 22 Aug 2023 10:17:39 +0200 Subject: [PATCH 03/11] Test `Powerset::count` --- tests/test_std.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_std.rs b/tests/test_std.rs index 3d6d66cf1..c6034122f 100644 --- a/tests/test_std.rs +++ b/tests/test_std.rs @@ -989,6 +989,19 @@ fn powerset() { assert_eq!((0..4).powerset().count(), 1 << 4); assert_eq!((0..8).powerset().count(), 1 << 8); assert_eq!((0..16).powerset().count(), 1 << 16); + + for n in 0..=10 { + let mut it = (0..n).powerset(); + let len = 1 << n; + assert_eq!(len, it.clone().count()); + for count in (0..len).rev() { + let elem = it.next(); + assert!(elem.is_some()); + assert_eq!(count, it.clone().count()); + } + let should_be_none = it.next(); + assert!(should_be_none.is_none()); + } } #[test] From 913678a07166120fc59257402d2102e0eca2fafb Mon Sep 17 00:00:00 2001 From: Philippe-Cholet Date: Tue, 22 Aug 2023 11:48:48 +0200 Subject: [PATCH 04/11] Remove the `pos` field of `Powerset` --- src/powerset.rs | 30 +++--------------------------- src/size_hint.rs | 1 + 2 files changed, 4 insertions(+), 27 deletions(-) diff --git a/src/powerset.rs b/src/powerset.rs index f5d79d807..dde7d80e6 100644 --- a/src/powerset.rs +++ b/src/powerset.rs @@ -4,7 +4,6 @@ use std::usize; use alloc::vec::Vec; use super::combinations::{Combinations, checked_binomial, combinations}; -use super::size_hint; /// An iterator to iterate through the powerset of the elements from an iterator. /// @@ -13,22 +12,20 @@ use super::size_hint; #[must_use = "iterator adaptors are lazy and do nothing unless consumed"] pub struct Powerset { combs: Combinations, - // Iterator `position` (equal to count of yielded elements). - pos: usize, } impl Clone for Powerset where I: Clone + Iterator, I::Item: Clone, { - clone_fields!(combs, pos); + clone_fields!(combs); } impl fmt::Debug for Powerset where I: Iterator + fmt::Debug, I::Item: fmt::Debug, { - debug_fmt_fields!(Powerset, combs, pos); + debug_fmt_fields!(Powerset, combs); } /// Create a new `Powerset` from a clonable iterator. @@ -38,7 +35,6 @@ pub fn powerset(src: I) -> Powerset { Powerset { combs: combinations(src, 0), - pos: 0, } } @@ -51,37 +47,17 @@ impl Iterator for Powerset fn next(&mut self) -> Option { if let Some(elt) = self.combs.next() { - self.pos = self.pos.saturating_add(1); Some(elt) } else if self.combs.k() < self.combs.n() || self.combs.k() == 0 { self.combs.reset(self.combs.k() + 1); - self.combs.next().map(|elt| { - self.pos = self.pos.saturating_add(1); - elt - }) + self.combs.next() } else { None } } - fn size_hint(&self) -> (usize, Option) { - // Total bounds for source iterator. - let src_total = self.combs.src().size_hint(); - - // Total bounds for self ( length(powerset(set) == 2 ^ length(set) ) - let self_total = size_hint::pow_scalar_base(2, src_total); - - if self.pos < usize::MAX { - // Subtract count of elements already yielded from total. - size_hint::sub_scalar(self_total, self.pos) - } else { - // Fallback: self.pos is saturated and no longer reliable. - (0, self_total.1) - } - } - fn count(self) -> usize { let k = self.combs.k(); let (n, combs_count) = self.combs.n_and_count(); diff --git a/src/size_hint.rs b/src/size_hint.rs index 71ea1412b..1146445b5 100644 --- a/src/size_hint.rs +++ b/src/size_hint.rs @@ -77,6 +77,7 @@ pub fn mul_scalar(sh: SizeHint, x: usize) -> SizeHint { /// Raise `base` correctly by a `SizeHint` exponent. #[inline] +#[allow(dead_code)] pub fn pow_scalar_base(base: usize, exp: SizeHint) -> SizeHint { let exp_low = cmp::min(exp.0, u32::MAX as usize) as u32; let low = base.saturating_pow(exp_low); From 6d92ac3c1910dc3c8b04c8aa6ccfc693185dc0fb Mon Sep 17 00:00:00 2001 From: Philippe-Cholet Date: Tue, 22 Aug 2023 11:52:47 +0200 Subject: [PATCH 05/11] Add free function `remaining_for` It will be used two other times making this function useful. --- src/powerset.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/powerset.rs b/src/powerset.rs index dde7d80e6..9b2cb797b 100644 --- a/src/powerset.rs +++ b/src/powerset.rs @@ -61,7 +61,7 @@ impl Iterator for Powerset fn count(self) -> usize { let k = self.combs.k(); let (n, combs_count) = self.combs.n_and_count(); - combs_count + (k + 1..=n).map(|i| checked_binomial(n, i).unwrap()).sum::() + remaining_for(combs_count, n, k).unwrap() } } @@ -70,3 +70,9 @@ impl FusedIterator for Powerset I: Iterator, I::Item: Clone, {} + +fn remaining_for(init_count: usize, n: usize, k: usize) -> Option { + (k + 1..=n).fold(Some(init_count), |sum, i| { + sum.and_then(|s| s.checked_add(checked_binomial(n, i)?)) + }) +} From bbc1885c96354dde4a2161e204168d9fcc6d975a Mon Sep 17 00:00:00 2001 From: Philippe-Cholet Date: Tue, 22 Aug 2023 12:03:35 +0200 Subject: [PATCH 06/11] Add back `Powerset::size_hint` --- src/powerset.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/powerset.rs b/src/powerset.rs index 9b2cb797b..18ca82b32 100644 --- a/src/powerset.rs +++ b/src/powerset.rs @@ -58,6 +58,17 @@ impl Iterator for Powerset } } + fn size_hint(&self) -> (usize, Option) { + let k = self.combs.k(); + // Total bounds for source iterator. + let (n_min, n_max) = self.combs.src().size_hint(); + // Total bounds for the current combinations. + let (mut low, mut upp) = self.combs.size_hint(); + low = remaining_for(low, n_min, k).unwrap_or(usize::MAX); + upp = upp.and_then(|upp| remaining_for(upp, n_max?, k)); + (low, upp) + } + fn count(self) -> usize { let k = self.combs.k(); let (n, combs_count) = self.combs.n_and_count(); From 75ead8ea9cc52ef4511e6275d6ed89f72e99697c Mon Sep 17 00:00:00 2001 From: Philippe-Cholet Date: Tue, 22 Aug 2023 12:05:11 +0200 Subject: [PATCH 07/11] Test size hint alongside `Powerset` counts --- tests/test_std.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_std.rs b/tests/test_std.rs index c6034122f..7ed875144 100644 --- a/tests/test_std.rs +++ b/tests/test_std.rs @@ -994,10 +994,14 @@ fn powerset() { let mut it = (0..n).powerset(); let len = 1 << n; assert_eq!(len, it.clone().count()); + assert_eq!(len, it.size_hint().0); + assert_eq!(Some(len), it.size_hint().1); for count in (0..len).rev() { let elem = it.next(); assert!(elem.is_some()); assert_eq!(count, it.clone().count()); + assert_eq!(count, it.size_hint().0); + assert_eq!(Some(count), it.size_hint().1); } let should_be_none = it.next(); assert!(should_be_none.is_none()); From 467134594c41f61a296f16b3aa3d41200c5e62b7 Mon Sep 17 00:00:00 2001 From: Philippe-Cholet Date: Thu, 24 Aug 2023 23:26:29 +0200 Subject: [PATCH 08/11] Remove `size_hint::pow_scalar_base` --- src/size_hint.rs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/size_hint.rs b/src/size_hint.rs index 1146445b5..f7278aec9 100644 --- a/src/size_hint.rs +++ b/src/size_hint.rs @@ -3,7 +3,6 @@ use std::usize; use std::cmp; -use std::u32; /// `SizeHint` is the return type of `Iterator::size_hint()`. pub type SizeHint = (usize, Option); @@ -75,21 +74,6 @@ pub fn mul_scalar(sh: SizeHint, x: usize) -> SizeHint { (low, hi) } -/// Raise `base` correctly by a `SizeHint` exponent. -#[inline] -#[allow(dead_code)] -pub fn pow_scalar_base(base: usize, exp: SizeHint) -> SizeHint { - let exp_low = cmp::min(exp.0, u32::MAX as usize) as u32; - let low = base.saturating_pow(exp_low); - - let hi = exp.1.and_then(|exp| { - let exp_hi = cmp::min(exp, u32::MAX as usize) as u32; - base.checked_pow(exp_hi) - }); - - (low, hi) -} - /// Return the maximum #[inline] pub fn max(a: SizeHint, b: SizeHint) -> SizeHint { From bb661d78d55bb66ba7aeac40c58218a8cace033a Mon Sep 17 00:00:00 2001 From: Philippe-Cholet Date: Thu, 24 Aug 2023 23:36:04 +0200 Subject: [PATCH 09/11] Use `size_hint::add` in `Powerset::size_hint` --- src/powerset.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/powerset.rs b/src/powerset.rs index 18ca82b32..f84cecfbd 100644 --- a/src/powerset.rs +++ b/src/powerset.rs @@ -4,6 +4,7 @@ use std::usize; use alloc::vec::Vec; use super::combinations::{Combinations, checked_binomial, combinations}; +use crate::size_hint::{self, SizeHint}; /// An iterator to iterate through the powerset of the elements from an iterator. /// @@ -58,21 +59,19 @@ impl Iterator for Powerset } } - fn size_hint(&self) -> (usize, Option) { + fn size_hint(&self) -> SizeHint { let k = self.combs.k(); // Total bounds for source iterator. let (n_min, n_max) = self.combs.src().size_hint(); - // Total bounds for the current combinations. - let (mut low, mut upp) = self.combs.size_hint(); - low = remaining_for(low, n_min, k).unwrap_or(usize::MAX); - upp = upp.and_then(|upp| remaining_for(upp, n_max?, k)); - (low, upp) + let low = remaining_for(n_min, k).unwrap_or(usize::MAX); + let upp = n_max.and_then(|n| remaining_for(n, k)); + size_hint::add(self.combs.size_hint(), (low, upp)) } fn count(self) -> usize { let k = self.combs.k(); let (n, combs_count) = self.combs.n_and_count(); - remaining_for(combs_count, n, k).unwrap() + combs_count + remaining_for(n, k).unwrap() } } @@ -82,8 +81,8 @@ impl FusedIterator for Powerset I::Item: Clone, {} -fn remaining_for(init_count: usize, n: usize, k: usize) -> Option { - (k + 1..=n).fold(Some(init_count), |sum, i| { +fn remaining_for(n: usize, k: usize) -> Option { + (k + 1..=n).fold(Some(0), |sum, i| { sum.and_then(|s| s.checked_add(checked_binomial(n, i)?)) }) } From 6683db44f7feaec098025f9ae60155a1cc783d74 Mon Sep 17 00:00:00 2001 From: Philippe-Cholet Date: Thu, 24 Aug 2023 23:41:33 +0200 Subject: [PATCH 10/11] `2.pow` in powerset test --- tests/test_std.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_std.rs b/tests/test_std.rs index 7ed875144..470807c7b 100644 --- a/tests/test_std.rs +++ b/tests/test_std.rs @@ -992,7 +992,7 @@ fn powerset() { for n in 0..=10 { let mut it = (0..n).powerset(); - let len = 1 << n; + let len = 2_usize.pow(n); assert_eq!(len, it.clone().count()); assert_eq!(len, it.size_hint().0); assert_eq!(Some(len), it.size_hint().1); From 04d7573a4b65e9cf0ef870b42d788f2fea341f80 Mon Sep 17 00:00:00 2001 From: Philippe-Cholet Date: Fri, 25 Aug 2023 10:38:53 +0200 Subject: [PATCH 11/11] Test `combinations_inexact_size_hints` --- tests/test_std.rs | 55 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/tests/test_std.rs b/tests/test_std.rs index 470807c7b..8ea992183 100644 --- a/tests/test_std.rs +++ b/tests/test_std.rs @@ -909,15 +909,19 @@ fn combinations_zero() { it::assert_equal((0..0).combinations(0), vec![vec![]]); } +fn binomial(n: usize, k: usize) -> usize { + if k > n { + 0 + } else { + (n - k + 1..=n).product::() / (1..=k).product::() + } +} + #[test] fn combinations_range_count() { for n in 0..=10 { for k in 0..=10 { - let len = if k<=n { - (n - k + 1..=n).product::() / (1..=k).product::() - } else { - 0 - }; + let len = binomial(n, k); let mut it = (0..n).combinations(k); assert_eq!(len, it.clone().count()); assert_eq!(len, it.size_hint().0); @@ -935,6 +939,47 @@ fn combinations_range_count() { } } +#[test] +fn combinations_inexact_size_hints() { + for k in 0..=10 { + let mut numbers = (0..18).filter(|i| i % 2 == 0); // 9 elements + let mut it = numbers.clone().combinations(k); + let real_n = numbers.clone().count(); + let len = binomial(real_n, k); + assert_eq!(len, it.clone().count()); + + let mut nb_loaded = numbers.by_ref().take(k).count(); // because of `LazyBuffer::prefill(k)` + let sh = numbers.size_hint(); + assert_eq!(binomial(sh.0 + nb_loaded, k), it.size_hint().0); + assert_eq!(sh.1.map(|n| binomial(n + nb_loaded, k)), it.size_hint().1); + + for next_count in 1..=len { + let elem = it.next(); + assert!(elem.is_some()); + assert_eq!(len - next_count, it.clone().count()); + // It does not load anything more the very first time (it's prefilled). + if next_count > 1 { + // Then it loads one item each time until exhausted. + let nb = numbers.next(); + if nb.is_some() { + nb_loaded += 1; + } + } + let sh = numbers.size_hint(); + if next_count > real_n - k + 1 { + assert_eq!(0, sh.0); + assert_eq!(Some(0), sh.1); + assert_eq!(real_n, nb_loaded); + // Once it's fully loaded, size hints of `it` are exacts. + } + assert_eq!(binomial(sh.0 + nb_loaded, k) - next_count, it.size_hint().0); + assert_eq!(sh.1.map(|n| binomial(n + nb_loaded, k) - next_count), it.size_hint().1); + } + let should_be_none = it.next(); + assert!(should_be_none.is_none()); + } +} + #[test] fn permutations_zero() { it::assert_equal((1..3).permutations(0), vec![vec![]]);