use std::fmt;
use std::ops::Not;
#[must_use]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Endpoint {
#[default]
Lo = 0,
Hi = 1,
}
impl Not for Endpoint {
type Output = Self;
#[inline]
fn not(self) -> Self {
match self {
Endpoint::Lo => Endpoint::Hi,
Endpoint::Hi => Endpoint::Lo,
}
}
}
impl From<bool> for Endpoint {
#[inline]
fn from(b: bool) -> Self {
if b { Endpoint::Hi } else { Endpoint::Lo }
}
}
#[must_use]
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Interval {
pub lo: f64,
pub hi: f64,
}
impl Interval {
pub fn new(lo: f64, hi: f64) -> Self {
Interval { lo, hi }
}
pub fn empty() -> Self {
Interval { lo: 1.0, hi: 0.0 }
}
pub fn from_point(p: f64) -> Self {
Interval { lo: p, hi: p }
}
pub fn from_point_pair(p1: f64, p2: f64) -> Self {
if p1 <= p2 {
Interval { lo: p1, hi: p2 }
} else {
Interval { lo: p2, hi: p1 }
}
}
pub fn is_empty(self) -> bool {
self.lo > self.hi
}
pub fn center(self) -> f64 {
0.5 * (self.lo + self.hi)
}
pub fn length(self) -> f64 {
self.hi - self.lo
}
pub fn contains(self, p: f64) -> bool {
p >= self.lo && p <= self.hi
}
pub fn interior_contains(self, p: f64) -> bool {
p > self.lo && p < self.hi
}
pub fn contains_interval(self, y: Interval) -> bool {
if y.is_empty() {
return true;
}
y.lo >= self.lo && y.hi <= self.hi
}
pub fn interior_contains_interval(self, y: Interval) -> bool {
if y.is_empty() {
return true;
}
y.lo > self.lo && y.hi < self.hi
}
pub fn intersects(self, y: Interval) -> bool {
if self.lo <= y.lo {
y.lo <= self.hi && y.lo <= y.hi
} else {
self.lo <= y.hi && self.lo <= self.hi
}
}
pub fn interior_intersects(self, y: Interval) -> bool {
y.lo < self.hi && self.lo < y.hi && self.lo < self.hi && y.lo <= y.hi
}
pub fn directed_hausdorff_distance(self, y: Interval) -> f64 {
if self.is_empty() {
return 0.0;
}
if y.is_empty() {
return f64::INFINITY;
}
0.0_f64.max((self.hi - y.hi).max(y.lo - self.lo))
}
pub fn add_point(self, p: f64) -> Interval {
if self.is_empty() {
Interval::from_point(p)
} else if p < self.lo {
Interval { lo: p, hi: self.hi }
} else if p > self.hi {
Interval { lo: self.lo, hi: p }
} else {
self
}
}
pub fn add_interval(self, y: Interval) -> Interval {
if y.is_empty() {
return self;
}
if self.is_empty() {
return y;
}
Interval {
lo: self.lo.min(y.lo),
hi: self.hi.max(y.hi),
}
}
pub fn project(self, p: f64) -> f64 {
debug_assert!(!self.is_empty());
p.clamp(self.lo, self.hi)
}
pub fn expanded(self, margin: f64) -> Interval {
if self.is_empty() {
return self;
}
Interval {
lo: self.lo - margin,
hi: self.hi + margin,
}
}
pub fn union(self, y: Interval) -> Interval {
if self.is_empty() {
return y;
}
if y.is_empty() {
return self;
}
Interval {
lo: self.lo.min(y.lo),
hi: self.hi.max(y.hi),
}
}
pub fn intersection(self, y: Interval) -> Interval {
Interval {
lo: self.lo.max(y.lo),
hi: self.hi.min(y.hi),
}
}
pub fn approx_eq_with(self, y: Interval, max_error: f64) -> bool {
if self.is_empty() {
return y.length() <= 2.0 * max_error;
}
if y.is_empty() {
return self.length() <= 2.0 * max_error;
}
(y.lo - self.lo).abs() <= max_error && (y.hi - self.hi).abs() <= max_error
}
pub fn approx_eq(self, y: Interval) -> bool {
self.approx_eq_with(y, 1e-15)
}
pub fn bound(self, i: Endpoint) -> f64 {
match i {
Endpoint::Lo => self.lo,
Endpoint::Hi => self.hi,
}
}
}
impl Default for Interval {
fn default() -> Self {
Interval::empty()
}
}
impl PartialEq for Interval {
fn eq(&self, other: &Self) -> bool {
(self.lo == other.lo && self.hi == other.hi) || (self.is_empty() && other.is_empty())
}
}
impl From<(f64, f64)> for Interval {
fn from((lo, hi): (f64, f64)) -> Self {
Interval { lo, hi }
}
}
impl fmt::Display for Interval {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}, {}]", self.lo, self.hi)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn is_send_sync<T: Sized + Send + Sync + Unpin>() {}
#[test]
fn interval_is_send_sync() {
is_send_sync::<Interval>();
}
const UNIT: Interval = Interval { lo: 0.0, hi: 1.0 };
const NEGUNIT: Interval = Interval { lo: -1.0, hi: 0.0 };
const HALF: Interval = Interval { lo: 0.5, hi: 0.5 };
fn empty() -> Interval {
Interval::empty()
}
fn test_interval_ops(x: Interval, y: Interval, expected: &str) {
let expected: Vec<bool> = expected.chars().map(|c| c == 'T').collect();
assert_eq!(
x.contains_interval(y),
expected[0],
"{x}.contains_interval({y})"
);
assert_eq!(
x.interior_contains_interval(y),
expected[1],
"{x}.interior_contains_interval({y})"
);
assert_eq!(x.intersects(y), expected[2], "{x}.intersects({y})");
assert_eq!(
x.interior_intersects(y),
expected[3],
"{x}.interior_intersects({y})"
);
assert_eq!(x.contains_interval(y), x.union(y) == x);
assert_eq!(x.intersects(y), !x.intersection(y).is_empty());
assert_eq!(x.union(y), x.add_interval(y));
}
#[test]
fn test_constructors_and_accessors() {
assert_eq!(UNIT.lo, 0.0);
assert_eq!(UNIT.hi, 1.0);
assert_eq!(NEGUNIT.bound(Endpoint::Lo), -1.0);
assert_eq!(NEGUNIT.bound(Endpoint::Hi), 0.0);
let ten = Interval::new(0.0, 10.0);
assert_eq!(ten, Interval::new(0.0, 10.0));
let ten2 = Interval::new(-10.0, 10.0);
assert_eq!(ten2.bound(Endpoint::Lo), -10.0);
assert_eq!(ten2.bound(Endpoint::Hi), 10.0);
}
#[test]
fn test_is_empty() {
assert!(!UNIT.is_empty());
assert!(!HALF.is_empty());
assert!(empty().is_empty());
}
#[test]
fn test_equality() {
assert_eq!(empty(), empty());
assert_eq!(UNIT, UNIT);
assert_ne!(UNIT, empty());
assert_ne!(Interval::new(1.0, 2.0), Interval::new(1.0, 3.0));
}
#[test]
fn test_default_is_empty() {
let default_empty: Interval = Interval::default();
assert!(default_empty.is_empty());
assert_eq!(empty().lo, default_empty.lo);
assert_eq!(empty().hi, default_empty.hi);
}
#[test]
fn test_center_and_length() {
assert_eq!(UNIT.center(), 0.5);
assert_eq!(HALF.center(), 0.5);
assert_eq!(NEGUNIT.length(), 1.0);
assert_eq!(HALF.length(), 0.0);
assert!(empty().length() < 0.0);
}
#[test]
fn test_contains_point() {
assert!(UNIT.contains(0.5));
assert!(UNIT.interior_contains(0.5));
assert!(UNIT.contains(0.0));
assert!(!UNIT.interior_contains(0.0));
assert!(UNIT.contains(1.0));
assert!(!UNIT.interior_contains(1.0));
}
#[test]
fn test_interval_ops_cases() {
test_interval_ops(empty(), empty(), "TTFF");
test_interval_ops(empty(), UNIT, "FFFF");
test_interval_ops(UNIT, HALF, "TTTT");
test_interval_ops(UNIT, UNIT, "TFTT");
test_interval_ops(UNIT, empty(), "TTFF");
test_interval_ops(UNIT, NEGUNIT, "FFTF");
test_interval_ops(UNIT, Interval::new(0.0, 0.5), "TFTT");
test_interval_ops(HALF, Interval::new(0.0, 0.5), "FFTF");
}
#[test]
fn test_add_point() {
let r = empty();
let r = r.add_point(5.0);
assert_eq!(r.lo, 5.0);
assert_eq!(r.hi, 5.0);
let r = r.add_point(-1.0);
assert_eq!(r.lo, -1.0);
assert_eq!(r.hi, 5.0);
let r = r.add_point(0.0);
assert_eq!(r.lo, -1.0);
assert_eq!(r.hi, 5.0);
}
#[test]
fn test_project() {
assert_eq!(Interval::new(0.1, 0.4).project(0.3), 0.3);
assert_eq!(Interval::new(0.1, 0.4).project(-7.0), 0.1);
assert_eq!(Interval::new(0.1, 0.4).project(0.6), 0.4);
}
#[test]
fn test_from_point_pair() {
assert_eq!(Interval::from_point_pair(4.0, 4.0), Interval::new(4.0, 4.0));
assert_eq!(
Interval::from_point_pair(-1.0, -2.0),
Interval::new(-2.0, -1.0)
);
assert_eq!(
Interval::from_point_pair(-5.0, 3.0),
Interval::new(-5.0, 3.0)
);
}
#[test]
fn test_expanded() {
assert_eq!(empty().expanded(0.45), empty());
assert_eq!(UNIT.expanded(0.5), Interval::new(-0.5, 1.5));
assert_eq!(UNIT.expanded(-0.5), Interval::new(0.5, 0.5));
assert!(UNIT.expanded(-0.51).is_empty());
assert!(UNIT.expanded(-0.51).expanded(0.51).is_empty());
}
#[test]
fn test_union() {
assert_eq!(
Interval::new(99.0, 100.0).union(empty()),
Interval::new(99.0, 100.0)
);
assert_eq!(
empty().union(Interval::new(99.0, 100.0)),
Interval::new(99.0, 100.0)
);
assert!(
Interval::new(5.0, 3.0)
.union(Interval::new(0.0, -2.0))
.is_empty()
);
assert!(
Interval::new(0.0, -2.0)
.union(Interval::new(5.0, 3.0))
.is_empty()
);
assert_eq!(UNIT.union(UNIT), UNIT);
assert_eq!(UNIT.union(NEGUNIT), Interval::new(-1.0, 1.0));
assert_eq!(NEGUNIT.union(UNIT), Interval::new(-1.0, 1.0));
assert_eq!(HALF.union(UNIT), UNIT);
}
#[test]
fn test_intersection() {
assert_eq!(UNIT.intersection(HALF), HALF);
assert_eq!(UNIT.intersection(NEGUNIT), Interval::new(0.0, 0.0));
assert!(NEGUNIT.intersection(HALF).is_empty());
assert!(UNIT.intersection(empty()).is_empty());
assert!(empty().intersection(UNIT).is_empty());
}
#[test]
fn test_directed_hausdorff_distance() {
assert_eq!(empty().directed_hausdorff_distance(UNIT), 0.0);
assert_eq!(UNIT.directed_hausdorff_distance(empty()), f64::INFINITY);
assert_eq!(
Interval::new(0.0, 1.0).directed_hausdorff_distance(Interval::new(0.0, 2.0)),
0.0
);
assert_eq!(
Interval::new(0.0, 2.0).directed_hausdorff_distance(Interval::new(0.0, 1.0)),
1.0
);
}
#[test]
fn test_approx_equals() {
let lo = 4.0 * f64::EPSILON; let hi = 6.0 * f64::EPSILON;
assert!(empty().approx_eq(empty()));
assert!(Interval::new(0.0, 0.0).approx_eq(empty()));
assert!(empty().approx_eq(Interval::new(0.0, 0.0)));
assert!(Interval::new(1.0, 1.0).approx_eq(empty()));
assert!(empty().approx_eq(Interval::new(1.0, 1.0)));
assert!(!empty().approx_eq(Interval::new(0.0, 1.0)));
assert!(empty().approx_eq(Interval::new(1.0, 1.0 + 2.0 * lo)));
assert!(!empty().approx_eq(Interval::new(1.0, 1.0 + 2.0 * hi)));
assert!(Interval::new(1.0, 1.0).approx_eq(Interval::new(1.0, 1.0)));
assert!(Interval::new(1.0, 1.0).approx_eq(Interval::new(1.0 - lo, 1.0 - lo)));
assert!(Interval::new(1.0, 1.0).approx_eq(Interval::new(1.0 + lo, 1.0 + lo)));
assert!(!Interval::new(1.0, 1.0).approx_eq(Interval::new(1.0 - hi, 1.0)));
assert!(!Interval::new(1.0, 1.0).approx_eq(Interval::new(1.0, 1.0 + hi)));
assert!(Interval::new(1.0, 1.0).approx_eq(Interval::new(1.0 - lo, 1.0 + lo)));
assert!(!Interval::new(0.0, 0.0).approx_eq(Interval::new(1.0, 1.0)));
assert!(Interval::new(1.0 - lo, 2.0 + lo).approx_eq(Interval::new(1.0, 2.0)));
assert!(Interval::new(1.0 + lo, 2.0 - lo).approx_eq(Interval::new(1.0, 2.0)));
assert!(!Interval::new(1.0 - hi, 2.0 + lo).approx_eq(Interval::new(1.0, 2.0)));
assert!(!Interval::new(1.0 + hi, 2.0 - lo).approx_eq(Interval::new(1.0, 2.0)));
assert!(!Interval::new(1.0 - lo, 2.0 + hi).approx_eq(Interval::new(1.0, 2.0)));
assert!(!Interval::new(1.0 + lo, 2.0 - hi).approx_eq(Interval::new(1.0, 2.0)));
}
#[test]
fn test_display() {
assert_eq!(format!("{}", Interval::new(2.0, 4.5)), "[2, 4.5]");
}
#[test]
fn test_from_tuple() {
let i: Interval = (1.0, 2.0).into();
assert_eq!(i, Interval::new(1.0, 2.0));
}
}
#[cfg(test)]
mod quickcheck_tests {
use super::*;
use quickcheck_macros::quickcheck;
fn finite(x: f64) -> f64 {
if x.is_finite() { x } else { 0.0 }
}
fn make_interval(a: f64, b: f64) -> Interval {
let a = finite(a);
let b = finite(b);
if a <= b {
Interval::new(a, b)
} else {
Interval::new(b, a)
}
}
#[quickcheck]
fn prop_union_contains_both(a1: f64, a2: f64, b1: f64, b2: f64) -> bool {
let a = make_interval(a1, a2);
let b = make_interval(b1, b2);
let u = a.union(b);
u.contains_interval(a) && u.contains_interval(b)
}
#[quickcheck]
fn prop_intersection_subset_of_both(a1: f64, a2: f64, b1: f64, b2: f64) -> bool {
let a = make_interval(a1, a2);
let b = make_interval(b1, b2);
let i = a.intersection(b);
a.contains_interval(i) && b.contains_interval(i)
}
#[quickcheck]
fn prop_expanded_contains_original(a1: f64, a2: f64, margin: f64) -> bool {
let a = make_interval(a1, a2);
let margin = finite(margin).abs().min(1e15); a.expanded(margin).contains_interval(a)
}
#[quickcheck]
fn prop_empty_is_empty() -> bool {
let e = Interval::empty();
e.is_empty() && !e.contains(0.0) && !e.contains(1.0) && !e.contains(-1.0)
}
#[quickcheck]
fn prop_from_point_singleton(p: f64) -> bool {
let p = finite(p);
let i = Interval::from_point(p);
i.lo == p && i.hi == p && i.contains(p) && !i.is_empty()
}
#[quickcheck]
fn prop_add_point_contains(a1: f64, a2: f64, p: f64) -> bool {
let a = make_interval(a1, a2);
let p = finite(p);
a.add_point(p).contains(p)
}
#[quickcheck]
fn prop_from_point_pair_ordered(a: f64, b: f64) -> bool {
let a = finite(a);
let b = finite(b);
let i = Interval::from_point_pair(a, b);
i.lo <= i.hi && i.contains(a) && i.contains(b)
}
#[quickcheck]
fn prop_project_in_interval(a1: f64, a2: f64, p: f64) -> bool {
let a = make_interval(a1, a2);
let p = finite(p);
let proj = a.project(p);
a.contains(proj)
}
#[quickcheck]
fn prop_union_is_commutative(a1: f64, a2: f64, b1: f64, b2: f64) -> bool {
let a = make_interval(a1, a2);
let b = make_interval(b1, b2);
a.union(b) == b.union(a)
}
#[quickcheck]
fn prop_intersection_is_commutative(a1: f64, a2: f64, b1: f64, b2: f64) -> bool {
let a = make_interval(a1, a2);
let b = make_interval(b1, b2);
a.intersection(b) == b.intersection(a)
}
#[quickcheck]
fn prop_length_non_negative(a1: f64, a2: f64) -> bool {
let a = make_interval(a1, a2);
if a.is_empty() {
a.length() < 0.0
} else {
a.length() >= 0.0
}
}
#[quickcheck]
fn prop_contains_endpoints(a1: f64, a2: f64) -> bool {
let a = make_interval(a1, a2);
if a.is_empty() {
return true;
}
a.contains(a.lo) && a.contains(a.hi)
}
#[quickcheck]
fn prop_center_in_interval(a1: f64, a2: f64) -> bool {
let a1 = finite(a1).clamp(-1e150, 1e150);
let a2 = finite(a2).clamp(-1e150, 1e150);
let a = if a1 <= a2 {
Interval::new(a1, a2)
} else {
Interval::new(a2, a1)
};
if a.is_empty() {
return true;
}
a.contains(a.center())
}
#[quickcheck]
fn prop_add_interval_contains_both(a1: f64, a2: f64, b1: f64, b2: f64) -> bool {
let a = make_interval(a1, a2);
let b = make_interval(b1, b2);
let r = a.add_interval(b);
r.contains_interval(a) && r.contains_interval(b)
}
#[quickcheck]
fn prop_directed_hausdorff_non_negative(a1: f64, a2: f64, b1: f64, b2: f64) -> bool {
let a = make_interval(a1, a2);
let b = make_interval(b1, b2);
a.directed_hausdorff_distance(b) >= 0.0
}
#[cfg(feature = "serde")]
#[quickcheck]
fn prop_serde_roundtrip(a: i32, b: i32) -> bool {
let i = make_interval(f64::from(a), f64::from(b));
let json = serde_json::to_string(&i).unwrap();
let back: Interval = serde_json::from_str(&json).unwrap();
serde_json::to_string(&back).unwrap() == json
}
#[cfg(feature = "serde")]
#[test]
fn test_serde_endpoint_roundtrip() {
for ep in [Endpoint::Lo, Endpoint::Hi] {
let json = serde_json::to_string(&ep).unwrap();
let back: Endpoint = serde_json::from_str(&json).unwrap();
assert_eq!(ep, back);
}
}
}