Skip to main content

tor_basic_utils/
rangebounds.rs

1//! This module exposes helpers for working with types that implement
2//! [`RangeBounds`].
3
4use std::cmp::{self, Ord};
5use std::ops::{Bound, RangeBounds};
6
7/// An extension trait for [`RangeBounds`].
8pub trait RangeBoundsExt<T>: RangeBounds<T> {
9    /// Compute the intersection of two `RangeBound`s.
10    ///
11    /// In essence, this computes the intersection of the intervals described by bounds of the
12    /// two objects.
13    ///
14    /// Returns `None` if the intersection of the two ranges is the empty set.
15    fn intersect<'a, U: RangeBounds<T>>(
16        &'a self,
17        other: &'a U,
18    ) -> Option<(Bound<&'a T>, Bound<&'a T>)>;
19}
20
21impl<T, R> RangeBoundsExt<T> for R
22where
23    R: RangeBounds<T>,
24    T: Ord,
25{
26    fn intersect<'a, U: RangeBounds<T>>(
27        &'a self,
28        other: &'a U,
29    ) -> Option<(Bound<&'a T>, Bound<&'a T>)> {
30        use Bound::*;
31
32        let this_start = self.start_bound();
33        let other_start = other.start_bound();
34        let this_end = self.end_bound();
35        let other_end = other.end_bound();
36
37        let start = bounds_max(this_start, other_start);
38        let end = bounds_min(this_end, other_end);
39
40        match (start, end) {
41            (Excluded(start), Excluded(end)) | (Included(start), Excluded(end)) if start == end => {
42                // The interval (n, n) = [n, n) = {} (empty set).
43                None
44            }
45            (Included(start), Included(end))
46            | (Included(start), Excluded(end))
47            | (Excluded(start), Included(end))
48            | (Excluded(start), Excluded(end))
49                if start > end =>
50            {
51                // For any a > b, the intervals [a, b], [a, b), (a, b], (a, b) are empty.
52                None
53            }
54            _ => Some((start, end)),
55        }
56    }
57}
58
59/// Return the largest of `b1` and `b2`.
60///
61/// If one of the bounds is [Unbounded](Bound::Unbounded), the other will be returned.
62fn bounds_max<'a, T: Ord>(b1: Bound<&'a T>, b2: Bound<&'a T>) -> Bound<&'a T> {
63    use Bound::*;
64
65    match (b1, b2) {
66        (Included(b1), Included(b2)) => Included(cmp::max(b1, b2)),
67        (Excluded(b1), Excluded(b2)) => Excluded(cmp::max(b1, b2)),
68
69        (Excluded(b1), Included(b2)) if b1 >= b2 => Excluded(b1),
70        (Excluded(_), Included(b2)) => Included(b2),
71
72        (Included(b1), Excluded(b2)) if b2 >= b1 => Excluded(b2),
73        (Included(b1), Excluded(_)) => Included(b1),
74
75        (b, Unbounded) | (Unbounded, b) => b,
76    }
77}
78
79/// Return the smallest of `b1` and `b2`.
80///
81/// If one of the bounds is [Unbounded](Bound::Unbounded), the other will be returned.
82fn bounds_min<'a, T: Ord>(b1: Bound<&'a T>, b2: Bound<&'a T>) -> Bound<&'a T> {
83    use Bound::*;
84
85    match (b1, b2) {
86        (Included(b1), Included(b2)) => Included(cmp::min(b1, b2)),
87        (Excluded(b1), Excluded(b2)) => Excluded(cmp::min(b1, b2)),
88
89        (Excluded(b1), Included(b2)) if b1 <= b2 => Excluded(b1),
90        (Excluded(_), Included(b2)) => Included(b2),
91
92        (Included(b1), Excluded(b2)) if b2 <= b1 => Excluded(b2),
93        (Included(b1), Excluded(_)) => Included(b1),
94
95        (b, Unbounded) | (Unbounded, b) => b,
96    }
97}
98
99#[cfg(test)]
100mod test {
101    // @@ begin test lint list maintained by maint/add_warning @@
102    #![allow(clippy::bool_assert_comparison)]
103    #![allow(clippy::clone_on_copy)]
104    #![allow(clippy::dbg_macro)]
105    #![allow(clippy::mixed_attributes_style)]
106    #![allow(clippy::print_stderr)]
107    #![allow(clippy::print_stdout)]
108    #![allow(clippy::single_char_pattern)]
109    #![allow(clippy::unwrap_used)]
110    #![allow(clippy::unchecked_time_subtraction)]
111    #![allow(clippy::useless_vec)]
112    #![allow(clippy::needless_pass_by_value)]
113    #![allow(clippy::string_slice)] // See arti#2571
114    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
115    use super::*;
116    use Bound::{Excluded as Excl, Included as Incl, Unbounded};
117    use std::fmt::Debug;
118    use web_time_compat::{Duration, SystemTime, SystemTimeExt};
119
120    /// A helper that computes the intersection of `range1` and `range2`.
121    ///
122    /// This function also asserts that the intersection operation is commutative.
123    fn intersect<'a, T, R: RangeBounds<T>>(
124        range1: &'a R,
125        range2: &'a R,
126    ) -> Option<(Bound<&'a T>, Bound<&'a T>)>
127    where
128        T: PartialEq + Ord + Debug,
129    {
130        let intersection1 = range1.intersect(range2);
131        let intersection2 = range2.intersect(range1);
132
133        assert_eq!(intersection1, intersection2);
134
135        intersection1
136    }
137
138    /// A helper for randomly generating either an inclusive or an exclusive bound with a
139    /// particular value.
140    fn random_bound<T>(value: T) -> Bound<T> {
141        if rand::random() {
142            Bound::Included(value)
143        } else {
144            Bound::Excluded(value)
145        }
146    }
147
148    #[test]
149    fn no_overlap() {
150        #[allow(clippy::type_complexity)]
151        const NON_OVERLAPPING_RANGES: &[(
152            (Bound<usize>, Bound<usize>),
153            (Bound<usize>, Bound<usize>),
154        )] = &[
155            // (1, 2) and (3, 4)
156            ((Excl(1), Excl(2)), (Excl(3), Excl(4))),
157            // (1, 2) and (2, 3)
158            ((Excl(1), Excl(2)), (Excl(2), Excl(3))),
159            // (1, 2) and [2, 3)
160            ((Excl(1), Excl(2)), (Incl(2), Excl(3))),
161            // (1, 2) and [2, 3]
162            ((Excl(1), Excl(2)), (Incl(3), Incl(4))),
163            // (-inf, 2) and [2, 3]
164            ((Unbounded, Excl(2)), (Incl(2), Incl(3))),
165            // (-inf, 2) and (2, inf)
166            ((Unbounded, Excl(2)), (Excl(2), Unbounded)),
167            // (-inf, 2) and [2, inf)
168            ((Unbounded, Excl(2)), (Incl(2), Unbounded)),
169        ];
170
171        for (range1, range2) in NON_OVERLAPPING_RANGES {
172            let intersection = intersect(range1, range2);
173            assert!(
174                intersection.is_none(),
175                "{:?} and {:?} => {:?}",
176                range1,
177                range2,
178                intersection
179            );
180        }
181    }
182
183    #[test]
184    fn intersect_unbounded_start() {
185        // (-inf, 3)
186        let range1 = (Unbounded, Excl(3));
187        // [2, 5]
188        let range2 = (Incl(2), Incl(5));
189
190        let intersection = intersect(&range1, &range2).unwrap();
191
192        // intersection = [2 3]
193        assert_eq!(intersection.start_bound(), Bound::Included(&2));
194        assert_eq!(intersection.end_bound(), Bound::Excluded(&3));
195    }
196
197    #[test]
198    fn intersect_unbounded_end() {
199        // (8, inf)
200        let range1 = (Excl(8), Unbounded);
201        // [8, 20]
202        let range2 = (Incl(8), Incl(20));
203
204        let intersection = intersect(&range1, &range2).unwrap();
205
206        // intersection = (8, 20]
207        assert_eq!(intersection.start_bound(), Bound::Excluded(&8));
208        assert_eq!(intersection.end_bound(), Bound::Included(&20));
209    }
210
211    #[test]
212    fn intersect_unbounded_range() {
213        #[allow(clippy::type_complexity)]
214        const RANGES: &[(Bound<usize>, Bound<usize>)] = &[
215            // (1, 2)
216            (Excl(1), Excl(2)),
217            // (1, 2]
218            (Excl(1), Incl(2)),
219            // [1, 2]
220            (Incl(1), Incl(2)),
221            // [1, 2)
222            (Incl(1), Excl(2)),
223            // (1, inf)
224            (Excl(1), Unbounded),
225            // [1, inf)
226            (Incl(1), Unbounded),
227            // (-inf, 2)
228            (Unbounded, Excl(2)),
229            // (-inf, 2]
230            (Unbounded, Incl(2)),
231        ];
232
233        // The intersection of any interval I with (Unbounded, Unbounded) will be I.
234        let range1 = (Unbounded, Unbounded);
235
236        for range2 in RANGES {
237            let range2 = (range2.0.as_ref(), range2.1.as_ref());
238            assert_eq!(intersect(&range1, &range2).unwrap(), range2);
239        }
240    }
241
242    #[test]
243    fn intersect_time_bounds() {
244        const MIN: Duration = Duration::from_secs(60);
245
246        // time (relative to now):  0   1   2   3
247        //                          |   |   |   |
248        // [t1, t2]:                [.......]
249        // [t3, t4]:                    [.......]
250        // intersection:                [...]
251        let now = SystemTime::get();
252        let t1 = now;
253        let t2 = now + 2 * MIN;
254
255        let t3 = now + 1 * MIN;
256        let t4 = now + 3 * MIN;
257
258        let b1 = (Bound::Included(t1), Bound::Included(t2));
259        let b2 = (Bound::Included(t3), Bound::Included(t4));
260        let expected = (Bound::Included(&t3), Bound::Included(&t2));
261        assert_eq!(intersect(&b1, &b2).unwrap(), expected);
262
263        //  t1  -  -  t2  -  -
264        //                   t3  -  -  t4
265        //
266        // time (relative to now):  0   1   2   3   4   5   6   7
267        //                          |   |   |   |   |   |   |   |
268        // [t1, t2]:                [.......]
269        // [t3, t4]:                                [............]
270        let t3 = now + 4 * MIN;
271        let t4 = now + 7 * MIN;
272        let b2 = (Bound::Included(t3), Bound::Included(t4));
273        assert!(intersect(&b1, &b2).is_none());
274    }
275
276    #[test]
277    fn combinatorial() {
278        for i in 0..10 {
279            for j in 0..10 {
280                for k in 0..10 {
281                    for l in 0..10 {
282                        let range1 = (random_bound(i), random_bound(j));
283                        let range2 = (random_bound(k), random_bound(l));
284
285                        let intersection = intersect(&range1, &range2);
286
287                        for witness in 0..10 {
288                            let c1 = range1.contains(&witness);
289                            let c2 = range2.contains(&witness);
290                            let both_contain_witness = c1 && c2;
291
292                            if both_contain_witness {
293                                // If both ranges contain `witness` they definitely intersect.
294                                assert!(intersection.unwrap().contains(&witness));
295                            } else if let Some(intersection) = intersection {
296                                // If one of them doesn't contain `witness`, `witness` is
297                                // definitely not part of the intersection.
298                                assert!(!intersection.contains(&witness));
299                            }
300                        }
301                    }
302                }
303            }
304        }
305    }
306}