pub mod bucket;
pub mod io;
pub mod layout;
pub mod serde;
use std::ops::RangeInclusive;
pub use roaring::RoaringBitmap;
pub type Bucket = u32;
#[derive(Debug, Clone, Default)]
pub struct Coverage {
bitmap: RoaringBitmap,
}
impl Coverage {
pub fn empty() -> Self {
Self {
bitmap: RoaringBitmap::new(),
}
}
pub fn from_bitmap(bitmap: RoaringBitmap) -> Self {
Self { bitmap }
}
pub fn present(&self) -> &RoaringBitmap {
&self.bitmap
}
pub fn into_bitmap(self) -> RoaringBitmap {
self.bitmap
}
pub fn union(&self, other: &Coverage) -> Coverage {
let bitmap = &self.bitmap | &other.bitmap;
Coverage { bitmap }
}
pub fn intersect(&self, other: &Coverage) -> Coverage {
let bitmap = &self.bitmap & &other.bitmap;
Coverage { bitmap }
}
pub fn cardinality(&self) -> u64 {
self.bitmap.len()
}
pub fn missing_points(&self, expected: &RoaringBitmap) -> RoaringBitmap {
let mut missing = expected.clone();
missing -= &self.bitmap;
missing
}
pub fn missing_runs(
&self,
expected: &RoaringBitmap,
max_run_len: Option<u64>,
) -> Vec<RangeInclusive<Bucket>> {
let missing = self.missing_points(expected);
let base_runs = runs_from_bitmap(&missing);
if let Some(max_len) = max_run_len {
split_runs_by_len(base_runs, max_len)
} else {
base_runs
}
}
pub fn last_run_with_min_len(
&self,
expected: &RoaringBitmap,
min_len: u64,
) -> Option<RangeInclusive<Bucket>> {
if min_len == 0 {
return None;
}
let covered = &self.bitmap & expected;
let runs = runs_from_bitmap(&covered);
for range in runs.into_iter().rev() {
let (start, end) = (*range.start() as u64, *range.end() as u64);
let len = end.saturating_sub(start) + 1;
if len >= min_len {
return Some(start as Bucket..=end as Bucket);
}
}
None
}
pub fn coverage_ratio(&self, expected: &RoaringBitmap) -> f64 {
let expected_count = expected.len();
if expected_count == 0 {
return 1.0;
}
let covered = &self.bitmap & expected;
let covered_count = covered.len();
covered_count as f64 / expected_count as f64
}
pub fn max_gap_len(&self, expected: &RoaringBitmap) -> u64 {
let missing = self.missing_points(expected);
let runs = runs_from_bitmap(&missing);
runs.into_iter()
.map(|r| {
let (start, end) = (*r.start(), *r.end());
(end as u64).saturating_sub(start as u64) + 1
})
.max()
.unwrap_or(0)
}
pub fn union_inplace(&mut self, other: &Coverage) {
self.bitmap |= other.present();
}
pub fn last_window_at_or_before(
&self,
end_bucket: Bucket,
len: u64,
) -> Option<RangeInclusive<Bucket>> {
if len == 0 {
return None;
}
let it = self.bitmap.iter().rev();
let mut started = false;
let mut run_end: Bucket = 0;
let mut prev_seen: Option<Bucket> = None;
let mut run_len: u64 = 0;
for b in it {
if b > end_bucket {
continue;
}
if !started {
started = true;
run_end = b;
run_len = 1;
} else if prev_seen == Some(b + 1) {
run_len += 1;
} else {
run_end = b;
run_len = 1;
}
prev_seen = Some(b);
if run_len >= len {
let end_u64 = run_end as u64;
let start_u64 = end_u64.checked_add(1)?.checked_sub(len)?;
if start_u64 > u32::MAX as u64 {
return None;
}
return Some(start_u64 as Bucket..=run_end);
}
}
None
}
}
impl FromIterator<Bucket> for Coverage {
fn from_iter<I>(iter: I) -> Self
where
I: IntoIterator<Item = Bucket>,
{
let bitmap: RoaringBitmap = iter.into_iter().collect();
Self { bitmap }
}
}
fn runs_from_bitmap(bitmap: &RoaringBitmap) -> Vec<RangeInclusive<Bucket>> {
let mut out = Vec::new();
let mut iter = bitmap.iter();
let Some(mut start) = iter.next() else {
return out;
};
let mut prev = start;
for v in iter {
if prev.checked_add(1) == Some(v) {
prev = v;
} else {
out.push(start..=prev);
start = v;
prev = v;
}
}
out.push(start..=prev);
out
}
fn split_runs_by_len(
runs: Vec<RangeInclusive<Bucket>>,
max_len: u64,
) -> Vec<RangeInclusive<Bucket>> {
if max_len == 0 {
return Vec::new();
}
let mut out = Vec::new();
let step = max_len - 1;
for range in runs {
let (start, end) = (*range.start() as u64, *range.end() as u64);
let mut cur = start;
while cur <= end {
let chunk_end = match cur.checked_add(step) {
Some(v) => v.min(end),
None => break, };
out.push(cur as Bucket..=chunk_end as Bucket);
if chunk_end == end {
break;
}
cur = chunk_end + 1;
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use roaring::RoaringBitmap;
fn bm_from_range(start: u32, end_exclusive: u32) -> RoaringBitmap {
(start..end_exclusive).collect()
}
#[test]
fn from_iterator_builds_expected_bitmap() {
let cov: Coverage = (10u32..15u32).collect();
assert_eq!(cov.cardinality(), 5);
for b in 10u32..15u32 {
assert!(cov.present().contains(b));
}
}
#[test]
fn basic_api_cardinality_union_intersect_into_bitmap() {
let cov_a = Coverage::from_bitmap(bm_from_range(0, 5)); let cov_b = Coverage::from_bitmap(bm_from_range(3, 8));
assert_eq!(cov_a.cardinality(), 5);
let union = cov_a.union(&cov_b);
assert_eq!(union.cardinality(), 8);
assert!(union.present().contains(0));
assert!(union.present().contains(7));
let inter = cov_a.intersect(&cov_b);
assert_eq!(inter.cardinality(), 2);
assert!(inter.present().contains(3));
assert!(inter.present().contains(4));
assert!(!inter.present().contains(2));
let bitmap = inter.into_bitmap();
assert!(bitmap.contains(3));
assert!(bitmap.contains(4));
assert_eq!(bitmap.len(), 2);
}
#[test]
fn full_coverage_continuous() {
let expected = bm_from_range(0, 10);
let present = bm_from_range(0, 10);
let cov = Coverage::from_bitmap(present);
let missing = cov.missing_points(&expected);
assert!(missing.is_empty());
let runs = cov.missing_runs(&expected, None);
assert!(runs.is_empty());
for min_len in 1..=10 {
let run = cov.last_run_with_min_len(&expected, min_len).unwrap();
assert_eq!(*run.start(), 0);
assert_eq!(*run.end(), 9);
}
assert!(cov.last_run_with_min_len(&expected, 11).is_none());
assert!((cov.coverage_ratio(&expected) - 1.0).abs() < 1e-12);
assert_eq!(cov.max_gap_len(&expected), 0);
}
#[test]
fn single_gap_in_middle() {
let expected = bm_from_range(0, 10);
let mut present = bm_from_range(0, 10);
present.remove(5);
let cov = Coverage::from_bitmap(present);
let missing = cov.missing_points(&expected);
assert_eq!(missing.len(), 1);
assert!(missing.contains(5));
let runs = cov.missing_runs(&expected, None);
assert_eq!(runs.len(), 1);
let r = &runs[0];
assert_eq!((*r.start(), *r.end()), (5, 5));
assert_eq!(cov.max_gap_len(&expected), 1);
}
#[test]
fn multiple_gaps_and_run_splitting() {
let expected = bm_from_range(0, 20);
let mut present = bm_from_range(0, 20);
for b in [3, 4, 10, 11, 12, 18] {
present.remove(b);
}
let cov = Coverage::from_bitmap(present);
let missing = cov.missing_points(&expected);
let mut missing_vec: Vec<_> = missing.iter().collect();
missing_vec.sort_unstable();
assert_eq!(missing_vec, vec![3, 4, 10, 11, 12, 18]);
let runs = cov.missing_runs(&expected, None);
assert_eq!(runs.len(), 3);
assert_eq!((*runs[0].start(), *runs[0].end()), (3, 4)); assert_eq!((*runs[1].start(), *runs[1].end()), (10, 12)); assert_eq!((*runs[2].start(), *runs[2].end()), (18, 18));
let runs_split = cov.missing_runs(&expected, Some(2));
assert_eq!(runs_split.len(), 4);
assert_eq!((*runs_split[0].start(), *runs_split[0].end()), (3, 4));
assert_eq!((*runs_split[1].start(), *runs_split[1].end()), (10, 11));
assert_eq!((*runs_split[2].start(), *runs_split[2].end()), (12, 12));
assert_eq!((*runs_split[3].start(), *runs_split[3].end()), (18, 18));
let runs_split_single = cov.missing_runs(&expected, Some(1));
let expected_singletons = vec![3, 4, 10, 11, 12, 18];
assert_eq!(runs_split_single.len(), expected_singletons.len());
for (range, expected_bucket) in runs_split_single.iter().zip(expected_singletons.iter()) {
assert_eq!(
(*range.start(), *range.end()),
(*expected_bucket, *expected_bucket)
);
}
let runs_zero = cov.missing_runs(&expected, Some(0));
assert!(runs_zero.is_empty());
}
#[test]
fn edge_cases_empty_expected() {
let expected = RoaringBitmap::new();
let present = bm_from_range(0, 10);
let cov = Coverage::from_bitmap(present);
let missing = cov.missing_points(&expected);
assert!(missing.is_empty());
let runs = cov.missing_runs(&expected, None);
assert!(runs.is_empty());
let ratio = cov.coverage_ratio(&expected);
assert!((ratio - 1.0).abs() < 1e-12);
assert_eq!(cov.max_gap_len(&expected), 0);
assert!(cov.last_run_with_min_len(&expected, 1).is_none());
}
#[test]
fn edge_cases_empty_present() {
let expected = bm_from_range(0, 5);
let cov = Coverage::empty();
let missing = cov.missing_points(&expected);
assert_eq!(missing.len(), expected.len());
let runs = cov.missing_runs(&expected, None);
assert_eq!(runs.len(), 1);
let r = &runs[0];
assert_eq!((*r.start(), *r.end()), (0, 4));
assert_eq!(cov.coverage_ratio(&expected), 0.0);
assert_eq!(cov.max_gap_len(&expected), 5);
assert!(cov.last_run_with_min_len(&expected, 6).is_none());
assert!(cov.last_run_with_min_len(&expected, 3).is_none());
}
#[test]
fn single_point_cases() {
let mut expected = RoaringBitmap::new();
expected.insert(42);
let mut present = RoaringBitmap::new();
present.insert(42);
let cov = Coverage::from_bitmap(present);
assert!(cov.missing_points(&expected).is_empty());
assert!(cov.missing_runs(&expected, None).is_empty());
assert_eq!(cov.coverage_ratio(&expected), 1.0);
assert_eq!(cov.max_gap_len(&expected), 0);
let run = cov.last_run_with_min_len(&expected, 1).unwrap();
assert_eq!((*run.start(), *run.end()), (42, 42));
let cov_empty = Coverage::empty();
let missing = cov_empty.missing_points(&expected);
assert!(missing.contains(42));
}
#[test]
fn last_window_contiguous_runs() {
let cov = Coverage::from_bitmap(bm_from_range(0, 10));
assert_eq!(cov.last_window_at_or_before(9, 3), Some(7u32..=9u32));
assert_eq!(cov.last_window_at_or_before(8, 3), Some(6u32..=8u32));
assert!(cov.last_window_at_or_before(9, 11).is_none());
}
#[test]
fn last_window_skips_over_gaps() {
let mut bm = bm_from_range(0, 5); bm.extend((7u32..=10u32).collect::<RoaringBitmap>()); let cov = Coverage::from_bitmap(bm);
assert_eq!(cov.last_window_at_or_before(10, 3), Some(8u32..=10u32));
assert_eq!(cov.last_window_at_or_before(10, 4), Some(7u32..=10u32));
assert_eq!(cov.last_window_at_or_before(6, 2), Some(3u32..=4u32));
assert_eq!(cov.last_window_at_or_before(9, 5), Some(0u32..=4u32));
}
#[test]
fn last_window_handles_len_zero_and_empty() {
let cov = Coverage::empty();
assert!(cov.last_window_at_or_before(100, 1).is_none());
assert!(cov.last_window_at_or_before(100, 0).is_none());
}
}