diff --git a/src/lib.rs b/src/lib.rs index 82ff210d..4df6184c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -77,7 +77,7 @@ pub use crate::set::demo_read_ranges_from_file; pub use crate::set::{IntoIter, Iter, RangeSetBlaze}; mod sorted_disjoint; -pub use sorted_disjoint::{CheckSortedDisjoint, SortedDisjoint, SortedStarts}; +pub use sorted_disjoint::{CheckSortedDisjoint, RangeOnce, SortedDisjoint, SortedStarts}; mod sorted_disjoint_map; pub use sorted_disjoint_map::{ diff --git a/src/set.rs b/src/set.rs index f4e1dd10..07d718e5 100644 --- a/src/set.rs +++ b/src/set.rs @@ -17,6 +17,7 @@ use std::{ }; use crate::alloc::string::ToString; +use crate::sorted_disjoint::RangeOnce; use alloc::collections::{BTreeMap, btree_map}; use alloc::string::String; use alloc::vec::Vec; @@ -89,6 +90,7 @@ where /// | [`from_slice`][5] | slice of integers | Fast, but nightly-only | /// | [`from_sorted_disjoint`][3]/[`into_range_set_blaze`][3] | [`SortedDisjoint`] iterator | | /// | [`from`][5] /[`into`][5] | array of integers | | +/// | [`from`][7] | `RangeInclusive` | | /// /// /// [`BTreeMap`]: alloc::collections::BTreeMap @@ -100,6 +102,7 @@ where /// [SortedDisjoint]: crate::SortedDisjoint.html#table-of-contents /// [5]: RangeSetBlaze::from /// [6]: RangeSetBlaze::from_slice() +/// [7]: #method.from-1 /// /// # Constructor Performance /// @@ -1418,6 +1421,13 @@ impl From<[T; N]> for RangeSetBlaze { } } +impl From> for RangeSetBlaze { + /// Construct a [`RangeSetBlaze`] directly from a [`RangeInclusive`]. + fn from(value: RangeInclusive) -> Self { + Self::from_sorted_disjoint(RangeOnce::new(value)) + } +} + gen_ops_ex!( ; types ref RangeSetBlaze, ref RangeSetBlaze => RangeSetBlaze; diff --git a/src/sorted_disjoint.rs b/src/sorted_disjoint.rs index c4d49125..9130b812 100644 --- a/src/sorted_disjoint.rs +++ b/src/sorted_disjoint.rs @@ -45,6 +45,7 @@ pub trait SortedStarts: Iterator> + FusedIt /// | [`RangeSetBlaze`] | [`ranges`] | /// | [`RangeSetBlaze`] | [`into_ranges`] | /// | sorted & disjoint ranges | [`CheckSortedDisjoint::new`] | +/// | [`RangeInclusive`] | [`RangeOnce::new`] | /// | *your iterator type* | *[How to mark your type as `SortedDisjoint`][1]* | /// /// [`ranges`]: RangeSetBlaze::ranges @@ -76,7 +77,7 @@ pub trait SortedStarts: Iterator> + FusedIt /// /// | Set Operators | Operator | Multiway (same type) | Multiway (different types) | /// |------------------------------------|-------------|---------------------------------------------------|--------------------------------------| -/// | [`union`] | [`a` | `b`] | `[a, b, c].`[`union`][multiway_union]`() ` | [`union_dyn!`]`(a, b, c)` | +/// | [`union`] | [`a` | `b`] | `[a, b, c].`[`union`][multiway_union]`() ` | [`union_dyn!`]`(a, b, c)` | /// | [`intersection`] | [`a & b`] | `[a, b, c].`[`intersection`][multiway_intersection]`() ` | [`intersection_dyn!`]`(a, b, c)`| /// | [`difference`] | [`a - b`] | *n/a* | *n/a* | /// | [`symmetric_difference`] | [`a ^ b`] | `[a, b, c].`[`symmetric_difference`][multiway_symmetric_difference]`() ` | [`symmetric_difference_dyn!`]`(a, b, c)` | @@ -686,6 +687,67 @@ pub trait AnythingGoes: Iterator> + FusedIt impl AnythingGoes for I where I: Iterator> + FusedIterator {} +/// `RangeOnce` is an iterator which emits a single `RangeInclusive` value before +/// fusing. +/// +/// `RangeOnce` is analogous to [`core::iter::Once`], but modified to treat an +/// empty [`RangeInclusive`] as an empty [`Iterator`]. This allows `RangeOnce` +/// to be safely used as a [`SortedDisjoint`] Iterator. +/// +/// # Example +/// +/// ``` +/// use range_set_blaze::{ RangeSetBlaze, RangeOnce }; +/// +/// let a = RangeOnce::new(0..=10); +/// let b = RangeOnce::new(3..=2); // empty range +/// let c = RangeOnce::new(5..=15); +/// +/// let combined = RangeSetBlaze::from_sorted_disjoint(a | b | c); +/// assert_eq!(combined.into_string(), "0..=15"); +/// ``` +pub struct RangeOnce(core::option::IntoIter>); + +impl RangeOnce { + /// Creates a new [`RangeOnce`] from a single range. See [`RangeOnce`] for details and examples. + pub fn new(range: RangeInclusive) -> Self { + Self((!range.is_empty()).then_some(range).into_iter()) + } +} + +impl From> for RangeOnce { + #[inline] + fn from(value: RangeInclusive) -> Self { + Self::new(value) + } +} + +impl Iterator for RangeOnce { + type Item = RangeInclusive; + + fn next(&mut self) -> Option { + self.0.next() + } + + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } +} + +impl DoubleEndedIterator for RangeOnce { + fn next_back(&mut self) -> Option { + self.0.next_back() + } +} + +impl ExactSizeIterator for RangeOnce { + fn len(&self) -> usize { + self.0.len() + } +} + +impl FusedIterator for RangeOnce {} + macro_rules! impl_sorted_traits_and_ops { ($IterType:ty, $($more_generics:tt)*) => { #[allow(single_use_lifetimes)] @@ -767,5 +829,4 @@ impl_sorted_traits_and_ops!(RangesIter<'a, T>, 'a); impl_sorted_traits_and_ops!(RangeValuesToRangesIter, VR: ValueRef, I: SortedDisjointMap); impl_sorted_traits_and_ops!(SymDiffIter, I: SortedStarts); impl_sorted_traits_and_ops!(UnionIter, I: SortedStarts); - -// We're not allowed to define methods on outside types, so we only define the traits +impl_sorted_traits_and_ops!(RangeOnce, 'ignore); diff --git a/tests/set_tests.rs b/tests/set_tests.rs index c701a01d..d64dc6c3 100644 --- a/tests/set_tests.rs +++ b/tests/set_tests.rs @@ -3,7 +3,7 @@ #![cfg(test)] use range_set_blaze::{ AssumeSortedStarts, IntoIter, IntoRangesIter, Iter, KMerge, MapIntoRangesIter, MapRangesIter, - Merge, RangeValuesIter, RangeValuesToRangesIter, RangesIter, + Merge, RangeOnce, RangeValuesIter, RangeValuesToRangesIter, RangesIter, }; use wasm_bindgen_test::*; @@ -615,6 +615,7 @@ fn tricky_case3() { #[test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] #[allow(unused_assignments)] +#[allow(clippy::reversed_empty_ranges)] fn constructors() { // #9: new let mut range_set_blaze; @@ -631,6 +632,9 @@ fn constructors() { // #16 into / from iter (T,T) + SortedDisjoint range_set_blaze = range_set_blaze.ranges().into_range_set_blaze(); range_set_blaze = RangeSetBlaze::from_sorted_disjoint(range_set_blaze.ranges()); + // from RangeInclusive + range_set_blaze = RangeSetBlaze::from(10..=20); + range_set_blaze = RangeSetBlaze::from(6..=5); } #[cfg(target_os = "linux")] @@ -2024,6 +2028,69 @@ fn test_every_sorted_disjoint_method() { )*}} } +#[test] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +#[allow(clippy::reversed_empty_ranges)] +fn test_from_empty_range() { + let a = RangeSetBlaze::from(0..=10); + let e = RangeSetBlaze::from(6..=5); // empty range + + assert!(e.is_empty()); + assert!((&a & &e).is_empty()); + assert_eq!(&a | &e, a); + assert_eq!(&a - &e, a); + assert_eq!(&a ^ &e, a); +} + +#[test] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +#[should_panic(expected = "start must be less or equal to end")] +#[allow(clippy::reversed_empty_ranges)] +fn test_from_sorted_disjoint_empty_array() { + RangeSetBlaze::from_sorted_disjoint(CheckSortedDisjoint::new([6..=5])); +} + +#[test] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +fn test_range_once_from() { + let mut r: RangeOnce<_> = (0..=15).into(); + assert_eq!(r.next(), Some(0..=15)); +} + +#[test] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +#[allow(clippy::reversed_empty_ranges)] +fn test_range_once_empty() { + assert_eq!(RangeOnce::new(6..=5).next(), None); +} + +#[test] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +#[allow(clippy::items_after_statements)] +fn test_range_once_sorted_disjoint() { + macro_rules! testiter { + () => { + RangeOnce::new(10..=20) + }; + } + + assert!(!testiter!().is_universal()); + assert!(!testiter!().is_empty()); + assert!((!testiter!()).equal(CheckSortedDisjoint::new([ + -2_147_483_648..=9, + 21..=2_147_483_647 + ]))); + assert!((testiter!() | RangeOnce::new(15..=25)).equal(RangeOnce::new(10..=25))); + assert!((testiter!() & RangeOnce::new(15..=25)).equal(RangeOnce::new(15..=20))); + assert!( + (testiter!() ^ RangeOnce::new(15..=25)).equal(CheckSortedDisjoint::new([10..=14, 21..=25])) + ); + assert!((testiter!() - RangeOnce::new(15..=25)).equal(CheckSortedDisjoint::new([10..=14]))); + + fn is_fused(_iter: T) {} + is_fused::<_>(testiter!()); +} + #[test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] fn multiway3() {