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_duration_subtraction)]
111    #![allow(clippy::useless_vec)]
112    #![allow(clippy::needless_pass_by_value)]
113    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
114    use super::*;
115    use std::fmt::Debug;
116    use std::time::{Duration, SystemTime};
117    use Bound::{Excluded as Excl, Included as Incl, Unbounded};
118
119    /// A helper that computes the intersection of `range1` and `range2`.
120    ///
121    /// This function also asserts that the intersection operation is commutative.
122    fn intersect<'a, T, R: RangeBounds<T>>(
123        range1: &'a R,
124        range2: &'a R,
125    ) -> Option<(Bound<&'a T>, Bound<&'a T>)>
126    where
127        T: PartialEq + Ord + Debug,
128    {
129        let intersection1 = range1.intersect(range2);
130        let intersection2 = range2.intersect(range1);
131
132        assert_eq!(intersection1, intersection2);
133
134        intersection1
135    }
136
137    /// A helper for randomly generating either an inclusive or an exclusive bound with a
138    /// particular value.
139    fn random_bound<T>(value: T) -> Bound<T> {
140        if rand::random() {
141            Bound::Included(value)
142        } else {
143            Bound::Excluded(value)
144        }
145    }
146
147    #[test]
148    fn no_overlap() {
149        #[allow(clippy::type_complexity)]
150        const NON_OVERLAPPING_RANGES: &[(
151            (Bound<usize>, Bound<usize>),
152            (Bound<usize>, Bound<usize>),
153        )] = &[
154            // (1, 2) and (3, 4)
155            ((Excl(1), Excl(2)), (Excl(3), Excl(4))),
156            // (1, 2) and (2, 3)
157            ((Excl(1), Excl(2)), (Excl(2), Excl(3))),
158            // (1, 2) and [2, 3)
159            ((Excl(1), Excl(2)), (Incl(2), Excl(3))),
160            // (1, 2) and [2, 3]
161            ((Excl(1), Excl(2)), (Incl(3), Incl(4))),
162            // (-inf, 2) and [2, 3]
163            ((Unbounded, Excl(2)), (Incl(2), Incl(3))),
164            // (-inf, 2) and (2, inf)
165            ((Unbounded, Excl(2)), (Excl(2), Unbounded)),
166            // (-inf, 2) and [2, inf)
167            ((Unbounded, Excl(2)), (Incl(2), Unbounded)),
168        ];
169
170        for (range1, range2) in NON_OVERLAPPING_RANGES {
171            let intersection = intersect(range1, range2);
172            assert!(
173                intersection.is_none(),
174                "{:?} and {:?} => {:?}",
175                range1,
176                range2,
177                intersection
178            );
179        }
180    }
181
182    #[test]
183    fn intersect_unbounded_start() {
184        // (-inf, 3)
185        let range1 = (Unbounded, Excl(3));
186        // [2, 5]
187        let range2 = (Incl(2), Incl(5));
188
189        let intersection = intersect(&range1, &range2).unwrap();
190
191        // intersection = [2 3]
192        assert_eq!(intersection.start_bound(), Bound::Included(&2));
193        assert_eq!(intersection.end_bound(), Bound::Excluded(&3));
194    }
195
196    #[test]
197    fn intersect_unbounded_end() {
198        // (8, inf)
199        let range1 = (Excl(8), Unbounded);
200        // [8, 20]
201        let range2 = (Incl(8), Incl(20));
202
203        let intersection = intersect(&range1, &range2).unwrap();
204
205        // intersection = (8, 20]
206        assert_eq!(intersection.start_bound(), Bound::Excluded(&8));
207        assert_eq!(intersection.end_bound(), Bound::Included(&20));
208    }
209
210    #[test]
211    fn intersect_unbounded_range() {
212        #[allow(clippy::type_complexity)]
213        const RANGES: &[(Bound<usize>, Bound<usize>)] = &[
214            // (1, 2)
215            (Excl(1), Excl(2)),
216            // (1, 2]
217            (Excl(1), Incl(2)),
218            // [1, 2]
219            (Incl(1), Incl(2)),
220            // [1, 2)
221            (Incl(1), Excl(2)),
222            // (1, inf)
223            (Excl(1), Unbounded),
224            // [1, inf)
225            (Incl(1), Unbounded),
226            // (-inf, 2)
227            (Unbounded, Excl(2)),
228            // (-inf, 2]
229            (Unbounded, Incl(2)),
230        ];
231
232        // The intersection of any interval I with (Unbounded, Unbounded) will be I.
233        let range1 = (Unbounded, Unbounded);
234
235        for range2 in RANGES {
236            let range2 = (range2.0.as_ref(), range2.1.as_ref());
237            assert_eq!(intersect(&range1, &range2).unwrap(), range2);
238        }
239    }
240
241    #[test]
242    fn intersect_time_bounds() {
243        const MIN: Duration = Duration::from_secs(60);
244
245        // time (relative to now):  0   1   2   3
246        //                          |   |   |   |
247        // [t1, t2]:                [.......]
248        // [t3, t4]:                    [.......]
249        // intersection:                [...]
250        let now = SystemTime::now();
251        let t1 = now;
252        let t2 = now + 2 * MIN;
253
254        let t3 = now + 1 * MIN;
255        let t4 = now + 3 * MIN;
256
257        let b1 = (Bound::Included(t1), Bound::Included(t2));
258        let b2 = (Bound::Included(t3), Bound::Included(t4));
259        let expected = (Bound::Included(&t3), Bound::Included(&t2));
260        assert_eq!(intersect(&b1, &b2).unwrap(), expected);
261
262        //  t1  -  -  t2  -  -
263        //                   t3  -  -  t4
264        //
265        // time (relative to now):  0   1   2   3   4   5   6   7
266        //                          |   |   |   |   |   |   |   |
267        // [t1, t2]:                [.......]
268        // [t3, t4]:                                [............]
269        let t3 = now + 4 * MIN;
270        let t4 = now + 7 * MIN;
271        let b2 = (Bound::Included(t3), Bound::Included(t4));
272        assert!(intersect(&b1, &b2).is_none());
273    }
274
275    #[test]
276    fn combinatorial() {
277        for i in 0..10 {
278            for j in 0..10 {
279                for k in 0..10 {
280                    for l in 0..10 {
281                        let range1 = (random_bound(i), random_bound(j));
282                        let range2 = (random_bound(k), random_bound(l));
283
284                        let intersection = intersect(&range1, &range2);
285
286                        for witness in 0..10 {
287                            let c1 = range1.contains(&witness);
288                            let c2 = range2.contains(&witness);
289                            let both_contain_witness = c1 && c2;
290
291                            if both_contain_witness {
292                                // If both ranges contain `witness` they definitely intersect.
293                                assert!(intersection.unwrap().contains(&witness));
294                            } else if let Some(intersection) = intersection {
295                                // If one of them doesn't contain `witness`, `witness` is
296                                // definitely not part of the intersection.
297                                assert!(!intersection.contains(&witness));
298                            }
299                        }
300                    }
301                }
302            }
303        }
304    }
305}