1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4#[doc(inline)]
7pub use bound::Bound;
8#[doc(inline)]
9pub use interval::Interval;
10
11pub mod bound {
13 #[derive(Debug, Clone, Copy, PartialEq)]
18 pub enum Bound<T> {
19 Open(T),
21 Closed(T),
23 Unbounded,
25 }
26}
27
28pub mod interval {
30 use core::cmp::Ordering;
31
32 use crate::bound::Bound;
33
34 #[derive(Debug, Clone, Copy, PartialEq)]
50 pub struct Interval<T> {
51 pub lower: Bound<T>,
53 pub upper: Bound<T>,
55 }
56
57 impl<T> Interval<T> {
58 #[must_use]
60 pub const fn new(lower: Bound<T>, upper: Bound<T>) -> Self {
61 Self { lower, upper }
62 }
63
64 #[must_use]
66 pub const fn open(lower: T, upper: T) -> Self {
67 Self::new(Bound::Open(lower), Bound::Open(upper))
68 }
69
70 #[must_use]
72 pub const fn closed(lower: T, upper: T) -> Self {
73 Self::new(Bound::Closed(lower), Bound::Closed(upper))
74 }
75
76 #[must_use]
78 pub const fn open_closed(lower: T, upper: T) -> Self {
79 Self::new(Bound::Open(lower), Bound::Closed(upper))
80 }
81
82 #[must_use]
84 pub const fn closed_open(lower: T, upper: T) -> Self {
85 Self::new(Bound::Closed(lower), Bound::Open(upper))
86 }
87
88 #[must_use]
90 pub const fn greater_than(lower: T) -> Self {
91 Self::new(Bound::Open(lower), Bound::Unbounded)
92 }
93
94 #[must_use]
96 pub const fn greater_than_or_equal(lower: T) -> Self {
97 Self::new(Bound::Closed(lower), Bound::Unbounded)
98 }
99
100 #[must_use]
102 pub const fn less_than(upper: T) -> Self {
103 Self::new(Bound::Unbounded, Bound::Open(upper))
104 }
105
106 #[must_use]
108 pub const fn less_than_or_equal(upper: T) -> Self {
109 Self::new(Bound::Unbounded, Bound::Closed(upper))
110 }
111
112 #[must_use]
114 pub const fn unbounded() -> Self {
115 Self::new(Bound::Unbounded, Bound::Unbounded)
116 }
117 }
118
119 impl<T: PartialOrd + Copy> Interval<T> {
120 #[must_use]
122 pub fn contains(&self, value: T) -> bool {
123 if self.is_empty() {
124 return false;
125 }
126
127 lower_contains(&self.lower, &value) && upper_contains(&self.upper, &value)
128 }
129
130 #[must_use]
134 pub fn is_empty(&self) -> bool {
135 match (bound_value(&self.lower), bound_value(&self.upper)) {
136 (Some(lower), Some(upper)) => match lower.partial_cmp(upper) {
137 Some(Ordering::Less) => false,
138 Some(Ordering::Greater) | None => true,
139 Some(Ordering::Equal) => {
140 !matches!(self.lower, Bound::Closed(_))
141 || !matches!(self.upper, Bound::Closed(_))
142 },
143 },
144 _ => false,
145 }
146 }
147
148 #[must_use]
150 pub fn is_bounded(&self) -> bool {
151 self.has_lower_bound() && self.has_upper_bound()
152 }
153
154 #[must_use]
156 pub fn has_lower_bound(&self) -> bool {
157 !matches!(self.lower, Bound::Unbounded)
158 }
159
160 #[must_use]
162 pub fn has_upper_bound(&self) -> bool {
163 !matches!(self.upper, Bound::Unbounded)
164 }
165
166 #[must_use]
168 pub fn overlaps(&self, other: &Self) -> bool {
169 self.intersection(other).is_some()
170 }
171
172 #[must_use]
174 pub fn intersection(&self, other: &Self) -> Option<Self> {
175 let interval = Self::new(
176 max_lower_bound(self.lower, other.lower),
177 min_upper_bound(self.upper, other.upper),
178 );
179
180 (!interval.is_empty()).then_some(interval)
181 }
182 }
183
184 impl Interval<f64> {
185 #[must_use]
190 pub fn length(&self) -> Option<f64> {
191 match (self.lower, self.upper) {
192 (
193 Bound::Open(lower) | Bound::Closed(lower),
194 Bound::Open(upper) | Bound::Closed(upper),
195 ) => {
196 if self.is_empty() {
197 Some(0.0)
198 } else {
199 Some(upper - lower)
200 }
201 },
202 _ => None,
203 }
204 }
205 }
206
207 fn bound_value<T>(bound: &Bound<T>) -> Option<&T> {
208 match bound {
209 Bound::Open(value) | Bound::Closed(value) => Some(value),
210 Bound::Unbounded => None,
211 }
212 }
213
214 fn lower_contains<T: PartialOrd>(bound: &Bound<T>, value: &T) -> bool {
215 match bound {
216 Bound::Open(lower) => value > lower,
217 Bound::Closed(lower) => value >= lower,
218 Bound::Unbounded => true,
219 }
220 }
221
222 fn upper_contains<T: PartialOrd>(bound: &Bound<T>, value: &T) -> bool {
223 match bound {
224 Bound::Open(upper) => value < upper,
225 Bound::Closed(upper) => value <= upper,
226 Bound::Unbounded => true,
227 }
228 }
229
230 fn max_lower_bound<T: PartialOrd + Copy>(left: Bound<T>, right: Bound<T>) -> Bound<T> {
231 match (&left, &right) {
232 (Bound::Unbounded, _) => right,
233 (_, Bound::Unbounded) => left,
234 (
235 Bound::Open(left_value) | Bound::Closed(left_value),
236 Bound::Open(right_value) | Bound::Closed(right_value),
237 ) => match left_value.partial_cmp(right_value) {
238 Some(Ordering::Less) => right,
239 Some(Ordering::Greater) | None => left,
240 Some(Ordering::Equal) => {
241 if matches!(left, Bound::Open(_)) || matches!(right, Bound::Open(_)) {
242 Bound::Open(*left_value)
243 } else {
244 Bound::Closed(*left_value)
245 }
246 },
247 },
248 }
249 }
250
251 fn min_upper_bound<T: PartialOrd + Copy>(left: Bound<T>, right: Bound<T>) -> Bound<T> {
252 match (&left, &right) {
253 (Bound::Unbounded, _) => right,
254 (_, Bound::Unbounded) => left,
255 (
256 Bound::Open(left_value) | Bound::Closed(left_value),
257 Bound::Open(right_value) | Bound::Closed(right_value),
258 ) => match left_value.partial_cmp(right_value) {
259 Some(Ordering::Less) => left,
260 Some(Ordering::Greater) | None => right,
261 Some(Ordering::Equal) => {
262 if matches!(left, Bound::Open(_)) || matches!(right, Bound::Open(_)) {
263 Bound::Open(*left_value)
264 } else {
265 Bound::Closed(*left_value)
266 }
267 },
268 },
269 }
270 }
271
272 #[cfg(test)]
273 mod tests {
274 use super::Interval;
275 use crate::Bound;
276
277 #[test]
278 fn open_intervals_exclude_endpoints() {
279 let interval = Interval::open(1.0, 3.0);
280
281 assert!(!interval.contains(1.0));
282 assert!(interval.contains(2.0));
283 assert!(!interval.contains(3.0));
284 }
285
286 #[test]
287 fn closed_intervals_include_endpoints() {
288 let interval = Interval::closed(1.0, 3.0);
289
290 assert!(interval.contains(1.0));
291 assert!(interval.contains(3.0));
292 assert!(!interval.contains(4.0));
293 }
294
295 #[test]
296 fn half_open_intervals_respect_each_endpoint() {
297 let left_closed = Interval::closed_open(1.0, 3.0);
298 let right_closed = Interval::open_closed(1.0, 3.0);
299
300 assert!(left_closed.contains(1.0));
301 assert!(!left_closed.contains(3.0));
302 assert!(!right_closed.contains(1.0));
303 assert!(right_closed.contains(3.0));
304 }
305
306 #[test]
307 fn unbounded_intervals_work_correctly() {
308 let above = Interval::greater_than_or_equal(2.0);
309 let below = Interval::less_than(5.0);
310 let everywhere = Interval::<f64>::unbounded();
311
312 assert!(above.contains(2.0));
313 assert!(above.contains(10.0));
314 assert!(!above.contains(1.5));
315 assert!(below.contains(-100.0));
316 assert!(!below.contains(5.0));
317 assert!(everywhere.contains(0.0));
318 assert!(everywhere.overlaps(&above));
319 assert!(above.has_lower_bound());
320 assert!(!above.has_upper_bound());
321 assert!(!everywhere.is_bounded());
322 }
323
324 #[test]
325 fn same_endpoint_rules_define_emptiness() {
326 assert!(Interval::open(2.0, 2.0).is_empty());
327 assert!(Interval::open_closed(2.0, 2.0).is_empty());
328 assert!(Interval::closed_open(2.0, 2.0).is_empty());
329 assert!(!Interval::closed(2.0, 2.0).is_empty());
330 }
331
332 #[test]
333 fn lower_greater_than_upper_is_empty() {
334 let interval = Interval::closed(4.0, 2.0);
335
336 assert!(interval.is_empty());
337 assert!(!interval.contains(3.0));
338 assert_eq!(interval.length(), Some(0.0));
339 }
340
341 #[test]
342 fn overlaps_require_a_shared_included_point() {
343 let closed = Interval::closed(1.0, 3.0);
344 let open = Interval::open(3.0, 5.0);
345 let also_closed = Interval::closed(3.0, 5.0);
346
347 assert!(!closed.overlaps(&open));
348 assert!(closed.overlaps(&also_closed));
349 }
350
351 #[test]
352 fn intersection_returns_none_when_empty() {
353 let left = Interval::closed(1.0, 3.0);
354 let right = Interval::open(3.0, 5.0);
355
356 assert_eq!(left.intersection(&right), None);
357 }
358
359 #[test]
360 fn intersection_keeps_shared_included_endpoint() {
361 let left = Interval::closed(1.0, 3.0);
362 let right = Interval::closed(3.0, 5.0);
363
364 assert_eq!(left.intersection(&right), Some(Interval::closed(3.0, 3.0)));
365 }
366
367 #[test]
368 fn intersection_of_unbounded_intervals_stays_precise() {
369 let left = Interval::greater_than_or_equal(1.0);
370 let right = Interval::less_than_or_equal(4.0);
371
372 assert_eq!(
373 left.intersection(&right),
374 Some(Interval::new(Bound::Closed(1.0), Bound::Closed(4.0)))
375 );
376 }
377
378 #[test]
379 fn length_handles_bounded_and_unbounded_intervals() {
380 assert_eq!(Interval::closed(2.0, 2.0).length(), Some(0.0));
381 assert_eq!(Interval::open(1.0, 4.5).length(), Some(3.5));
382 assert_eq!(Interval::greater_than(1.0).length(), None);
383 assert_eq!(Interval::less_than_or_equal(4.0).length(), None);
384 }
385 }
386}