From 324923b284d5afdc6b8ae8f3229f732854d9b5bb Mon Sep 17 00:00:00 2001 From: Keenan Tims Date: Fri, 5 Dec 2025 18:02:17 -0800 Subject: [PATCH] utils: add RangeSet early implementation based on RBTree --- Cargo.lock | 19 ++++++ utils/grid/Cargo.toml | 2 +- utils/misc/Cargo.lock | 19 ++++++ utils/misc/Cargo.toml | 3 +- utils/misc/src/lib.rs | 3 + utils/misc/src/range.rs | 124 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 utils/misc/src/range.rs diff --git a/Cargo.lock b/Cargo.lock index f20472c..1ec1aff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -202,6 +202,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "intrusive-collections" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "189d0897e4cbe8c75efedf3502c18c887b05046e59d28404d4d8e46cbc4d1e86" +dependencies = [ + "memoffset", +] + [[package]] name = "itertools" version = "0.14.0" @@ -233,10 +242,20 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "misc" version = "0.1.0" dependencies = [ + "intrusive-collections", "num-traits", ] diff --git a/utils/grid/Cargo.toml b/utils/grid/Cargo.toml index 6507dcf..a95c55e 100644 --- a/utils/grid/Cargo.toml +++ b/utils/grid/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "grid" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] diff --git a/utils/misc/Cargo.lock b/utils/misc/Cargo.lock index f6716d7..6dfd16a 100644 --- a/utils/misc/Cargo.lock +++ b/utils/misc/Cargo.lock @@ -8,10 +8,29 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "intrusive-collections" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "189d0897e4cbe8c75efedf3502c18c887b05046e59d28404d4d8e46cbc4d1e86" +dependencies = [ + "memoffset", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "misc" version = "0.1.0" dependencies = [ + "intrusive-collections", "num-traits", ] diff --git a/utils/misc/Cargo.toml b/utils/misc/Cargo.toml index 3704ff9..cf51035 100644 --- a/utils/misc/Cargo.toml +++ b/utils/misc/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "misc" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] +intrusive-collections = "0.9.7" num-traits = "0.2.19" diff --git a/utils/misc/src/lib.rs b/utils/misc/src/lib.rs index 46d447f..97836e2 100644 --- a/utils/misc/src/lib.rs +++ b/utils/misc/src/lib.rs @@ -2,6 +2,8 @@ use num_traits::Signed; use std::fmt::Display; use std::ops::{Add, AddAssign}; +pub mod range; + const POW10MAX: usize = u64::MAX.ilog10() as usize; pub const POW10: [u64; POW10MAX] = pow10_lut(); @@ -105,6 +107,7 @@ mod tests { #[test] fn test_pow10() { + #[allow(clippy::needless_range_loop)] for i in 0..POW10MAX { assert_eq!(POW10[i], 10u64.pow(i as u32)) } diff --git a/utils/misc/src/range.rs b/utils/misc/src/range.rs new file mode 100644 index 0000000..b2d44f8 --- /dev/null +++ b/utils/misc/src/range.rs @@ -0,0 +1,124 @@ +use intrusive_collections::Bound as IBound; +use intrusive_collections::{KeyAdapter, intrusive_adapter}; +use intrusive_collections::{RBTreeLink, rbtree::RBTree}; +use std::cmp::{max, min}; +use std::ops::{Add, Bound, RangeBounds, Sub}; + +#[derive(Debug)] +pub struct NaiveRange { + link: RBTreeLink, + pub start: T, + pub end: T, +} + +intrusive_adapter!( + pub NaiveRangeAdapter = Box>: NaiveRange { link: RBTreeLink } +); + +impl<'a, T: Ord + Clone> KeyAdapter<'a> for NaiveRangeAdapter { + type Key = T; + fn get_key(&self, node: &'a NaiveRange) -> T { + node.start.clone() + } +} + +impl NaiveRange { + fn new(start: T, end: T) -> Self { + Self { + link: RBTreeLink::new(), + start, + end, + } + } +} + +pub struct RangeSet { + pub store: RBTree>, +} + +fn normalize_range, T: Copy + Ord + Add + From>( + r: &R, +) -> NaiveRange { + let start = match r.start_bound() { + Bound::Included(x) => *x, + Bound::Excluded(x) => *x + T::from(1), + Bound::Unbounded => panic!(), + }; + let end = match r.end_bound() { + Bound::Included(x) => *x + T::from(1), + Bound::Excluded(x) => *x, + Bound::Unbounded => panic!(), + }; + + NaiveRange::new(start, end) +} + +impl + Sub + From> Default for RangeSet { + fn default() -> Self { + Self::new() + } +} + +impl + Sub + From> RangeSet { + pub fn new() -> Self { + Self { + store: RBTree::new(NaiveRangeAdapter::::new()), + } + } + pub fn add>(&mut self, r: &R) { + //normalize r to Range + let mut new_r = normalize_range(r); + + // Find the position of nearest entry >= and <= than new_r's start + let mut cur = self.store.lower_bound_mut(IBound::Included(&new_r.start)); + + while let Some(val) = cur.get() + && new_r.end >= val.start + { + // then remove them and update our new range + new_r.start = min(new_r.start, val.start); + new_r.end = max(new_r.end, val.end); + cur.remove(); // moves the cursor ahead + } + + // Move the cursor back one (this will be the element before our insertion point, 'below' start) + cur.move_prev(); + while let Some(val) = cur.get() + && new_r.start <= val.end + { + new_r.start = min(new_r.start, val.start); + new_r.end = max(new_r.end, val.end); + cur.remove(); + cur.move_prev(); + } + + // We are already in position, we just checked the previous element and it's before us + // since we fell out of the loop, so we need to go after this position + cur.insert_after(Box::new(new_r)); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ranges() { + let mut set = RangeSet::new(); + // edge cases are tricky to construct, run the aoc2025 day 5 puzzle and compare :) + set.add(&(0..10)); + set.add(&(100..1000)); + set.add(&(50..100)); + set.add(&(50..100)); + set.add(&(1001..2000)); + set.add(&(2000..2001)); + set.add(&(-10..-2)); + set.add(&(-1..-1)); + let end = set + .store + .iter() + .map(|r| (r.start, r.end)) + .collect::>(); + assert_eq!(end, [(-10, 10), (50, 2001)]) + } +}