#interval #bounds #no-std

no-std intrval

Generic intervals (ranges) library

7 releases

0.2.0 Mar 30, 2026
0.1.4 Feb 26, 2026
0.1.1 Jan 20, 2026
0.0.1 Dec 19, 2025

#202 in Math

MIT/Apache

330KB
7K SLoC

intrvals

Build Status Latest version MSRV Documentation codecov

intrvals is a generic intervals library with basic arithmetic (+, -, *) and sets (union, intersect, complement) operations support.

Its main structure is an Interval which represents a single-dimensioned (possibly unbounded) span of values instantiated by any type T.

Continuous intervals

If the underlying type T is discrete like one of the integer types, the Interval is not really continuous, but we still consider it contains every value between the given bounds.

macro syntax

Allows to simplify the definitions of an interval in common inequality and ranges terms.

For the half-open intervals, the inclusive bound is marked with = symbol; for the closed interval both [a, b] and (=a, =b) definitions are possible.

# use intrval::{interval, Interval};

let i0: Interval<i16> = interval!(0);
assert_eq!(i0, Interval::Empty);

let igt2: Interval<i16> = interval!(> 2);
assert_eq!(igt2, Interval::GreaterThan(2));

let igt10: Interval<i16> = interval!(> 10);
assert_eq!(igt10, Interval::GreaterThan(10));

let i_2to10_incl: Interval<i16> = interval!([-2, 10]);
assert_eq!(i_2to10_incl, Interval::Closed((-2, 10)));

let i5to20_excl: Interval<i16> = interval!((5, =20));
assert_eq!(i5to20_excl, Interval::LeftOpen((5, 20)));

let iuni: Interval<i16> = interval!(U);
assert_eq!(iuni, Interval::Full);

common functions

Functions not falling into arithmetic or set categories, but still common for all intervals.

# use core::cmp::Ordering;
# use intrval::{interval, Size};

assert!(interval!(0: i32).is_empty());
assert!(interval!((1, 0)).is_empty());
assert!(interval!((0, 0)).is_empty());
assert!(!(interval!([0, 0]).is_empty()));
assert!(interval!(U: i32).is_full());

let igt10 = interval!(> 10);
assert!(igt10.contains(&11));
assert!(!(igt10.contains(&10)));
assert!(!(interval!(0).contains(&0)));

assert_eq!(interval!(0: i8).len(), Size::Empty);
assert_eq!(interval!((1, 0)).len(), Size::Empty);
assert_eq!(interval!([1, 1]).len(), Size::SinglePoint);
assert_eq!(interval!((-10, =10)).len(), Size::Finite(20));
assert_eq!(interval!(> 2).len(), Size::Infinite);

let i_left_open = interval!((2, =5));
assert_eq!(i_left_open.clamp(10).unwrap(), (Ordering::Equal, 5));
assert_eq!(i_left_open.clamp(3).unwrap(), (Ordering::Equal, 3));
assert_eq!(i_left_open.clamp(2).unwrap(), (Ordering::Greater, 2));
assert_eq!(i_left_open.clamp(0).unwrap(), (Ordering::Greater, 2));

scalar arithmetic

Add, subtract or multiply the interval bounds with a scalar value of type U if the underlying type T: {Add, Sub, Mul}<U>.

# use intrval::{interval, Interval};

// negation changes the sign and flips the bounds
assert_eq!(-interval!(> 2), interval!(< -2));
assert_eq!(-interval!([-2, 10]), interval!([-10, 2]));

// full and empy does not change with scalars
assert_eq!(interval!(0) + 5, interval!(0));
assert_eq!(interval!(0: i32) * 2, interval!(0));
assert_eq!(interval!(U) - 100, interval!(U));
assert_eq!(interval!(U: i32) * -5, interval!(U));
// however, multiplying by 0 is different
assert_eq!(interval!(U: i32) * 0, interval!([0, 0]));

