use core::cmp::Ordering;
use puremp::Rational;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Bound {
Infinite,
Finite { value: Rational, open: bool },
}
impl Bound {
pub fn closed(value: Rational) -> Bound {
Bound::Finite { value, open: false }
}
pub fn open(value: Rational) -> Bound {
Bound::Finite { value, open: true }
}
pub fn infinite() -> Bound {
Bound::Infinite
}
fn is_open(&self) -> bool {
matches!(self, Bound::Finite { open: true, .. }) || matches!(self, Bound::Infinite)
}
fn finite_value(&self) -> Option<&Rational> {
match self {
Bound::Finite { value, .. } => Some(value),
Bound::Infinite => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Interval {
Empty,
Range { lower: Bound, upper: Bound },
}
impl Interval {
pub fn new(lower: Bound, upper: Bound) -> Interval {
match (lower.finite_value(), upper.finite_value()) {
(Some(l), Some(u)) => match l.cmp(u) {
Ordering::Greater => Interval::Empty,
Ordering::Equal if lower.is_open() || upper.is_open() => Interval::Empty,
_ => Interval::Range { lower, upper },
},
_ => Interval::Range { lower, upper },
}
}
pub fn all() -> Interval {
Interval::new(Bound::infinite(), Bound::infinite())
}
pub fn point(v: Rational) -> Interval {
Interval::new(Bound::closed(v.clone()), Bound::closed(v))
}
pub fn closed(lo: Rational, hi: Rational) -> Interval {
Interval::new(Bound::closed(lo), Bound::closed(hi))
}
pub fn is_empty(&self) -> bool {
matches!(self, Interval::Empty)
}
pub fn contains(&self, x: &Rational) -> bool {
let Interval::Range { lower, upper } = self else {
return false;
};
let lower_ok = match lower {
Bound::Infinite => true,
Bound::Finite { value, open } => match x.cmp(value) {
Ordering::Greater => true,
Ordering::Equal => !open,
Ordering::Less => false,
},
};
let upper_ok = match upper {
Bound::Infinite => true,
Bound::Finite { value, open } => match x.cmp(value) {
Ordering::Less => true,
Ordering::Equal => !open,
Ordering::Greater => false,
},
};
lower_ok && upper_ok
}
pub fn bounds(&self) -> Option<(&Bound, &Bound)> {
match self {
Interval::Empty => None,
Interval::Range { lower, upper } => Some((lower, upper)),
}
}
pub fn neg(&self) -> Interval {
let Interval::Range { lower, upper } = self else {
return Interval::Empty;
};
Interval::new(neg_bound(upper), neg_bound(lower))
}
pub fn add(&self, other: &Interval) -> Interval {
let (
Interval::Range {
lower: l1,
upper: u1,
},
Interval::Range {
lower: l2,
upper: u2,
},
) = (self, other)
else {
return Interval::Empty;
};
Interval::new(add_bound(l1, l2), add_bound(u1, u2))
}
pub fn sub(&self, other: &Interval) -> Interval {
self.add(&other.neg())
}
pub fn mul(&self, other: &Interval) -> Interval {
let (
Interval::Range {
lower: l1,
upper: u1,
},
Interval::Range {
lower: l2,
upper: u2,
},
) = (self, other)
else {
return Interval::Empty;
};
let corners = [
mul_bound(l1, l2),
mul_bound(l1, u2),
mul_bound(u1, l2),
mul_bound(u1, u2),
];
if corners.iter().any(|c| c.is_none()) {
return infinite_mul_hull(&corners);
}
let mut pts: Vec<(Rational, bool)> = corners.into_iter().map(|c| c.unwrap()).collect();
pts.sort_by(|a, b| a.0.cmp(&b.0));
let min = pts.first().unwrap().clone();
let max = pts.last().unwrap().clone();
Interval::new(
Bound::Finite {
value: min.0,
open: min.1,
},
Bound::Finite {
value: max.0,
open: max.1,
},
)
}
pub fn intersect(&self, other: &Interval) -> Interval {
let (
Interval::Range {
lower: l1,
upper: u1,
},
Interval::Range {
lower: l2,
upper: u2,
},
) = (self, other)
else {
return Interval::Empty;
};
let lower = max_lower(l1, l2);
let upper = min_upper(u1, u2);
Interval::new(lower, upper)
}
}
use alloc::vec::Vec;
fn neg_bound(b: &Bound) -> Bound {
match b {
Bound::Infinite => Bound::Infinite,
Bound::Finite { value, open } => Bound::Finite {
value: value.neg(),
open: *open,
},
}
}
fn add_bound(a: &Bound, b: &Bound) -> Bound {
match (a, b) {
(Bound::Infinite, _) | (_, Bound::Infinite) => Bound::Infinite,
(Bound::Finite { value: x, open: ox }, Bound::Finite { value: y, open: oy }) => {
Bound::Finite {
value: x.add(y),
open: *ox || *oy,
}
}
}
}
fn mul_bound(a: &Bound, b: &Bound) -> Option<(Rational, bool)> {
match (a, b) {
(Bound::Finite { value: x, open: ox }, Bound::Finite { value: y, open: oy }) => {
let prod = x.mul(y);
let open = (*ox && !y.is_zero()) || (*oy && !x.is_zero());
Some((prod, open))
}
(Bound::Infinite, Bound::Finite { value, .. })
| (Bound::Finite { value, .. }, Bound::Infinite)
if value.is_zero() =>
{
Some((Rational::from_integer(0.into()), false))
}
_ => None,
}
}
fn infinite_mul_hull(corners: &[Option<(Rational, bool)>; 4]) -> Interval {
let finite: Vec<&(Rational, bool)> = corners.iter().filter_map(|c| c.as_ref()).collect();
if finite.is_empty() {
return Interval::all();
}
Interval::all()
}
fn max_lower(a: &Bound, b: &Bound) -> Bound {
match (a, b) {
(Bound::Infinite, other) | (other, Bound::Infinite) => other.clone(),
(Bound::Finite { value: x, open: ox }, Bound::Finite { value: y, open: oy }) => {
match x.cmp(y) {
Ordering::Greater => a.clone(),
Ordering::Less => b.clone(),
Ordering::Equal => Bound::Finite {
value: x.clone(),
open: *ox || *oy, },
}
}
}
}
fn min_upper(a: &Bound, b: &Bound) -> Bound {
match (a, b) {
(Bound::Infinite, other) | (other, Bound::Infinite) => other.clone(),
(Bound::Finite { value: x, open: ox }, Bound::Finite { value: y, open: oy }) => {
match x.cmp(y) {
Ordering::Less => a.clone(),
Ordering::Greater => b.clone(),
Ordering::Equal => Bound::Finite {
value: x.clone(),
open: *ox || *oy,
},
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn r(n: i64) -> Rational {
Rational::from_integer(n.into())
}
#[test]
fn membership_open_closed() {
let c = Interval::closed(r(1), r(3));
assert!(c.contains(&r(1)) && c.contains(&r(3)) && c.contains(&r(2)));
assert!(!c.contains(&r(0)) && !c.contains(&r(4)));
let o = Interval::new(Bound::open(r(1)), Bound::open(r(3)));
assert!(!o.contains(&r(1)) && !o.contains(&r(3)) && o.contains(&r(2)));
}
#[test]
fn empty_normalisation() {
assert!(Interval::new(Bound::closed(r(3)), Bound::closed(r(1))).is_empty());
assert!(Interval::new(Bound::open(r(2)), Bound::closed(r(2))).is_empty());
assert!(!Interval::point(r(2)).is_empty());
}
#[test]
fn add_and_neg() {
let a = Interval::closed(r(1), r(2));
let b = Interval::closed(r(10), r(20));
assert_eq!(a.add(&b), Interval::closed(r(11), r(22)));
assert_eq!(a.neg(), Interval::closed(r(-2), r(-1)));
assert_eq!(a.sub(&b), Interval::closed(r(-19), r(-8)));
}
#[test]
fn mul_sign_cases() {
let a = Interval::closed(r(-2), r(3));
let b = Interval::closed(r(-1), r(4));
assert_eq!(a.mul(&b), Interval::closed(r(-8), r(12)));
}
#[test]
fn intersect_basic() {
let a = Interval::closed(r(0), r(5));
let b = Interval::closed(r(3), r(9));
assert_eq!(a.intersect(&b), Interval::closed(r(3), r(5)));
let disjoint = Interval::closed(r(0), r(1)).intersect(&Interval::closed(r(2), r(3)));
assert!(disjoint.is_empty());
}
#[test]
fn unbounded_bounds() {
let nonneg = Interval::new(Bound::closed(r(0)), Bound::infinite());
assert!(nonneg.contains(&r(1000000)) && nonneg.contains(&r(0)));
assert!(!nonneg.contains(&r(-1)));
let pos = Interval::new(Bound::open(r(0)), Bound::infinite());
assert_eq!(pos.add(&pos.neg()), Interval::all());
}
#[test]
fn mul_soundness_sampled() {
let a = Interval::closed(r(-3), r(2));
let b = Interval::closed(r(-5), r(7));
let prod = a.mul(&b);
for x in -3..=2 {
for y in -5..=7 {
assert!(
prod.contains(&r(x * y)),
"{}*{}={} not in {:?}",
x,
y,
x * y,
prod
);
}
}
}
}