int-interval 0.9.4

A small, no_std half-open interval algebra library for primitive integer types.
Documentation
use super::*;

mod about_start_len {
    use super::*;

    mod unit_tests {
        use super::*;

        const MIN: u8 = u8::MIN;
        const MAX: u8 = u8::MAX;
        const ONE: u8 = 1;

        fn span(lo: u8, hi: u8) -> U8CO {
            U8CO::try_new(lo, hi).unwrap()
        }

        #[test]
        fn checked_from_start_len_basic() {
            let iv = U8CO::checked_from_start_len(10, 5).unwrap();
            assert_eq!(iv, span(10, 15));
            assert_eq!(iv.start(), 10);
            assert_eq!(iv.end_excl(), 15);
            assert_eq!(iv.len(), 5);

            assert!(U8CO::checked_from_start_len(10, 0).is_none());
        }

        #[test]
        fn checked_from_start_len_accepts_max_end_excl() {
            let len = 5;
            let start = MAX - len;

            let iv = U8CO::checked_from_start_len(start, len).unwrap();

            assert_eq!(iv, span(start, MAX));
            assert_eq!(iv.start(), start);
            assert_eq!(iv.end_excl(), MAX);
            assert_eq!(iv.len(), len);
        }

        #[test]
        fn checked_from_start_len_rejects_overflow() {
            assert!(U8CO::checked_from_start_len(MAX, ONE).is_none());
            assert!(U8CO::checked_from_start_len(MAX - ONE, 2).is_none());
            assert!(U8CO::checked_from_start_len(MAX - 5, 6).is_none());
        }

        #[test]
        fn saturating_from_start_len_basic() {
            let iv = U8CO::saturating_from_start_len(10, 5).unwrap();

            assert_eq!(iv, span(10, 15));
            assert_eq!(iv.start(), 10);
            assert_eq!(iv.end_excl(), 15);
            assert_eq!(iv.len(), 5);

            assert!(U8CO::saturating_from_start_len(10, 0).is_none());
        }

        #[test]
        fn saturating_from_start_len_accepts_exact_max_end_excl() {
            let len = 5;
            let start = MAX - len;

            let iv = U8CO::saturating_from_start_len(start, len).unwrap();

            assert_eq!(iv, span(start, MAX));
            assert_eq!(iv.start(), start);
            assert_eq!(iv.end_excl(), MAX);
            assert_eq!(iv.len(), len);
        }

        #[test]
        fn saturating_from_start_len_clamps_end() {
            let exact_len = 5;
            let requested_len = exact_len + ONE;
            let start = MAX - exact_len;

            let iv = U8CO::saturating_from_start_len(start, requested_len).unwrap();

            assert_eq!(iv, span(start, MAX));
            assert_eq!(iv.start(), start);
            assert_eq!(iv.end_excl(), MAX);
            assert_eq!(iv.len(), exact_len);
            assert!(iv.len() < requested_len);
        }

        #[test]
        fn saturating_from_start_len_rejects_empty_after_saturation() {
            assert!(U8CO::saturating_from_start_len(MAX, ONE).is_none());
            assert!(U8CO::saturating_from_start_len(MAX, MAX).is_none());
        }

        #[test]
        fn checked_from_start_len_reconstructs_existing_interval() {
            let cases = [
                (MIN, MIN + ONE),
                (MIN, 10),
                (10, 20),
                (MAX - 10, MAX),
                (MAX - ONE, MAX),
            ];

            for (start, end_excl) in cases {
                let base = span(start, end_excl);
                let rebuilt = U8CO::checked_from_start_len(start, base.len()).unwrap();

                assert_eq!(rebuilt, base, "case [{start}, {end_excl})");
            }
        }
    }

    mod prop_tests {
        use std::{vec, vec::Vec};

        use super::*;
        use proptest::prelude::*;

        fn edge_values() -> Vec<u8> {
            let mut v = vec![
                u8::MIN,
                u8::MIN.saturating_add(1),
                1,
                u8::MAX.saturating_sub(1),
                u8::MAX,
            ];
            v.sort_unstable();
            v.dedup();
            v
        }

        fn mixed_scalar() -> impl Strategy<Value = u8> {
            prop_oneof! {
                3 => prop::sample::select(edge_values()),
                7 => any::<u8>(),
            }
        }

