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