Skip to main content

arris_math/
interval.rs

1//! Closed parameter intervals.
2
3use core::fmt;
4
5/// A closed interval `[lo, hi]` with `lo ≤ hi`, neither end NaN. Either end
6/// may be infinite: a line's domain is [`Interval::REAL`]. An interval may
7/// be longer than a curve's period — an edge range on a periodic curve may
8/// cross the period (`docs/DATA-MODEL.md` §Topology).
9///
10/// ```
11/// use arris_math::Interval;
12///
13/// let i = Interval::new(1.0, 3.0).unwrap();
14/// assert!(i.contains(3.0));
15/// assert_eq!(i.clamp(5.0), 3.0);
16/// assert_eq!(i.lerp(0.5), 2.0);
17/// assert!(i.overlaps(&Interval::new(3.0, 4.0).unwrap()));
18/// assert!(Interval::new(3.0, 1.0).is_err());
19/// ```
20#[derive(Debug, Clone, Copy, PartialEq)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22#[cfg_attr(feature = "serde", serde(try_from = "IntervalRepr"))]
23pub struct Interval {
24    lo: f64,
25    hi: f64,
26}
27
28/// The wire form of an [`Interval`]: the two ends, validated by
29/// [`Interval::new`] on the way in so a decoded interval is one that could
30/// have been built.
31#[cfg(feature = "serde")]
32#[derive(serde::Deserialize)]
33struct IntervalRepr {
34    lo: f64,
35    hi: f64,
36}
37
38#[cfg(feature = "serde")]
39impl TryFrom<IntervalRepr> for Interval {
40    type Error = IntervalError;
41
42    fn try_from(r: IntervalRepr) -> Result<Self, IntervalError> {
43        Interval::new(r.lo, r.hi)
44    }
45}
46
47/// Why two numbers are not an [`Interval`].
48#[derive(Debug, Clone, Copy, PartialEq)]
49pub enum IntervalError {
50    /// `lo > hi`.
51    Reversed {
52        /// The lower end given.
53        lo: f64,
54        /// The upper end given.
55        hi: f64,
56    },
57    /// An end is NaN.
58    NotANumber,
59}
60
61impl fmt::Display for IntervalError {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        match self {
64            IntervalError::Reversed { lo, hi } => write!(f, "interval [{lo}, {hi}] is reversed"),
65            IntervalError::NotANumber => write!(f, "interval end is NaN"),
66        }
67    }
68}
69
70impl std::error::Error for IntervalError {}
71
72impl Interval {
73    /// The whole real line, `[−∞, +∞]`: the domain of a line or a plane's
74    /// parameter.
75    pub const REAL: Interval = Interval {
76        lo: f64::NEG_INFINITY,
77        hi: f64::INFINITY,
78    };
79    /// `[0, 1]`.
80    pub const UNIT: Interval = Interval { lo: 0.0, hi: 1.0 };
81    /// `[0, 2π]`: one turn of an angular parameter, both ends included —
82    /// the closed fundamental interval of every periodic direction.
83    pub const TURN: Interval = Interval {
84        lo: 0.0,
85        hi: core::f64::consts::TAU,
86    };
87
88    /// `[lo, hi]`; an error when `lo > hi` or either is NaN.
89    pub fn new(lo: f64, hi: f64) -> Result<Self, IntervalError> {
90        if lo.is_nan() || hi.is_nan() {
91            return Err(IntervalError::NotANumber);
92        }
93        if lo > hi {
94            return Err(IntervalError::Reversed { lo, hi });
95        }
96        Ok(Interval { lo, hi })
97    }
98
99    /// The lower end.
100    pub const fn lo(&self) -> f64 {
101        self.lo
102    }
103
104    /// The upper end.
105    pub const fn hi(&self) -> f64 {
106        self.hi
107    }
108
109    /// `hi − lo`; infinite for an unbounded interval, zero for a point.
110    pub fn length(&self) -> f64 {
111        self.hi - self.lo
112    }
113
114    /// `true` when both ends are finite.
115    pub fn is_bounded(&self) -> bool {
116        self.lo.is_finite() && self.hi.is_finite()
117    }
118
119    /// `(lo + hi) / 2`; NaN for [`Interval::REAL`], where no midpoint
120    /// exists.
121    pub fn midpoint(&self) -> f64 {
122        self.lerp(0.5)
123    }
124
125    /// `lo ≤ t ≤ hi`. Both ends are inside; NaN is not.
126    pub fn contains(&self, t: f64) -> bool {
127        self.lo <= t && t <= self.hi
128    }
129
130    /// The nearest parameter inside: `t` itself when it is contained, the
131    /// nearer end otherwise. NaN stays NaN.
132    pub fn clamp(&self, t: f64) -> f64 {
133        if t < self.lo {
134            self.lo
135        } else if t > self.hi {
136            self.hi
137        } else {
138            t
139        }
140    }
141
142    /// `lo` at `s = 0`, `hi` at `s = 1`, both exactly, linear between and
143    /// beyond. Bounded intervals only; an infinite end yields an infinite
144    /// or NaN result.
145    pub fn lerp(&self, s: f64) -> f64 {
146        (1.0 - s) * self.lo + s * self.hi
147    }
148
149    /// `n` parameters evenly over the interval, both ends included; the
150    /// midpoint alone when `n` is one or zero. Bounded intervals only.
151    ///
152    /// ```
153    /// use arris_math::Interval;
154    ///
155    /// let i = Interval::new(0.0, 1.0).unwrap();
156    /// assert_eq!(i.samples(3), vec![0.0, 0.5, 1.0]);
157    /// assert_eq!(i.samples(1), vec![0.5]);
158    /// ```
159    pub fn samples(&self, n: usize) -> Vec<f64> {
160        if n <= 1 {
161            return vec![self.midpoint()];
162        }
163        (0..n)
164            .map(|i| self.lerp(i as f64 / (n - 1) as f64))
165            .collect()
166    }
167
168    /// `true` when the two closed intervals share at least one point:
169    /// touching at an end counts.
170    pub fn overlaps(&self, other: &Interval) -> bool {
171        self.lo <= other.hi && other.lo <= self.hi
172    }
173
174    /// The common part, or `None` when they do not [`overlap`](Self::overlaps).
175    pub fn intersection(&self, other: &Interval) -> Option<Interval> {
176        self.overlaps(other).then(|| Interval {
177            lo: self.lo.max(other.lo),
178            hi: self.hi.min(other.hi),
179        })
180    }
181
182    /// The smallest interval containing both.
183    pub fn hull(&self, other: &Interval) -> Interval {
184        Interval {
185            lo: self.lo.min(other.lo),
186            hi: self.hi.max(other.hi),
187        }
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    fn iv(lo: f64, hi: f64) -> Interval {
196        Interval::new(lo, hi).unwrap()
197    }
198
199    #[test]
200    fn construction_rejects_reversed_and_nan() {
201        assert_eq!(
202            Interval::new(2.0, 1.0),
203            Err(IntervalError::Reversed { lo: 2.0, hi: 1.0 })
204        );
205        assert_eq!(Interval::new(f64::NAN, 1.0), Err(IntervalError::NotANumber));
206        assert!(Interval::new(1.0, 1.0).is_ok());
207        assert!(Interval::new(f64::NEG_INFINITY, 0.0).is_ok());
208    }
209
210    #[test]
211    fn clamp_and_contains() {
212        let i = iv(-1.0, 2.0);
213        assert!(i.contains(-1.0) && i.contains(2.0) && i.contains(0.5));
214        assert!(!i.contains(-1.0000001) && !i.contains(f64::NAN));
215        assert_eq!(i.clamp(-5.0), -1.0);
216        assert_eq!(i.clamp(5.0), 2.0);
217        assert_eq!(i.clamp(0.25), 0.25);
218        assert!(i.clamp(f64::NAN).is_nan());
219        assert_eq!(Interval::REAL.clamp(1e300), 1e300);
220        assert!(Interval::REAL.contains(f64::INFINITY));
221    }
222
223    #[test]
224    fn lerp_hits_the_ends_exactly() {
225        let i = iv(0.1, 0.7);
226        assert_eq!(i.lerp(0.0), 0.1);
227        assert_eq!(i.lerp(1.0), 0.7);
228        assert!((i.midpoint() - 0.4).abs() <= f64::EPSILON);
229        assert_eq!(i.length(), 0.7 - 0.1);
230        assert!(i.is_bounded() && !Interval::REAL.is_bounded());
231        assert!(Interval::REAL.midpoint().is_nan());
232    }
233
234    #[test]
235    fn overlaps_intersection_hull() {
236        let a = iv(0.0, 1.0);
237        let b = iv(1.0, 2.0);
238        let c = iv(1.5, 3.0);
239        assert!(a.overlaps(&b) && b.overlaps(&a));
240        assert!(!a.overlaps(&c) && !c.overlaps(&a));
241        assert_eq!(a.intersection(&b), Some(iv(1.0, 1.0)));
242        assert_eq!(a.intersection(&c), None);
243        assert_eq!(b.intersection(&c), Some(iv(1.5, 2.0)));
244        assert_eq!(a.hull(&c), iv(0.0, 3.0));
245        assert_eq!(Interval::REAL.intersection(&c), Some(c));
246        assert_eq!(Interval::UNIT, a);
247        assert_eq!(Interval::TURN.hi(), core::f64::consts::TAU);
248    }
249
250    #[cfg(feature = "serde")]
251    #[test]
252    fn serde_round_trips_and_rejects_a_reversed_interval() {
253        let i = iv(-1.5, 2.25);
254        let text = serde_json::to_string(&i).unwrap();
255        assert_eq!(text, r#"{"lo":-1.5,"hi":2.25}"#);
256        let back: Interval = serde_json::from_str(&text).unwrap();
257        assert_eq!(back, i);
258        assert!(serde_json::from_str::<Interval>(r#"{"lo":2.0,"hi":1.0}"#).is_err());
259    }
260}