assert_eq!(interval!(> 2) + 3, interval!(> 5));
assert_eq!(interval!(> 10) - 5, interval!(> 5));
assert_eq!(interval!(> 2) * 5, interval!(> 10));
assert_eq!(interval!((5, =20)) / 5, Interval::LeftOpen((1, 4)));
// multiplying/dividing by negative flips the bounds
assert_eq!(interval!([-2, 10]) * -4, interval!([-40, 8]));
assert_eq!(interval!([16, 79]) / -8, Interval::Closed((-9, -2)));

interval arithmetic

Add, subtract or multiply an Interval<T> with an Interval<U> to produce another Interval<Z> if the underlying type T: {Add, Sub, Mul}<U, Output=Z>.

# use intrval::interval;

let i0 = interval!(0: i32);
let igt10 = interval!(> 10);
let i_2to10_incl = interval!([-2, 10]);
let i5to20_excl = interval!((5, =20));
let iuni = interval!(U: i32);

assert_eq!(igt10 + i_2to10_incl, interval!(> 8));
// adding an empty interval one does not change the proper one
assert_eq!(igt10 + interval!((1, 0)), igt10);
assert_eq!(interval!((1, 0)) + igt10, igt10);

assert_eq!(i_2to10_incl - i5to20_excl, interval!((=-22, 5)));
// subtracting an empty interval does not change the proper one
assert_eq!(igt10 - interval!((1, 0)), igt10);
// subtracting _from_ an empty interval negates the proper one
assert_eq!(interval!((2, 0)) - i_2to10_incl, -i_2to10_incl);

// Interval::Empty is neutral over multiplication
assert_eq!(i0 * i_2to10_incl, i0);
// positive (+inf) times positive is positive
assert_eq!(interval!(> 2) * igt10, interval!(> 20));
// positive (+inf) times (negative and positive) is (-inf, +inf)
assert_eq!(igt10 * i_2to10_incl, interval!(U));
assert_eq!(igt10 * i5to20_excl, interval!(> 50));
// Interval::Full is neutral over multiplication
assert_eq!(i5to20_excl * iuni, iuni);

set operations

Representation of an Interval-s as a sets of points allows to apply various set operations on them.

# use intrval::{interval, SetOps as _};

let igt2 = interval!(> 2);
let igt_e2 = interval!(>= 2);
let igt10 = interval!(> 10);

assert!(igt_e2.is_super(&igt2));
assert!(igt2.is_sub(&igt_e2));
assert!(!igt2.is_super(&igt_e2));
assert!(!igt_e2.is_sub(&igt2));
assert!(igt2.is_super(&igt10));
assert!(igt10.is_sub(&igt2));
assert!(igt2.is_disjoint(&interval!(< 0)));
assert!(igt2.is_disjoint(&interval!(< 2)));
assert!(!igt2.is_disjoint(&interval!(<= 2)));

assert_eq!(igt2.complement().into_single().unwrap(), interval!(<= 2));
// `.complement` is aliased with `!`
assert_eq!(
    (!interval!([-2, 10])).into_pair().unwrap(),
    (interval!(< -2), igt10)
);

assert_eq!(
    igt2.difference(igt10).unwrap().into_single().unwrap(),
    interval!((2, =10)),
);
assert_eq!(
    igt2.symmetric_difference(interval!(<= 5))
        .unwrap()
        .into_pair()
        .unwrap(),
    (interval!(<= 2), interval!(> 5)),
);
// `.symmetric_difference` is aliased with `^` (falling back to Interval::Empty)
assert_eq!(
    (interval!(<= 5) ^ interval!((0, 5))).into_pair().unwrap(),
    (interval!(<= 0), interval!(== 5)),
);

assert_eq!(igt2.intersect(igt10).unwrap(), igt10);
// `.intersect` is aliased with `&` (falling back to Interval::Empty)
assert!((igt10 & interval!([-2, 10])).is_empty());
assert_eq!(
    interval!([-2, 10]) & interval!((5, =20)),
    interval!((5, =10))
);

