Skip to content

Commit eb528c3

Browse files
authored
Implement [Partial]Ord for Range (#250)
* 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. * Better docs * Add reverse sorting check
1 parent 68fd200 commit eb528c3

File tree

1 file changed

+177
-0
lines changed

1 file changed

+177
-0
lines changed

src/range.rs

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,154 @@ 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+
Some(match (left, right) {
326+
// left: ∞-----
327+
// right: ∞-----
328+
(Unbounded, Unbounded) => Ordering::Equal,
329+
// left: [---
330+
// right: ∞-----
331+
(Included(_left), Unbounded) => Ordering::Greater,
332+
// left: ]---
333+
// right: ∞-----
334+
(Excluded(_left), Unbounded) => Ordering::Greater,
335+
// left: ∞-----
336+
// right: [---
337+
(Unbounded, Included(_right)) => Ordering::Less,
338+
// left: [----- OR [----- OR [-----
339+
// right: [--- OR [----- OR [---
340+
(Included(left), Included(right)) => left.partial_cmp(right)?,
341+
(Excluded(left), Included(right)) => match left.partial_cmp(right)? {
342+
// left: ]-----
343+
// right: [---
344+
Ordering::Less => Ordering::Less,
345+
// left: ]-----
346+
// right: [---
347+
Ordering::Equal => Ordering::Greater,
348+
// left: ]---
349+
// right: [-----
350+
Ordering::Greater => Ordering::Greater,
351+
},
352+
// left: ∞-----
353+
// right: ]---
354+
(Unbounded, Excluded(_right)) => Ordering::Less,
355+
(Included(left), Excluded(right)) => match left.partial_cmp(right)? {
356+
// left: [-----
357+
// right: ]---
358+
Ordering::Less => Ordering::Less,
359+
// left: [-----
360+
// right: ]---
361+
Ordering::Equal => Ordering::Less,
362+
// left: [---
363+
// right: ]-----
364+
Ordering::Greater => Ordering::Greater,
365+
},
366+
// left: ]----- OR ]----- OR ]---
367+
// right: ]--- OR ]----- OR ]-----
368+
(Excluded(left), Excluded(right)) => left.partial_cmp(right)?,
369+
})
370+
}
371+
372+
/// Implementing `PartialOrd` for end `Bound` of an interval.
373+
///
374+
/// We flip the unbounded ranges from `-∞` to `∞`, while `V`-valued bounds checks remain the same.
375+
///
376+
/// Legend: `∞` is unbounded, `[1,2]` is `>=1,<=2`, `]1,2[` is `>1,<2`.
377+
///
378+
/// ```text
379+
/// left: [--------∞
380+
/// right: [-----]
381+
/// left is greater, since it starts earlier.
382+
///
383+
/// left: [-----[
384+
/// right: [-----]
385+
/// left is smaller, since it ends earlier.
386+
/// ```
387+
fn cmp_bounds_end<V: PartialOrd>(left: Bound<&V>, right: Bound<&V>) -> Option<Ordering> {
388+
Some(match (left, right) {
389+
// left: -----∞
390+
// right: -----∞
391+
(Unbounded, Unbounded) => Ordering::Equal,
392+
// left: ---]
393+
// right: -----∞
394+
(Included(_left), Unbounded) => Ordering::Less,
395+
// left: ---[
396+
// right: -----∞
397+
(Excluded(_left), Unbounded) => Ordering::Less,
398+
// left: -----∞
399+
// right: ---]
400+
(Unbounded, Included(_right)) => Ordering::Greater,
401+
// left: -----] OR -----] OR ---]
402+
// right: ---] OR -----] OR -----]
403+
(Included(left), Included(right)) => left.partial_cmp(right)?,
404+
(Excluded(left), Included(right)) => match left.partial_cmp(right)? {
405+
// left: ---[
406+
// right: -----]
407+
Ordering::Less => Ordering::Less,
408+
// left: -----[
409+
// right: -----]
410+
Ordering::Equal => Ordering::Less,
411+
// left: -----[
412+
// right: ---]
413+
Ordering::Greater => Ordering::Greater,
414+
},
415+
(Unbounded, Excluded(_right)) => Ordering::Greater,
416+
(Included(left), Excluded(right)) => match left.partial_cmp(right)? {
417+
// left: ---]
418+
// right: -----[
419+
Ordering::Less => Ordering::Less,
420+
// left: -----]
421+
// right: -----[
422+
Ordering::Equal => Ordering::Greater,
423+
// left: -----]
424+
// right: ---[
425+
Ordering::Greater => Ordering::Greater,
426+
},
427+
// left: -----[ OR -----[ OR ---[
428+
// right: ---[ OR -----[ OR -----[
429+
(Excluded(left), Excluded(right)) => left.partial_cmp(right)?,
430+
})
431+
}
432+
433+
impl<V: PartialOrd> PartialOrd for Range<V> {
434+
/// A simple ordering scheme where we zip the segments and compare all bounds in order. If all
435+
/// bounds are equal, the longer range is considered greater. (And if all zipped bounds are
436+
/// equal and we have the same number of segments, the ranges are equal).
437+
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
438+
for (left, right) in self.segments.iter().zip(other.segments.iter()) {
439+
let start_cmp = cmp_bounds_start(left.start_bound(), right.start_bound())?;
440+
if start_cmp != Ordering::Equal {
441+
return Some(start_cmp);
442+
}
443+
let end_cmp = cmp_bounds_end(left.end_bound(), right.end_bound())?;
444+
if end_cmp != Ordering::Equal {
445+
return Some(end_cmp);
446+
}
447+
}
448+
Some(self.segments.len().cmp(&other.segments.len()))
449+
}
450+
}
451+
452+
impl<V: Ord> Ord for Range<V> {
453+
fn cmp(&self, other: &Self) -> Ordering {
454+
self.partial_cmp(other)
455+
.expect("PartialOrd must be `Some(Ordering)` for types that implement `Ord`")
456+
}
457+
}
458+
311459
/// The ordering of the version wrt to the interval.
312460
/// ```text
313461
/// |-------|
@@ -1069,4 +1217,33 @@ pub mod tests {
10691217
range.simplify(versions.into_iter())
10701218
);
10711219
}
1220+
1221+
#[test]
1222+
fn version_ord() {
1223+
let versions: &[Range<u32>] = &[
1224+
Range::strictly_lower_than(1u32),
1225+
Range::lower_than(1u32),
1226+
Range::singleton(1u32),
1227+
Range::between(1u32, 3u32),
1228+
Range::higher_than(1u32),
1229+
Range::strictly_higher_than(1u32),
1230+
Range::singleton(2u32),
1231+
Range::singleton(2u32).union(&Range::singleton(3u32)),
1232+
Range::singleton(2u32)
1233+
.union(&Range::singleton(3u32))
1234+
.union(&Range::singleton(4u32)),
1235+
Range::singleton(2u32).union(&Range::singleton(4u32)),
1236+
Range::singleton(3u32),
1237+
];
1238+
1239+
let mut versions_sorted = versions.to_vec();
1240+
versions_sorted.sort();
1241+
assert_eq!(versions_sorted, versions);
1242+
1243+
// Check that the sorting isn't just stable because we're returning equal.
1244+
let mut version_reverse_sorted = versions.to_vec();
1245+
version_reverse_sorted.reverse();
1246+
version_reverse_sorted.sort();
1247+
assert_eq!(version_reverse_sorted, versions);
1248+
}
10721249
}

0 commit comments

Comments
 (0)