use std::cmp::max;
const CP_MIN: u32 = 0x0000;
const CP_MAX: u32 = 0x10_FFFF;
const SUR_LO: u32 = 0xD800;
const SUR_HI: u32 = 0xDFFF;
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
pub struct ClassSet {
ranges: Vec<(u32, u32)>,
}
impl ClassSet {
pub fn empty() -> Self { Self { ranges: Vec::new() } }
pub fn universe() -> Self {
Self {
ranges: vec![
(CP_MIN, SUR_LO - 1),
(SUR_HI + 1, CP_MAX),
],
}
}
pub fn from_char(c: char) -> Self {
Self::from_range(c as u32, c as u32)
}
pub fn from_range(lo: u32, hi: u32) -> Self {
debug_assert!(lo <= hi);
Self { ranges: vec![(lo, hi)] }
}
pub fn from_sorted_ranges(ranges: Vec<(u32, u32)>) -> Self {
debug_assert!(is_canonical(&ranges));
Self { ranges }
}
pub fn from_ranges(mut ranges: Vec<(u32, u32)>) -> Self {
ranges.retain(|(lo, hi)| lo <= hi);
ranges.sort_unstable_by_key(|&(lo, _)| lo);
let mut out: Vec<(u32, u32)> = Vec::with_capacity(ranges.len());
for (lo, hi) in ranges {
match out.last_mut() {
Some(last) if lo <= last.1.saturating_add(1) => {
last.1 = max(last.1, hi);
}
_ => out.push((lo, hi)),
}
}
Self { ranges: out }
}
pub fn contains(&self, c: char) -> bool {
let cp = c as u32;
if self.ranges.len() < 16 {
self.ranges.iter().any(|&(lo, hi)| cp >= lo && cp <= hi)
} else {
match self.ranges.binary_search_by(|&(lo, hi)| {
if cp < lo { std::cmp::Ordering::Greater }
else if cp > hi { std::cmp::Ordering::Less }
else { std::cmp::Ordering::Equal }
}) {
Ok(_) => true,
Err(_) => false,
}
}
}
pub fn ranges(&self) -> &[(u32, u32)] { &self.ranges }
pub fn union(&self, other: &Self) -> Self {
let mut out: Vec<(u32, u32)> = Vec::with_capacity(
self.ranges.len() + other.ranges.len()
);
let (mut i, mut j) = (0usize, 0usize);
loop {
let r = match (self.ranges.get(i), other.ranges.get(j)) {
(Some(a), Some(b)) =>
if a.0 <= b.0 { i += 1; *a } else { j += 1; *b },
(Some(a), None) => { i += 1; *a }
(None, Some(b)) => { j += 1; *b }
(None, None) => break,
};
match out.last_mut() {
Some(last) if r.0 <= last.1.saturating_add(1) => {
last.1 = max(last.1, r.1);
}
_ => out.push(r),
}
}
Self { ranges: out }
}
pub fn subtract(&self, other: &Self) -> Self {
let mut out = Vec::new();
let mut j = 0usize;
for &(mut a_lo, a_hi) in &self.ranges {
while a_lo <= a_hi {
while j < other.ranges.len() && other.ranges[j].1 < a_lo {
j += 1;
}
let Some(&(b_lo, b_hi)) = other.ranges.get(j) else {
out.push((a_lo, a_hi));
break;
};
if b_lo > a_hi {
out.push((a_lo, a_hi));
break;
}
if b_lo > a_lo {
out.push((a_lo, b_lo - 1));
}
if b_hi >= a_hi { break; }
a_lo = b_hi + 1;
j += 1;
}
}
Self { ranges: out }
}
pub fn complement(&self) -> Self {
Self::universe().subtract(self)
}
}
fn is_canonical(ranges: &[(u32, u32)]) -> bool {
if ranges.is_empty() { return true; }
if ranges[0].0 > ranges[0].1 { return false; }
for w in ranges.windows(2) {
let (_, prev_hi) = w[0];
let (cur_lo, cur_hi) = w[1];
if cur_lo > cur_hi { return false; }
if cur_lo <= prev_hi.saturating_add(1) { return false; }
}
true
}
#[cfg(test)]
mod tests {
use super::*;
fn cs(ranges: &[(u32, u32)]) -> ClassSet {
ClassSet::from_ranges(ranges.to_vec())
}
#[test]
fn coalesces_abutting_ranges() {
let s = cs(&[(1, 5), (6, 10)]);
assert_eq!(s.ranges(), &[(1, 10)]);
}
#[test]
fn coalesces_overlapping_ranges() {
let s = cs(&[(1, 5), (3, 10)]);
assert_eq!(s.ranges(), &[(1, 10)]);
}
#[test]
fn keeps_disjoint_ranges() {
let s = cs(&[(1, 5), (10, 20)]);
assert_eq!(s.ranges(), &[(1, 5), (10, 20)]);
}
#[test]
fn contains_basic() {
let s = cs(&[('a' as u32, 'z' as u32)]);
assert!(s.contains('a'));
assert!(s.contains('m'));
assert!(s.contains('z'));
assert!(!s.contains('A'));
assert!(!s.contains('{'));
}
#[test]
fn union_merges() {
let a = cs(&[(1, 5), (10, 20)]);
let b = cs(&[(4, 12)]);
assert_eq!(a.union(&b).ranges(), &[(1, 20)]);
}
#[test]
fn subtract_basic() {
let a = cs(&[('a' as u32, 'z' as u32)]);
let vowels: Vec<(u32, u32)> = "aeiou".chars()
.map(|c| (c as u32, c as u32)).collect();
let b = ClassSet::from_ranges(vowels);
let diff = a.subtract(&b);
assert!(!diff.contains('a'));
assert!(!diff.contains('e'));
assert!(!diff.contains('i'));
assert!(!diff.contains('o'));
assert!(!diff.contains('u'));
assert!(diff.contains('b'));
assert!(diff.contains('z'));
}
#[test]
fn subtract_self_is_empty() {
let a = cs(&[(1, 100)]);
assert!(a.subtract(&a).ranges().is_empty());
}
#[test]
fn subtract_disjoint_is_identity() {
let a = cs(&[(1, 10)]);
let b = cs(&[(20, 30)]);
assert_eq!(a.subtract(&b).ranges(), a.ranges());
}
#[test]
fn subtract_splits_range() {
let a = cs(&[(1, 100)]);
let b = cs(&[(40, 60)]);
assert_eq!(a.subtract(&b).ranges(), &[(1, 39), (61, 100)]);
}
#[test]
fn complement_excludes_surrogates() {
let empty = ClassSet::empty();
let all = empty.complement();
assert_eq!(all.ranges(), &[(0, SUR_LO - 1), (SUR_HI + 1, CP_MAX)]);
}
#[test]
fn complement_round_trip() {
let a = cs(&[(1, 10), (20, 30)]);
let round = a.complement().complement();
assert_eq!(round, a);
}
}