assert_eq!(
    igt10.union(interval!([-2, 10])).into_single().unwrap(),
    interval!(>= -2)
);
// `.union` is aliased with `|`
assert_eq!(
    (interval!([-2, 10]) | igt2).into_single().unwrap(),
    interval!(>= -2)
);
assert_eq!(
    (interval!((5, 10)) | interval!([3, 4]))
        .into_pair()
        .unwrap(),
    // reorders the input intervals in left-to-right order if they do not intersect
    (interval!([3, 4]), interval!((5, 10)))
);
assert_eq!((interval!(0) | igt2).into_single().unwrap(), igt2);
assert_eq!((igt2 | interval!(0)).into_single().unwrap(), igt2);

assert_eq!(interval!([-2, 10]).enclosure(igt10 * 2), interval!(>= -2));

Discrete intervals

LinearSpace is an Interval combined with the notion of step to represent discrete space of points.

constructors (injectors) and getters (projectors)

Create and deconstruct discrete intervals.

# use core::convert::identity;
# use intrval::{discrete::LinearSpace, interval};

let space = LinearSpace::try_bounded(interval!([-2, 10]), 3_i8).unwrap();
assert_eq!(space.bounds(), &interval!([-2, 10]));
assert_eq!(space.step(), &3);
assert_eq!(space.into_parts(), (interval!([-2, 10]), 3));
assert_eq!(
    space.map(|x| x * 2, identity).unwrap().into_parts(),
    (interval!([-4, 20]), 3)
);

let space = LinearSpace::<i8, i8>::try_new(10).unwrap();
assert_eq!(space.bounds(), &interval!(U));
assert_eq!(space.step(), &10);
assert_eq!(space.into_parts(), (interval!(U), 10));
assert_eq!(
    space.map(|x| x + 2, |x| x + 2).unwrap().into_parts(),
    (interval!(U), 12)
);
assert!(space.map(identity, |x| x - 10).is_none());

arithmetic operations

Modify the space by doing some simple operations using underlying Interval implementations.

# use intrval::{discrete::LinearSpace, interval};

let space = LinearSpace::try_bounded(interval!((20, =142)), 10_u16).unwrap();

// `Shl` and `Shr` (forwarded to underlying `Interval`, without changing the `step`)
assert_eq!((space >> 5).into_parts(), (interval!((25, =147)), 10));
assert_eq!((space << 20).into_parts(), (interval!((0, =122)), 10));

// `Mul`/`Div` by scalar to extend/shrink the `Interval` along with the `step`
assert_eq!(
    (space * 10).unwrap().into_parts(),
    (interval!((200, =1420)), 100)
);
assert_eq!((space / 3).unwrap().into_parts(), (interval!((6, =47)), 3));

// `Mul` by another `LinearSpace`
let space2 = LinearSpace::try_bounded(interval!([5, 10]), 3).unwrap();
assert_eq!(
    (space * space2).unwrap().into_parts(),
    (interval!((100, =1420)), 30)
);

iterators

Convert a space into forward and backward iterators.

# use intrval::{discrete::LinearSpace, interval};

let space = LinearSpace::try_bounded(interval!((20_u16, =80)), 10).unwrap();
assert_eq!(
    space.try_into_forward_iter().unwrap().collect::<Vec<_>>(),
    [30, 40, 50, 60, 70, 80]
);
assert_eq!(
    space.into_forward_iter_from(65).collect::<Vec<_>>(),
    [70, 80]
);

let space_unbounded_lower = LinearSpace::try_bounded(interval!(<= 20_u16), 5).unwrap();
let _err = space_unbounded_lower.try_into_forward_iter().unwrap_err();
assert_eq!(
    space_unbounded_lower
        .into_forward_iter_from(8)
        .collect::<Vec<_>>(),
    [10, 15, 20]
);

