Skip to content

Commit d1ab0f7

Browse files
authored
Implement [Partial]Ord for Range (#31)
In uv, we want to have a stable ordering of certain types that `Range<Version>`, both for determinism internally and for deterministically ordered output files. This PR adds `impl<V: PartialOrd> PartialOrd for Range<V>` and `impl<V: Ord> Ord for Range<V>` implementations to `Range`. We use a simple ordering scheme where we zip the segments and compare all bounds in order. If all bounds are equal, the longer range is considered greater. (And if all zipped bounds are equal and we have the same number of segments, the ranges are equal). Not that this is distinct from contains operations, `r1 < r2` (implemented by `Ord`) and `r1 ⊂ r2` (`subset_of`) have no relationship.
1 parent 2fac393 commit d1ab0f7

File tree

1 file changed

+120
-0
lines changed

1 file changed

+120
-0
lines changed

src/range.rs

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,104 @@ impl<V: Ord> Range<V> {
308308
}
309309
}
310310

311+
/// Implementing `PartialOrd` for start `Bound` of an interval.
312+
///
313+
/// Legend: `∞` is unbounded, `[1,2]` is `>1,<2`, `]1,2[` is `>=1,<=2`.
314+
///
315+
/// ```text
316+
/// left: ∞-------]
317+
/// right: [-----]
318+
/// left is smaller, since it starts earlier.
319+
///
320+
/// left: [-----]
321+
/// right: ]-----]
322+
/// left is smaller, since it starts earlier.
323+
/// ```
324+
fn cmp_bounds_start<V: PartialOrd>(left: Bound<&V>, right: Bound<&V>) -> Option<Ordering> {
325+
match (left, right) {
326+
(Unbounded, Unbounded) => Some(Ordering::Equal),
327+
(Included(_left), Unbounded) => Some(Ordering::Greater),
328+
(Excluded(_left), Unbounded) => Some(Ordering::Greater),
329+
(Unbounded, Included(_right)) => Some(Ordering::Less),
330+
(Included(left), Included(right)) => left.partial_cmp(right),
331+
(Excluded(left), Included(right)) => match left.partial_cmp(right)? {
332+
Ordering::Less => Some(Ordering::Less),
333+
Ordering::Equal => Some(Ordering::Greater),
334+
Ordering::Greater => Some(Ordering::Greater),
335+
},
336+
(Unbounded, Excluded(_right)) => Some(Ordering::Less),
337+
(Included(left), Excluded(right)) => match left.partial_cmp(right)? {
338+
Ordering::Less => Some(Ordering::Less),
339+
Ordering::Equal => Some(Ordering::Less),
340+
Ordering::Greater => Some(Ordering::Greater),
341+
},
342+
(Excluded(left), Excluded(right)) => left.partial_cmp(right),
343+
}
344+
}
345+
346+
/// Implementing `PartialOrd` for end `Bound` of an interval.
347+
///
348+
/// We flip the unbounded ranges from `-∞` to `∞`, while `V`-valued bounds checks remain the same.
349+
///
350+
/// Legend: `∞` is unbounded, `[1,2]` is `>1,<2`, `]1,2[` is `>=1,<=2`.
351+
///
352+
/// ```text
353+
/// left: [--------∞
354+
/// right: [-----]
355+
/// left is greater, since it starts earlier.
356+
///
357+
/// left: [-----[
358+
/// right: [-----]
359+
/// left is smaller, since it ends earlier.
360+
/// ```
361+
fn cmp_bounds_end<V: PartialOrd>(left: Bound<&V>, right: Bound<&V>) -> Option<Ordering> {
362+
match (left, right) {
363+
(Unbounded, Unbounded) => Some(Ordering::Equal),
364+
(Included(_left), Unbounded) => Some(Ordering::Less),
365+
(Excluded(_left), Unbounded) => Some(Ordering::Less),
366+
(Unbounded, Included(_right)) => Some(Ordering::Greater),
367+
(Included(left), Included(right)) => left.partial_cmp(right),
368+
(Excluded(left), Included(right)) => match left.partial_cmp(right)? {
369+
Ordering::Less => Some(Ordering::Less),
370+
Ordering::Equal => Some(Ordering::Less),
371+
Ordering::Greater => Some(Ordering::Greater),
372+
},
373+
(Unbounded, Excluded(_right)) => Some(Ordering::Greater),
374+
(Included(left), Excluded(right)) => match left.partial_cmp(right)? {
375+
Ordering::Less => Some(Ordering::Less),
376+
Ordering::Equal => Some(Ordering::Greater),
377+
Ordering::Greater => Some(Ordering::Greater),
378+
},
379+
(Excluded(left), Excluded(right)) => left.partial_cmp(right),
380+
}
381+
}
382+
383+
impl<V: PartialOrd> PartialOrd for Range<V> {
384+
/// A simple ordering scheme where we zip the segments and compare all bounds in order. If all
385+
/// bounds are equal, the longer range is considered greater. (And if all zipped bounds are
386+
/// equal and we have the same number of segments, the ranges are equal).
387+
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
388+
for (left, right) in self.segments.iter().zip(other.segments.iter()) {
389+
let start_cmp = cmp_bounds_start(left.start_bound(), right.start_bound())?;
390+
if start_cmp != Ordering::Equal {
391+
return Some(start_cmp);
392+
}
393+
let end_cmp = cmp_bounds_end(left.end_bound(), right.end_bound())?;
394+
if end_cmp != Ordering::Equal {
395+
return Some(end_cmp);
396+
}
397+
}
398+
Some(self.segments.len().cmp(&other.segments.len()))
399+
}
400+
}
401+
402+
impl<V: Ord> Ord for Range<V> {
403+
fn cmp(&self, other: &Self) -> Ordering {
404+
self.partial_cmp(other)
405+
.expect("PartialOrd must be `Some(Ordering)` for types that implement `Ord`")
406+
}
407+
}
408+
311409
/// The ordering of the version wrt to the interval.
312410
/// ```text
313411
/// |-------|
@@ -1069,4 +1167,26 @@ pub mod tests {
10691167
range.simplify(versions.into_iter())
10701168
);
10711169
}
1170+
1171+
#[test]
1172+
fn version_ord() {
1173+
let versions: &[Range<u32>] = &[
1174+
Range::strictly_lower_than(1u32),
1175+
Range::lower_than(1u32),
1176+
Range::singleton(1u32),
1177+
Range::between(1u32, 3u32),
1178+
Range::higher_than(1u32),
1179+
Range::strictly_higher_than(1u32),
1180+
Range::singleton(2u32),
1181+
Range::singleton(2u32).union(&Range::singleton(3u32)),
1182+
Range::singleton(2u32)
1183+
.union(&Range::singleton(3u32))
1184+
.union(&Range::singleton(4u32)),
1185+
Range::singleton(2u32).union(&Range::singleton(4u32)),
1186+
Range::singleton(3u32),
1187+
];
1188+
let mut versions_sorted = versions.to_vec();
1189+
versions_sorted.sort();
1190+
assert_eq!(versions_sorted, versions);
1191+
}
10721192
}

0 commit comments

Comments
 (0)