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 overlaps(&self, other: &Interval) -> bool {
152 self.lo <= other.hi && other.lo <= self.hi
153 }
154
155 pub fn intersection(&self, other: &Interval) -> Option<Interval> {
157 self.overlaps(other).then(|| Interval {
158 lo: self.lo.max(other.lo),
159 hi: self.hi.min(other.hi),
160 })
161 }
162
163 pub fn hull(&self, other: &Interval) -> Interval {
165 Interval {
166 lo: self.lo.min(other.lo),
167 hi: self.hi.max(other.hi),
168 }
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 fn iv(lo: f64, hi: f64) -> Interval {
177 Interval::new(lo, hi).unwrap()
178 }
179
180 #[test]
181 fn construction_rejects_reversed_and_nan() {
182 assert_eq!(
183 Interval::new(2.0, 1.0),
184 Err(IntervalError::Reversed { lo: 2.0, hi: 1.0 })
185 );
186 assert_eq!(Interval::new(f64::NAN, 1.0), Err(IntervalError::NotANumber));
187 assert!(Interval::new(1.0, 1.0).is_ok());
188 assert!(Interval::new(f64::NEG_INFINITY, 0.0).is_ok());
189 }
190
191 #[test]
192 fn clamp_and_contains() {
193 let i = iv(-1.0, 2.0);
194 assert!(i.contains(-1.0) && i.contains(2.0) && i.contains(0.5));
195 assert!(!i.contains(-1.0000001) && !i.contains(f64::NAN));
196 assert_eq!(i.clamp(-5.0), -1.0);
197 assert_eq!(i.clamp(5.0), 2.0);
198 assert_eq!(i.clamp(0.25), 0.25);
199 assert!(i.clamp(f64::NAN).is_nan());
200 assert_eq!(Interval::REAL.clamp(1e300), 1e300);
201 assert!(Interval::REAL.contains(f64::INFINITY));
202 }
203
204 #[test]
205 fn lerp_hits_the_ends_exactly() {
206 let i = iv(0.1, 0.7);
207 assert_eq!(i.lerp(0.0), 0.1);
208 assert_eq!(i.lerp(1.0), 0.7);
209 assert!((i.midpoint() - 0.4).abs() <= f64::EPSILON);
210 assert_eq!(i.length(), 0.7 - 0.1);
211 assert!(i.is_bounded() && !Interval::REAL.is_bounded());
212 assert!(Interval::REAL.midpoint().is_nan());
213 }
214
215 #[test]
216 fn overlaps_intersection_hull() {
217 let a = iv(0.0, 1.0);
218 let b = iv(1.0, 2.0);
219 let c = iv(1.5, 3.0);
220 assert!(a.overlaps(&b) && b.overlaps(&a));
221 assert!(!a.overlaps(&c) && !c.overlaps(&a));
222 assert_eq!(a.intersection(&b), Some(iv(1.0, 1.0)));
223 assert_eq!(a.intersection(&c), None);
224 assert_eq!(b.intersection(&c), Some(iv(1.5, 2.0)));
225 assert_eq!(a.hull(&c), iv(0.0, 3.0));
226 assert_eq!(Interval::REAL.intersection(&c), Some(c));
227 assert_eq!(Interval::UNIT, a);
228 assert_eq!(Interval::TURN.hi(), core::f64::consts::TAU);
229 }
230
231 #[cfg(feature = "serde")]
232 #[test]
233 fn serde_round_trips_and_rejects_a_reversed_interval() {
234 let i = iv(-1.5, 2.25);
235 let text = serde_json::to_string(&i).unwrap();
236 assert_eq!(text, r#"{"lo":-1.5,"hi":2.25}"#);
237 let back: Interval = serde_json::from_str(&text).unwrap();
238 assert_eq!(back, i);
239 assert!(serde_json::from_str::<Interval>(r#"{"lo":2.0,"hi":1.0}"#).is_err());
240 }
241}