assert_eq!(
    space.try_into_backward_iter().unwrap().collect::<Vec<_>>(),
    [80, 70, 60, 50, 40, 30]
);
assert_eq!(
    space.into_backward_iter_up_to(42).collect::<Vec<_>>(),
    [40, 30]
);

let space_unbounded_upper = LinearSpace::try_bounded(interval!(> 100_u8), 5).unwrap();
let _err = space_unbounded_upper.try_into_backward_iter().unwrap_err();
assert_eq!(
    space_unbounded_upper
        .into_backward_iter_up_to(127)
        .collect::<Vec<_>>(),
    [125, 120, 115, 110, 105]
);

rounding

Round (using Rounding trait) a point: T to the values of the space.

# use intrval::{
#     discrete::LinearSpace,
#     interval,
#     rounding::{DirectedMode, NearestMode, RoundError, Rounding as _},
# };

let space = LinearSpace::try_bounded(interval!(> 100_u8), 4).unwrap();

assert_eq!(space.round(&102, DirectedMode::UP).unwrap(), 104);
assert_eq!(space.round(&117, DirectedMode::DOWN).unwrap(), 116);
assert_eq!(
    space.round(&253, DirectedMode::AwayFromZero).unwrap_err(),
    RoundError::InvalidDirection {
        rounded: 252,
        direction: DirectedMode::AwayFromZero
    }
);
assert_eq!(
    space
        .round(&142, NearestMode(DirectedMode::DOWN))
        .unwrap(),
    140,
);

#[cfg(feature = "random")]
{
    use intrval::rounding::StochasticMode;
    let rounded = space
        .round_with_rng(
            &141,
            StochasticMode,
            // you should provide an optional RNG for the stable results,
            // see the caveats of using fallback RNG in docs for `round_with_rng` method.
            None,
        )
        .unwrap();
    assert!([140, 144].contains(&rounded));
}

Features

By default, all the features below are disabled to ensure minimalistic no-dependency library.

std

Enables several improvements made thanks to including std library:

  • impls of numeric traits for std::time::SystemTime (useful in rounding);
  • using {f32,f64}::trunc() instead of manual implementation of it;
  • using thread_local!(RefCell) for default RNG (with random feature, see below).

serde

Enables the support of serde::{Serialize, Deserialize} for Interval<T> and a variety of rounding modes.

arbitrary

Enables the proptest::Arbitrary for Interval<T> along with the property tests (could be invoked with cargo test prop_test --features=arbitrary).

random

Enables the ability to use stochastic rounding (discrete::rounding::Mode::Stochastic) and random tie-breaking (in the discrete::rounding::Mode::Nearest).

This feature also enables the Rounding::round_with_rng to be able to provide a custom RNG (random number generator implementing rand::RngCore). If you do not provide an RNG (either calling Rounding::round_with_rng with None or calling Rounding::round) using any of the modes mentioned above, be aware that the default very simple RNG (rand::SmallRng) will be used. Its seed can be provided as a CONST_RANDOM_SEED environment variable at compile time (during cargo build or cargo run).

singleton

This feature is useful if you want to have a dedicated Interval::Singleton variant (otherwise represented as Interval::Closed). This allows to create an Interval from a single value without cloning it. However, to get the bounds of an Interval, you have to meet the requirement of T: Clone again.

The table below summarizes these limitations for the Interval<T>

use case singleton feature no singleton feature
create an Interval from a single point
(Singleton::singleton)
- T: Clone
convert an Interval into a pair of Endpoint-s
(Bounded::into_bounds)
T: Clone -

There are two helper traits used to implement the desired feature-gated behaviour:

  • trait Singleton to create a point-sized Interval from a single value (when the feature is disabled, create an Interval::Closed((x.clone(), x)) instead);
  • trait SingletonBounds to convert a single value into a pair of Endpoint-s (when the feature is disabled, this trait has no methods and implemented for an Interval<T> unconditionally).

Dependencies

~0–1MB
~19K SLoC