Skip to content

make it easier to work with single RangeInclusive<T> values#21

Merged
CarlKCarlK merged 4 commits intoCarlKCarlK:mainfrom
carlsverre:main
Oct 27, 2025
Merged

make it easier to work with single RangeInclusive<T> values#21
CarlKCarlK merged 4 commits intoCarlKCarlK:mainfrom
carlsverre:main

Conversation

@carlsverre
Copy link
Contributor

@carlsverre carlsverre commented Oct 22, 2025

  • Added a From<RangeInclusive<T>> constructor
  • Implemented RangeOnce<T>, which is analogous to std::iter::Once(RangeInclusive<T>) but modified to treat an empty range as an empty iterator.

These changes make interfacing with single range values easier. It's especially nice when you want to convert a range into a RangeSetBlaze, or when you want to perform a set operation (i.e., &) between a RangeSetBlaze and a single Range.

- Added a `From<RangeInclusive<T>>` constructor - Implemented `SortedStarts<T>`
and `SortedDisjoint<T>` for `Option<T>::IntoIter`

These changes make interfacing with single range values easier. It's especially
nice when you want to convert a range into a RangeSetBlaze, or when you want to
perform a set operation (i.e. &) between a RangeSetBlaze and a single Range.

Please let me know if I totally missed existing support for this functionality.
Cheers and thanks for the excellent library!
@CarlKCarlK
Copy link
Owner

Thanks for the PR. This looks like a very useful addition!

I see one issue: inclusive ranges can be empty (for example 5..=4). The current code should handle that case safely. Here are some examples that ought to work:

let a = RangeSetBlaze::from_iter([0..=10]);
let e = 5..=4; // empty

assert!(( &a & e ).is_empty());
assert_eq!(&a | e, a);
assert_eq!(&a - e, a);
assert_eq!(&a ^ e, a);

assert!(RangeSetBlaze::<i32>::from(5..=4).is_empty());

In other words, empty ranges should be treated as the canonical empty iterator rather than a single invalid item.
Could you update the PR to handle this case and add a test like the one above?

@carlsverre
Copy link
Contributor Author

Thanks for the quick feedback! While fixing/testing this I also noticed similar behavior with CheckSortedDisjoint::new([6..=5]). I assume that's expected? Added two tests to highlight this behavior:

#[test]
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[should_panic(expected = "start must be less or equal to end")]
fn test_from_sorted_disjoint_empty_array() {
    RangeSetBlaze::from_sorted_disjoint(CheckSortedDisjoint::new([6..=5]));
}

#[test]
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
#[should_panic(expected = "start <= end")]
fn test_from_sorted_disjoint_empty_option() {
    RangeSetBlaze::from_sorted_disjoint(Some(6..=5).into_iter());
}

@CarlKCarlK
Copy link
Owner

Carl,

I’m a bit concerned that Some(5..=3).into_iter() isn’t sound, but maybe we could take it as inspiration for a small iterator struct instead?

(not tested)

/// A zero-allocation iterator that yields **0 or 1** sorted, disjoint, non-empty ranges.
///
/// If `start <= end`, it yields exactly one `RangeInclusive<T>` representing `start..=end`.
/// Otherwise (if `start > end`), it yields nothing — a sound representation of the empty set.
///
/// This provides a simple and efficient way to create temporary `SortedDisjoint` iterators
/// without wrapping arrays or using `Some(..).into_iter()`.
#[derive(Clone, Copy, Debug)]
pub struct SortedDisjoint01<T: Integer> {
    start: T,
    end: T,
}

impl<T: Integer> SortedDisjoint01<T> {
    /// Creates a new adapter that will yield one range if `start <= end`, or none otherwise.
    #[inline]
    pub const fn new(start: T, end: T) -> Self {
        Self { start, end }
    }
}

impl<T: Integer> IntoIterator for SortedDisjoint01<T> {
    type Item = RangeInclusive<T>;
    type IntoIter = core::option::IntoIter<RangeInclusive<T>>;

    #[inline]
    fn into_iter(self) -> Self::IntoIter {
        if self.start <= self.end {
            Some(self.start..=self.end).into_iter()
        } else {
            None.into_iter()
        }
    }
}

/// Sound: always produces either 0 valid ranges, or 1 non-empty range.
/// There are never overlapping or reversed ranges.
impl<T: Integer> SortedStarts<T> for SortedDisjoint01<T> {}
impl<T: Integer> SortedDisjoint<T> for SortedDisjoint01<T> {}

Examples (not tested)

// Create a checked, multi-range sorted-disjoint sequence
let a = CheckSortedDisjoint::new([10..=20, 30..=40]);

// Intersection (&)
let inter = &a & SortedDisjoint01::new(15, 35);
assert!(inter.equal(CheckSortedDisjoint::new([15..=20, 30..=35])));

// Union (|)
let uni = &a | SortedDisjoint01::new(22, 25);
assert!(uni.equal(CheckSortedDisjoint::new([10..=20, 22..=25, 30..=40])));

// Difference (-)
let diff = &a - SortedDisjoint01::new(18, 32);
assert!(diff.equal(CheckSortedDisjoint::new([10..=17, 33..=40])));

// Symmetric Difference (^)
let sym = &a ^ SortedDisjoint01::new(18, 32);
assert!(sym.equal(CheckSortedDisjoint::new([10..=17, 21..=25, 33..=40])));

// Empty RHS (start > end)
let empty = &a & SortedDisjoint01::new(5, 3);
assert!(empty.equal(CheckSortedDisjoint::new([])));

Do you think this fits your intent? If you’re happy with it, you’re welcome to add it to the PR.

@carlsverre
Copy link
Contributor Author

I'm fine with a separate wrapper optimized for single ranges if you'd prefer that. However, I'm not sure SortedDisjoint01 is the right public name for it. I'll update this PR as it satisfies my needs.

@carlsverre
Copy link
Contributor Author

I updated the implementation and PR description to make things cleaner. I also added docs. I really like this new solution. It feels right. I appreciate your patience as we iterate!

@carlsverre
Copy link
Contributor Author

@CarlKCarlK I just saw the failing tests. Will address clippy issues I added + fix the wasm issue.

@CarlKCarlK
Copy link
Owner

CarlKCarlK commented Oct 26, 2025 via email

@carlsverre
Copy link
Contributor Author

Fixed! (I haven't got the wasm tests running fully locally yet, so hopefully my fix addresses the issue)

@CarlKCarlK CarlKCarlK merged commit f7ea0c6 into CarlKCarlK:main Oct 27, 2025
5 checks passed
@CarlKCarlK
Copy link
Owner

Closed and published with 0.4.1.
Thank you, @carlsverre for this contribution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments