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 (el1, eu1) = (Ev::lower(l1), Ev::upper(u1));
let (el2, eu2) = (Ev::lower(l2), Ev::upper(u2));
let corners = [
ev_mul(&el1, &el2),
ev_mul(&el1, &eu2),
ev_mul(&eu1, &el2),
ev_mul(&eu1, &eu2),
];
Interval::new(
ev_extreme(&corners, true).to_lower_bound(),
ev_extreme(&corners, false).to_upper_bound(),
)
}
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)
}
}
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,
}
}
}
}
#[derive(Clone)]
enum Ev {
NegInf,
PosInf,
Fin { value: Rational, open: bool },
}
impl Ev {
fn lower(b: &Bound) -> Ev {
match b {
Bound::Infinite => Ev::NegInf,
Bound::Finite { value, open } => Ev::Fin {
value: value.clone(),
open: *open,
},
}
}
fn upper(b: &Bound) -> Ev {
match b {
Bound::Infinite => Ev::PosInf,
Bound::Finite { value, open } => Ev::Fin {
value: value.clone(),
open: *open,
},
}
}
fn sign(&self) -> i32 {
match self {
Ev::NegInf => -1,
Ev::PosInf => 1,
Ev::Fin { value, .. } => value.signum(),
}
}
fn to_lower_bound(&self) -> Bound {
match self {
Ev::NegInf | Ev::PosInf => Bound::infinite(),
Ev::Fin { value, open } => Bound::Finite {
value: value.clone(),
open: *open,
},
}
}
fn to_upper_bound(&self) -> Bound {
self.to_lower_bound() }
}
fn ev_cmp(a: &Ev, b: &Ev) -> Ordering {
match (a, b) {
(Ev::NegInf, Ev::NegInf) | (Ev::PosInf, Ev::PosInf) => Ordering::Equal,
(Ev::NegInf, _) | (_, Ev::PosInf) => Ordering::Less,
(Ev::PosInf, _) | (_, Ev::NegInf) => Ordering::Greater,
(Ev::Fin { value: x, .. }, Ev::Fin { value: y, .. }) => x.cmp(y),
}
}
fn ev_mul(a: &Ev, b: &Ev) -> Ev {
if let (Ev::Fin { value: x, open: ox }, Ev::Fin { value: y, open: oy }) = (a, b) {
let open = (*ox && !y.is_zero()) || (*oy && !x.is_zero());
return Ev::Fin {
value: x.mul(y),
open,
};
}
let zero = |e: &Ev| matches!(e, Ev::Fin { value, .. } if value.is_zero());
if zero(a) || zero(b) {
return Ev::Fin {
value: Rational::from_integer(0.into()),
open: false,
};
}
if a.sign() * b.sign() < 0 {
Ev::NegInf
} else {
Ev::PosInf
}
}
fn ev_extreme(corners: &[Ev; 4], want_min: bool) -> Ev {
let mut best = &corners[0];
for c in &corners[1..] {
let o = ev_cmp(c, best);
if (want_min && o == Ordering::Less) || (!want_min && o == Ordering::Greater) {
best = c;
}
}
match best {
Ev::Fin { value, .. } => {
let open = corners.iter().all(|c| match c {
Ev::Fin { value: v, open } => v != value || *open,
_ => true,
});
Ev::Fin {
value: value.clone(),
open,
}
}
other => other.clone(),
}
}
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
);
}
}
}
}