int-intervals 0.1.1

Closed-open integer interval algebra, canonical interval sets, and overlap-stack structures.
Documentation
use crate::interval::traits::IntPrimitive;
use alloc::vec;
use proptest::prelude::*;

use crate::{
    I8COSet,
    set::test_support::{arb_iv, build, iv},
};

#[test]
fn empty_set_has_zero_covered_len() {
    let set = build([]);
    let query = iv(-10, 10);

    assert_eq!(set.covered_len_of(query), 0);
    assert_eq!(set.uncovered_len_of(query), query.len());
    assert_eq!(set.coverage_ratio_f32_of(query), 0.0);
    assert_eq!(set.coverage_ratio_f64_of(query), 0.0);
}

#[test]
fn full_cover_has_full_covered_len() {
    let set = build([(-10, 10)]);
    let query = iv(-10, 10);

    assert_eq!(set.covered_len_of(query), 20);
    assert_eq!(set.uncovered_len_of(query), 0);
    assert_eq!(set.coverage_ratio_f32_of(query), 1.0);
    assert_eq!(set.coverage_ratio_f64_of(query), 1.0);
}

#[test]
fn partial_cover_inside_single_interval() {
    let set = build([(-10, 10)]);
    let query = iv(0, 20);

    assert_eq!(set.covered_len_of(query), 10);
    assert_eq!(set.uncovered_len_of(query), 10);
    assert_eq!(set.coverage_ratio_f32_of(query), 0.5);
    assert_eq!(set.coverage_ratio_f64_of(query), 0.5);
}

#[test]
fn gap_only_query_has_zero_covered_len() {
    let set = build([(-20, -10), (10, 20)]);
    let query = iv(-10, 10);

    assert_eq!(set.covered_len_of(query), 0);
    assert_eq!(set.uncovered_len_of(query), query.len());
    assert_eq!(set.coverage_ratio_f32_of(query), 0.0);
    assert_eq!(set.coverage_ratio_f64_of(query), 0.0);
}

#[test]
fn query_across_multiple_intervals_sums_clipped_segments() {
    let set = build([(-50, -40), (-20, -10), (10, 20)]);
    let query = iv(-45, 15);

    assert_eq!(
        set.intersection_with_interval(query).as_slice().to_vec(),
        vec![iv(-45, -40), iv(-20, -10), iv(10, 15)]
    );
    assert_eq!(set.covered_len_of(query), 20);
    assert_eq!(set.uncovered_len_of(query), 40);
    assert_eq!(set.coverage_ratio_f32_of(query), 20.0 / 60.0);
    assert_eq!(set.coverage_ratio_f64_of(query), 20.0 / 60.0);
}

#[test]
fn coverage_is_computed_from_canonical_merged_intervals() {
    let set = build([(-20, -10), (-10, 0), (5, 15), (10, 20)]);
    let query = iv(-5, 10);

    assert_eq!(set.as_slice(), &[iv(-20, 0), iv(5, 20)]);
    assert_eq!(
        set.intersection_with_interval(query).as_slice().to_vec(),
        vec![iv(-5, 0), iv(5, 10)]
    );
    assert_eq!(set.covered_len_of(query), 10);
    assert_eq!(set.uncovered_len_of(query), 5);
    assert_eq!(set.coverage_ratio_f32_of(query), 10.0 / 15.0);
    assert_eq!(set.coverage_ratio_f64_of(query), 10.0 / 15.0);
}

#[test]
fn query_containing_all_intervals_sums_all_covered_segments() {
    let set = build([(-40, -30), (10, 20)]);
    let query = iv(-50, 50);

    assert_eq!(set.covered_len_of(query), 20);
    assert_eq!(set.uncovered_len_of(query), 80);
    assert_eq!(set.coverage_ratio_f32_of(query), 0.2);
    assert_eq!(set.coverage_ratio_f64_of(query), 0.2);
}

