1use core::fmt;
4
5#[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#[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#[derive(Debug, Clone, Copy, PartialEq)]
49pub enum IntervalError {
50 Reversed {
52 lo: f64,
54 hi: f64,
56 },
57 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 pub const REAL: Interval = Interval {
76 lo: f64::NEG_INFINITY,
77 hi: f64::INFINITY,
78 };
79 pub const UNIT: Interval = Interval { lo: 0.0, hi: 1.0 };
81 pub const TURN: Interval = Interval {
84 lo: 0.0,
85 hi: core::f64::consts::TAU,
86 };
87
88 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 pub const fn lo(&self) -> f64 {
101 self.lo
102 }
103
104 pub const fn hi(&self) -> f64 {
106 self.hi
107 }
108
109 pub fn length(&self) -> f64 {
111 self.hi - self.lo
112 }
113
114 pub fn is_bounded(&self) -> bool {
116 self.lo.is_finite() && self.hi.is_finite()
117 }
118
119 pub fn midpoint(&self) -> f64 {
122 self.lerp(0.5)
123 }
124
125 pub fn contains(&self, t: f64) -> bool {
127 self.lo <= t && t <= self.hi
128 }
129
130 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 pub fn lerp(&self, s: f64) -> f64 {
146 (1.0 - s) * self.lo + s * self.hi
147 }
148
149 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 pub fn overlaps(&self, other: &Interval) -> bool {
171 self.lo <= other.hi && other.lo <= self.hi
172 }
173
174 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 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}