From d1ab0f716d4768b75954963b289f6459ec7c9e4e Mon Sep 17 00:00:00 2001 From: konsti Date: Tue, 20 Aug 2024 18:42:07 +0200 Subject: [PATCH] Implement `[Partial]Ord` for `Range` (#31) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In uv, we want to have a stable ordering of certain types that `Range`, both for determinism internally and for deterministically ordered output files. This PR adds `impl PartialOrd for Range` and `impl Ord for Range` 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. --- src/range.rs | 120 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/src/range.rs b/src/range.rs index aca8655e..98285683 100644 --- a/src/range.rs +++ b/src/range.rs @@ -308,6 +308,104 @@ impl Range { } } +/// Implementing `PartialOrd` for start `Bound` of an interval. +/// +/// Legend: `∞` is unbounded, `[1,2]` is `>1,<2`, `]1,2[` is `>=1,<=2`. +/// +/// ```text +/// left: ∞-------] +/// right: [-----] +/// left is smaller, since it starts earlier. +/// +/// left: [-----] +/// right: ]-----] +/// left is smaller, since it starts earlier. +/// ``` +fn cmp_bounds_start(left: Bound<&V>, right: Bound<&V>) -> Option { + match (left, right) { + (Unbounded, Unbounded) => Some(Ordering::Equal), + (Included(_left), Unbounded) => Some(Ordering::Greater), + (Excluded(_left), Unbounded) => Some(Ordering::Greater), + (Unbounded, Included(_right)) => Some(Ordering::Less), + (Included(left), Included(right)) => left.partial_cmp(right), + (Excluded(left), Included(right)) => match left.partial_cmp(right)? { + Ordering::Less => Some(Ordering::Less), + Ordering::Equal => Some(Ordering::Greater), + Ordering::Greater => Some(Ordering::Greater), + }, + (Unbounded, Excluded(_right)) => Some(Ordering::Less), + (Included(left), Excluded(right)) => match left.partial_cmp(right)? { + Ordering::Less => Some(Ordering::Less), + Ordering::Equal => Some(Ordering::Less), + Ordering::Greater => Some(Ordering::Greater), + }, + (Excluded(left), Excluded(right)) => left.partial_cmp(right), + } +} + +/// Implementing `PartialOrd` for end `Bound` of an interval. +/// +/// We flip the unbounded ranges from `-∞` to `∞`, while `V`-valued bounds checks remain the same. +/// +/// Legend: `∞` is unbounded, `[1,2]` is `>1,<2`, `]1,2[` is `>=1,<=2`. +/// +/// ```text +/// left: [--------∞ +/// right: [-----] +/// left is greater, since it starts earlier. +/// +/// left: [-----[ +/// right: [-----] +/// left is smaller, since it ends earlier. +/// ``` +fn cmp_bounds_end(left: Bound<&V>, right: Bound<&V>) -> Option { + match (left, right) { + (Unbounded, Unbounded) => Some(Ordering::Equal), + (Included(_left), Unbounded) => Some(Ordering::Less), + (Excluded(_left), Unbounded) => Some(Ordering::Less), + (Unbounded, Included(_right)) => Some(Ordering::Greater), + (Included(left), Included(right)) => left.partial_cmp(right), + (Excluded(left), Included(right)) => match left.partial_cmp(right)? { + Ordering::Less => Some(Ordering::Less), + Ordering::Equal => Some(Ordering::Less), + Ordering::Greater => Some(Ordering::Greater), + }, + (Unbounded, Excluded(_right)) => Some(Ordering::Greater), + (Included(left), Excluded(right)) => match left.partial_cmp(right)? { + Ordering::Less => Some(Ordering::Less), + Ordering::Equal => Some(Ordering::Greater), + Ordering::Greater => Some(Ordering::Greater), + }, + (Excluded(left), Excluded(right)) => left.partial_cmp(right), + } +} + +impl PartialOrd for Range { + /// 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). + fn partial_cmp(&self, other: &Self) -> Option { + for (left, right) in self.segments.iter().zip(other.segments.iter()) { + let start_cmp = cmp_bounds_start(left.start_bound(), right.start_bound())?; + if start_cmp != Ordering::Equal { + return Some(start_cmp); + } + let end_cmp = cmp_bounds_end(left.end_bound(), right.end_bound())?; + if end_cmp != Ordering::Equal { + return Some(end_cmp); + } + } + Some(self.segments.len().cmp(&other.segments.len())) + } +} + +impl Ord for Range { + fn cmp(&self, other: &Self) -> Ordering { + self.partial_cmp(other) + .expect("PartialOrd must be `Some(Ordering)` for types that implement `Ord`") + } +} + /// The ordering of the version wrt to the interval. /// ```text /// |-------| @@ -1069,4 +1167,26 @@ pub mod tests { range.simplify(versions.into_iter()) ); } + + #[test] + fn version_ord() { + let versions: &[Range] = &[ + Range::strictly_lower_than(1u32), + Range::lower_than(1u32), + Range::singleton(1u32), + Range::between(1u32, 3u32), + Range::higher_than(1u32), + Range::strictly_higher_than(1u32), + Range::singleton(2u32), + Range::singleton(2u32).union(&Range::singleton(3u32)), + Range::singleton(2u32) + .union(&Range::singleton(3u32)) + .union(&Range::singleton(4u32)), + Range::singleton(2u32).union(&Range::singleton(4u32)), + Range::singleton(3u32), + ]; + let mut versions_sorted = versions.to_vec(); + versions_sorted.sort(); + assert_eq!(versions_sorted, versions); + } }