From 3a8e5bbe90ecd422f09c01a9591bfb01acaf92ec Mon Sep 17 00:00:00 2001 From: Jacob Finkelman Date: Wed, 22 Nov 2023 20:40:07 +0000 Subject: [PATCH] feat: add a `simplify` for error messages --- src/range.rs | 160 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 151 insertions(+), 9 deletions(-) diff --git a/src/range.rs b/src/range.rs index be7ff250..ac6d346e 100644 --- a/src/range.rs +++ b/src/range.rs @@ -203,15 +203,16 @@ impl Range { pub fn contains(&self, v: &V) -> bool { for segment in self.segments.iter() { if match segment { - (Unbounded, Unbounded) => true, - (Unbounded, Included(end)) => v <= end, - (Unbounded, Excluded(end)) => v < end, - (Included(start), Unbounded) => v >= start, - (Included(start), Included(end)) => v >= start && v <= end, - (Included(start), Excluded(end)) => v >= start && v < end, - (Excluded(start), Unbounded) => v > start, - (Excluded(start), Included(end)) => v > start && v <= end, - (Excluded(start), Excluded(end)) => v > start && v < end, + (Excluded(start), _) => v <= start, + (Included(start), _) => v < start, + (Unbounded, _) => false, + } { + return false; + } + if match segment { + (_, Unbounded) => true, + (_, Included(end)) => v <= end, + (_, Excluded(end)) => v < end, } { return true; } @@ -219,6 +220,38 @@ impl Range { false } + /// Returns true if the this Range contains the specified values. + /// + /// The `versions` iterator must be sorted. + /// Functionally equivalent to `versions.map(|v| self.contains(v))`. + /// Except it runs in `O(size_of_range + len_of_versions)` not `O(size_of_range * len_of_versions)` + pub fn contains_many<'s, I>(&'s self, versions: I) -> impl Iterator + 's + where + I: Iterator + 's, + V: 's, + { + versions.scan(0, |i, v| { + while let Some(segment) = self.segments.get(*i) { + if match segment { + (Excluded(start), _) => v <= start, + (Included(start), _) => v < start, + (Unbounded, _) => false, + } { + return Some(false); + } + if match segment { + (_, Unbounded) => true, + (_, Included(end)) => v <= end, + (_, Excluded(end)) => v < end, + } { + return Some(true); + } + *i += 1; + } + Some(false) + }) + } + /// Construct a simple range from anything that impls [RangeBounds] like `v1..v2`. pub fn from_range_bounds(bounds: R) -> Self where @@ -321,6 +354,71 @@ impl Range { Self { segments }.check_invariants() } + + /// Returns a simpler Range that contains the same versions + /// + /// For every one of the Versions provided in versions the existing range and + /// the simplified range will agree on whether it is contained. + /// The simplified version may include or exclude versions that are not in versions as the implementation wishes. + /// For example: + /// - If all the versions are contained in the original than the range will be simplified to `full`. + /// - If none of the versions are contained in the original than the range will be simplified to `empty`. + /// + /// If versions are not sorted the correctness of this function is not guaranteed. + pub fn simplify<'a, I>(&self, versions: I) -> Self + where + I: Iterator, + V: 'a, + { + // First we determined for each version if there is a segment that makes it match. + let mut matches = versions.scan(0, |i, v| { + while let Some(segment) = self.segments.get(*i) { + if match segment { + (Excluded(start), _) => v <= start, + (Included(start), _) => v < start, + (Unbounded, _) => false, + } { + return Some(None); + } + if match segment { + (_, Unbounded) => true, + (_, Included(end)) => v <= end, + (_, Excluded(end)) => v < end, + } { + return Some(Some(*i)); + } + *i += 1; + } + Some(None) + }); + let mut segments = SmallVec::Empty; + // If the first version matched, then the lower bound of that segment is not needed + let mut seg = if let Some(Some(ver)) = matches.next() { + Some((&Unbounded, &self.segments[ver].1)) + } else { + None + }; + for ver in matches { + if let Some(ver) = ver { + // As long as were still matching versions, we keep merging into the currently matching segment + seg = Some(( + seg.map(|(s, _)| s).unwrap_or(&self.segments[ver].0), + &self.segments[ver].1, + )); + } else { + // If we have found a version that doesn't match, then right the merge segment and prepare for a new one. + if let Some((s, e)) = seg { + segments.push((s.clone(), e.clone())); + } + seg = None; + } + } + // If the last version matched, then write out the merged segment but the upper bound is not needed. + if let Some((s, _)) = seg { + segments.push((s.clone(), Unbounded)); + } + Self { segments }.check_invariants() + } } impl VersionSet for Range { @@ -600,5 +698,49 @@ pub mod tests { let rv2: Range = rv.bounding_range().map(Range::from_range_bounds::<_, u32>).unwrap_or_else(Range::empty); assert_eq!(rv, rv2); } + + #[test] + fn contains(range in strategy(), versions in vec![version_strat()]) { + fn direct_implementation_of_contains(s: &Range, v: &u32) -> bool { + for segment in s.segments.iter() { + if match segment { + (Unbounded, Unbounded) => true, + (Unbounded, Included(end)) => v <= end, + (Unbounded, Excluded(end)) => v < end, + (Included(start), Unbounded) => v >= start, + (Included(start), Included(end)) => v >= start && v <= end, + (Included(start), Excluded(end)) => v >= start && v < end, + (Excluded(start), Unbounded) => v > start, + (Excluded(start), Included(end)) => v > start && v <= end, + (Excluded(start), Excluded(end)) => v > start && v < end, + } { + return true; + } + } + false + } + + for v in versions { + assert_eq!(range.contains(&v), direct_implementation_of_contains(&range, &v)); + } + } + + #[test] + fn contains_many(range in strategy(), mut versions in vec![version_strat()]) { + versions.sort(); + for (a, b) in versions.iter().map(|v| range.contains(v)).zip(range.contains_many(versions.iter())) { + assert_eq!(a, b); + } + } + + #[test] + fn simplify(range in strategy(), mut versions in vec![version_strat()]) { + versions.sort(); + let simp = range.simplify(versions.iter()); + for v in versions { + assert_eq!(range.contains(&v), simp.contains(&v)); + } + assert!(simp.segments.len() <= range.segments.len()) + } } }