Skip to main content

use_interval/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Small interval and bound primitives for `RustUse`.
5
6#[doc(inline)]
7pub use bound::Bound;
8#[doc(inline)]
9pub use interval::Interval;
10
11/// Bound primitives for interval endpoints.
12pub mod bound {
13    /// A lower or upper bound for an interval endpoint.
14    ///
15    /// `Open` excludes the endpoint, `Closed` includes it, and `Unbounded`
16    /// leaves the side unconstrained.
17    #[derive(Debug, Clone, Copy, PartialEq)]
18    pub enum Bound<T> {
19        /// An excluded endpoint.
20        Open(T),
21        /// An included endpoint.
22        Closed(T),
23        /// No endpoint on this side of the interval.
24        Unbounded,
25    }
26}
27
28/// Interval primitives and operations.
29pub mod interval {
30    use core::cmp::Ordering;
31
32    use crate::bound::Bound;
33
34    /// A mathematical interval described by a lower and upper bound.
35    ///
36    /// Intervals may be open, closed, half-open, or unbounded on either side.
37    ///
38    /// # Examples
39    ///
40    /// ```
41    /// use use_interval::{Bound, Interval};
42    ///
43    /// let closed = Interval::closed(1.0, 3.0);
44    /// let half_open = Interval::new(Bound::Closed(0.0), Bound::Open(1.0));
45    ///
46    /// assert!(closed.contains(2.0));
47    /// assert!(half_open.contains(0.5));
48    /// ```
49    #[derive(Debug, Clone, Copy, PartialEq)]
50    pub struct Interval<T> {
51        /// The lower endpoint rule.
52        pub lower: Bound<T>,
53        /// The upper endpoint rule.
54        pub upper: Bound<T>,
55    }
56
57    impl<T> Interval<T> {
58        /// Creates an interval from explicit lower and upper bounds.
59        #[must_use]
60        pub const fn new(lower: Bound<T>, upper: Bound<T>) -> Self {
61            Self { lower, upper }
62        }
63
64        /// Creates an open interval `(lower, upper)`.
65        #[must_use]
66        pub const fn open(lower: T, upper: T) -> Self {
67            Self::new(Bound::Open(lower), Bound::Open(upper))
68        }
69
70        /// Creates a closed interval `[lower, upper]`.
71        #[must_use]
72        pub const fn closed(lower: T, upper: T) -> Self {
73            Self::new(Bound::Closed(lower), Bound::Closed(upper))
74        }
75
76        /// Creates a half-open interval `(lower, upper]`.
77        #[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        /// Creates a half-open interval `[lower, upper)`.
83        #[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        /// Creates the interval `(lower, +inf)`.
89        #[must_use]
90        pub const fn greater_than(lower: T) -> Self {
91            Self::new(Bound::Open(lower), Bound::Unbounded)
92        }
93
94        /// Creates the interval `[lower, +inf)`.
95        #[must_use]
96        pub const fn greater_than_or_equal(lower: T) -> Self {
97            Self::new(Bound::Closed(lower), Bound::Unbounded)
98        }
99
100        /// Creates the interval `(-inf, upper)`.
101        #[must_use]
102        pub const fn less_than(upper: T) -> Self {
103            Self::new(Bound::Unbounded, Bound::Open(upper))
104        }
105
106        /// Creates the interval `(-inf, upper]`.
107        #[must_use]
108        pub const fn less_than_or_equal(upper: T) -> Self {
109            Self::new(Bound::Unbounded, Bound::Closed(upper))
110        }
111
112        /// Creates the interval `(-inf, +inf)`.
113        #[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        /// Returns `true` when `value` lies inside the interval.
121        #[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        /// Returns `true` when the interval contains no values.
131        ///
132        /// `(a, a)`, `(a, a]`, and `[a, a)` are empty. `[a, a]` is not empty.
133        #[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        /// Returns `true` when the interval has both lower and upper bounds.
149        #[must_use]
150        pub fn is_bounded(&self) -> bool {
151            self.has_lower_bound() && self.has_upper_bound()
152        }
153
154        /// Returns `true` when the interval has a lower bound.
155        #[must_use]
156        pub fn has_lower_bound(&self) -> bool {
157            !matches!(self.lower, Bound::Unbounded)
158        }
159
160        /// Returns `true` when the interval has an upper bound.
161        #[must_use]
162        pub fn has_upper_bound(&self) -> bool {
163            !matches!(self.upper, Bound::Unbounded)
164        }
165
166        /// Returns `true` when the interval and `other` share at least one value.
167        #[must_use]
168        pub fn overlaps(&self, other: &Self) -> bool {
169            self.intersection(other).is_some()
170        }
171
172        /// Returns the intersection with `other`, or `None` when it is empty.
173        #[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        /// Returns the bounded interval length.
186        ///
187        /// Returns `None` for unbounded intervals. Empty bounded intervals have
188        /// length `0.0`.
189        #[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}