#[test]
fn coverage_handles_domain_edges() {
    let set = build([(i8::MIN, i8::MIN + 1), (i8::MAX - 1, i8::MAX)]);

    let left = iv(i8::MIN, i8::MIN + 2);
    assert_eq!(set.covered_len_of(left), 1);
    assert_eq!(set.uncovered_len_of(left), 1);
    assert_eq!(set.coverage_ratio_f32_of(left), 0.5);
    assert_eq!(set.coverage_ratio_f64_of(left), 0.5);

    let right = iv(i8::MAX - 2, i8::MAX);
    assert_eq!(set.covered_len_of(right), 1);
    assert_eq!(set.uncovered_len_of(right), 1);
    assert_eq!(set.coverage_ratio_f32_of(right), 0.5);
    assert_eq!(set.coverage_ratio_f64_of(right), 0.5);
}

#[test]
fn representative_queries_partition_into_covered_and_uncovered_lengths() {
    let set = build([(-50, -40), (-20, -10), (10, 20)]);

    for query in [
        iv(-60, -50),
        iv(-51, -49),
        iv(-45, -15),
        iv(-10, 10),
        iv(-15, 15),
        iv(15, 21),
        iv(i8::MIN, i8::MIN + 1),
        iv(i8::MAX - 1, i8::MAX),
    ] {
        assert_eq!(
            set.covered_len_of(query) + set.uncovered_len_of(query),
            query.len(),
            "query = {query:?}"
        );
    }
}

#[test]
fn representative_ratios_match_covered_len_divided_by_query_len() {
    let set = build([(-50, -40), (-20, -10), (10, 20)]);

    for query in [
        iv(-60, -50),
        iv(-51, -49),
        iv(-45, -15),
        iv(-10, 10),
        iv(-15, 15),
        iv(15, 21),
        iv(i8::MIN, i8::MIN + 1),
        iv(i8::MAX - 1, i8::MAX),
    ] {
        let expected_f32 = set.covered_len_of(query).as_f32() / query.len().as_f32();
        let expected_f64 = set.covered_len_of(query).as_f64() / query.len().as_f64();

        assert_eq!(
            set.coverage_ratio_f32_of(query),
            expected_f32,
            "query = {query:?}"
        );
        assert_eq!(
            set.coverage_ratio_f64_of(query),
            expected_f64,
            "query = {query:?}"
        );
    }
}

proptest! {
    #[test]
    fn prop_lengths_partition_query(
        xs in prop::collection::vec(arb_iv(), 0..64),
        query in arb_iv(),
    ) {
        let set: I8COSet = xs.into_iter().collect();

        prop_assert_eq!(
            set.covered_len_of(query) + set.uncovered_len_of(query),
            query.len()
        );
    }

    #[test]
    fn prop_covered_len_is_positive_iff_query_intersects_set(
        xs in prop::collection::vec(arb_iv(), 0..64),
        query in arb_iv(),
    ) {
        let set: I8COSet = xs.into_iter().collect();

        prop_assert_eq!(
            set.covered_len_of(query) > 0,
            set.intersects_interval(query)
        );
    }

    #[test]
    fn prop_coverage_ratio_matches_covered_len_over_query_len(
        xs in prop::collection::vec(arb_iv(), 0..64),
        query in arb_iv(),
    ) {
        let set: I8COSet = xs.into_iter().collect();

        let expected_f32 = set.covered_len_of(query).as_f32() / query.len().as_f32();
        let expected_f64 = set.covered_len_of(query).as_f64() / query.len().as_f64();

        prop_assert_eq!(set.coverage_ratio_f32_of(query), expected_f32);
        prop_assert_eq!(set.coverage_ratio_f64_of(query), expected_f64);
    }

    #[test]
    fn prop_covered_len_matches_sum_of_clipped_intersections(
        xs in prop::collection::vec(arb_iv(), 0..64),
        query in arb_iv(),
    ) {
        let set: I8COSet = xs.into_iter().collect();

        let expected: u8 = set
            .intersection_with_interval(query)
            .iter_intervals()
            .map(|iv| iv.len())
            .sum();

        prop_assert_eq!(set.covered_len_of(query), expected);
    }
}