        proptest! {
            #[test]
            fn checked_from_start_len_matches_checked_add(
                start in mixed_scalar(),
                len in mixed_scalar(),
            ) {
                let got = U8CO::checked_from_start_len(start, len);

                let expected = if len == 0 {
                    None
                } else {
                    start
                        .checked_add(len)
                        .and_then(|end_excl| U8CO::try_new(start, end_excl))
                };

                prop_assert_eq!(got, expected);
            }

            #[test]
            fn checked_from_start_len_preserves_exact_len(
                start in mixed_scalar(),
                len in mixed_scalar(),
            ) {
                if let Some(iv) = U8CO::checked_from_start_len(start, len) {
                    prop_assert_eq!(iv.start(), start);
                    prop_assert_eq!(iv.len(), len);
                    prop_assert_eq!(iv.end_excl(), start + len);
                    prop_assert!(iv.start() < iv.end_excl());
                }
            }

            #[test]
            fn checked_from_start_len_reconstructs_existing_interval(
                start in mixed_scalar(),
                end_excl in mixed_scalar(),
            ) {
                if let Some(base) = U8CO::try_new(start, end_excl) {
                    let rebuilt = U8CO::checked_from_start_len(base.start(), base.len());

                    prop_assert_eq!(rebuilt, Some(base));
                }
            }

            #[test]
            fn checked_from_start_len_rejects_zero_len(start in mixed_scalar()) {
                prop_assert_eq!(U8CO::checked_from_start_len(start, 0), None);
            }

            #[test]
            fn saturating_from_start_len_matches_saturating_add(
                start in mixed_scalar(),
                len in mixed_scalar(),
            ) {
                let got = U8CO::saturating_from_start_len(start, len);

                let expected = if len == 0 {
                    None
                } else {
                    U8CO::try_new(start, start.saturating_add(len))
                };

                prop_assert_eq!(got, expected);
            }

            #[test]
            fn saturating_from_start_len_preserves_invariant(
                start in mixed_scalar(),
                len in mixed_scalar(),
            ) {
                if let Some(iv) = U8CO::saturating_from_start_len(start, len) {
                    prop_assert_eq!(iv.start(), start);
                    prop_assert!(iv.start() < iv.end_excl());
                    prop_assert!(iv.len() <= len);
                }
            }

            #[test]
            fn saturating_from_start_len_matches_checked_when_checked_succeeds(
                start in mixed_scalar(),
                len in mixed_scalar(),
            ) {
                let checked = U8CO::checked_from_start_len(start, len);
                let saturated = U8CO::saturating_from_start_len(start, len);

                if checked.is_some() {
                    prop_assert_eq!(saturated, checked);
                }
            }

            #[test]
            fn saturating_from_start_len_rejects_zero_len(start in mixed_scalar()) {
                prop_assert_eq!(U8CO::saturating_from_start_len(start, 0), None);
            }
        }
    }
}

mod about_midpoint {
    use super::*;

    mod unit_tests {
        use super::*;

        fn span(lo: u8, hi: u8) -> U8CO {
            U8CO::try_new(lo, hi).unwrap()
        }

        #[test]
        fn midpoint_basic() {
            let iv = span(10, 20);
            assert_eq!(iv.midpoint(), 15);

            let iv2 = span(0, 2);
            assert_eq!(iv2.midpoint(), 1);

            let iv3 = span(0, u8::MAX);
            assert_eq!(iv3.midpoint(), (0 + ((u8::MAX - 0) / 2)));
        }

        #[test]
        fn checked_from_midpoint_len_basic() {
            let mid = 10;
            let len = 5;
            let iv = U8CO::checked_from_midpoint_len(mid, len).unwrap();
            assert_eq!(iv.len(), len);
            assert_eq!(iv.midpoint(), mid);

            // len=0 returns None
            assert!(U8CO::checked_from_midpoint_len(mid, 0).is_none());

            // overflow start returns None
            assert!(U8CO::checked_from_midpoint_len(0, 5).is_none());

            // overflow end returns None
            assert!(U8CO::checked_from_midpoint_len(u8::MAX, 10).is_none());
        }

        #[test]
        fn saturating_from_midpoint_len_basic() {
            let mid = 10;
            let len = 5;
            let iv = U8CO::saturating_from_midpoint_len(mid, len).unwrap();
            assert_eq!(iv.len(), len);
            assert_eq!(iv.midpoint(), mid);

            // len=0 returns None
            assert!(U8CO::saturating_from_midpoint_len(mid, 0).is_none());

            // saturating start
            let iv2 = U8CO::saturating_from_midpoint_len(0, 5).unwrap();
            assert_eq!(iv2.start(), 0);
            assert_eq!(iv2.len(), 5);

            // saturating end
            let iv3 = U8CO::saturating_from_midpoint_len(u8::MAX, 10).unwrap();
            assert!(iv3.end_excl() <= u8::MAX);
        }
    }

    mod prop_tests {
        use std::{vec, vec::Vec};

        use super::*;
        use proptest::prelude::*;

        fn edge_values() -> Vec<u8> {
            let mut v = vec![u8::MIN, u8::MAX, 0, 1];
            if u8::MIN < u8::MAX {
                v.push(u8::MIN.saturating_add(1));
                v.push(u8::MAX.saturating_sub(1));
            }
            v.sort_unstable();
            v.dedup();
            v
        }

        fn edge_scalar() -> impl Strategy<Value = u8> {
            prop::sample::select(edge_values())
        }

        fn mixed_scalar() -> impl Strategy<Value = u8> {
            prop_oneof! {
                3 => edge_scalar(),
                7 => any::<u8>(),
            }
        }

        proptest! {
            #[test]
            fn midpoint_rounds_down(x in mixed_scalar(), y in mixed_scalar()) {
                let lo = x.min(y);
                let hi = x.max(y).saturating_add(1); // ensure non-empty
                if let Some(iv) = U8CO::try_new(lo, hi) {
                    let mp = iv.midpoint();
                    // midpoint in interval and closer to start
                    prop_assert!(mp >= iv.start() && mp <= iv.end_incl());
                    prop_assert_eq!(mp, iv.start() + (iv.len() / 2));
                }
            }

            #[test]
            fn checked_from_midpoint_len_inverse(mid in mixed_scalar(), len in 1u8..20) {
                if let Some(iv) = U8CO::checked_from_midpoint_len(mid, len) {
                    prop_assert_eq!(iv.len(), len);
                    prop_assert_eq!(iv.midpoint(), mid);
                }
            }

            #[test]
            fn saturating_from_midpoint_len_safety(mid in mixed_scalar(), len in 1u8..20) {
                if let Some(iv) = U8CO::saturating_from_midpoint_len(mid, len) {
                    // start < end_excl invariant
                    prop_assert!(iv.start() < iv.end_excl());
                    prop_assert_eq!(iv.midpoint(), iv.start() + (iv.len()/2));
                }
            }
        }
    }
}