Skip to content

Commit

Permalink
feat: add a simplify for error messages
Browse files Browse the repository at this point in the history
  • Loading branch information
Eh2406 committed Nov 22, 2023
1 parent acfbe99 commit b116cf6
Showing 1 changed file with 151 additions and 9 deletions.
160 changes: 151 additions & 9 deletions src/range.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,22 +203,55 @@ impl<V: Ord> Range<V> {
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;
}
}
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<Item = bool> + 's
where
I: Iterator<Item = &'s V> + '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;
}
return Some(false);
})
}

/// Construct a simple range from anything that impls [RangeBounds] like `v1..v2`.
pub fn from_range_bounds<R, IV>(bounds: R) -> Self
where
Expand Down Expand Up @@ -321,6 +354,71 @@ impl<V: Ord + Clone> Range<V> {

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<Item = &'a V>,
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;
}
return 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<T: Debug + Display + Clone + Eq + Ord> VersionSet for Range<T> {
Expand Down Expand Up @@ -600,5 +698,49 @@ pub mod tests {
let rv2: Range<u32> = 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<u32>, 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())
}
}
}

0 comments on commit b116cf6

Please sign in to comment.