Skip to content

Commit 30502ce

Browse files
committed
Add Ranges::from_iter
See pubgrub-rs#273, it's split out from that PR, for https://github.com/astral-sh/uv/pull/8797/files. Closes #33
1 parent 95e1390 commit 30502ce

File tree

1 file changed

+115
-0
lines changed

1 file changed

+115
-0
lines changed

version-ranges/src/lib.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,36 @@ impl<V: Ord> Ranges<V> {
283283
}
284284
}
285285

286+
/// We want to use `iterator_try_collect`, but since it's unstable at the time of writing,
287+
/// we expose a public `FromIterator<(Bound<V>, Bound<V>)>` method and use this for internal
288+
/// testing.
289+
fn try_from(
290+
into_iter: impl IntoIterator<Item = (Bound<V>, Bound<V>)>,
291+
) -> Result<Self, FromIterError> {
292+
let mut iter = into_iter.into_iter();
293+
let Some(mut previous) = iter.next() else {
294+
return Ok(Self {
295+
segments: SmallVec::new(),
296+
});
297+
};
298+
let mut segments = SmallVec::with_capacity(iter.size_hint().0);
299+
for current in iter {
300+
if !valid_segment(&previous.start_bound(), &previous.end_bound()) {
301+
return Err(FromIterError::InvalidSegment);
302+
}
303+
if !end_before_start_with_gap(&previous.end_bound(), &current.start_bound()) {
304+
return Err(FromIterError::OverlappingSegments);
305+
}
306+
segments.push(previous);
307+
previous = current;
308+
}
309+
if !valid_segment(&previous.start_bound(), &previous.end_bound()) {
310+
return Err(FromIterError::InvalidSegment);
311+
}
312+
segments.push(previous);
313+
Ok(Self { segments })
314+
}
315+
286316
fn check_invariants(self) -> Self {
287317
if cfg!(debug_assertions) {
288318
for p in self.segments.as_slice().windows(2) {
@@ -826,6 +856,40 @@ impl<V: Ord + Clone> Ranges<V> {
826856
}
827857
}
828858

859+
/// User provided segment iterator breaks [`Ranges`] invariants.
860+
///
861+
/// Not user accessible since `FromIterator<(Bound<V>, Bound<V>)>` panics and `iterator_try_collect`
862+
/// is unstable.
863+
#[derive(Debug, PartialEq, Eq)]
864+
enum FromIterError {
865+
/// The start of a segment must be before its end, and a segment must contain at least one
866+
/// version.
867+
InvalidSegment,
868+
/// The end of a segment is not before the start of the next segment, leaving at least one
869+
/// version space.
870+
OverlappingSegments,
871+
}
872+
873+
impl Display for FromIterError {
874+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
875+
match self {
876+
FromIterError::InvalidSegment => f.write_str("segment must be valid"),
877+
FromIterError::OverlappingSegments => {
878+
f.write_str("end of a segment and start of the next segment must not overlap")
879+
}
880+
}
881+
}
882+
}
883+
884+
impl<V: Ord> FromIterator<(Bound<V>, Bound<V>)> for Ranges<V> {
885+
/// Construct a [`Ranges`] from an ordered list of bounds.
886+
///
887+
/// Panics if the bounds aren't sorted, are empty or have no space to the next bound.
888+
fn from_iter<T: IntoIterator<Item = (Bound<V>, Bound<V>)>>(iter: T) -> Self {
889+
Self::try_from(iter).unwrap()
890+
}
891+
}
892+
829893
// REPORT ######################################################################
830894

831895
impl<V: Display + Eq> Display for Ranges<V> {
@@ -1130,6 +1194,24 @@ pub mod tests {
11301194
}
11311195
assert!(simp.segments.len() <= range.segments.len())
11321196
}
1197+
1198+
#[test]
1199+
fn from_iter_valid(segments in proptest::collection::vec(any::<(Bound<u32>, Bound<u32>)>(), ..30)) {
1200+
match Ranges::try_from(segments.clone()) {
1201+
Ok(ranges) => {
1202+
ranges.check_invariants();
1203+
}
1204+
Err(_) => {
1205+
assert!(
1206+
segments
1207+
.as_slice()
1208+
.windows(2)
1209+
.any(|p| !end_before_start_with_gap(&p[0].1, &p[1].0))
1210+
|| segments.iter().any(|(start, end)| !valid_segment(start, end))
1211+
);
1212+
}
1213+
}
1214+
}
11331215
}
11341216

11351217
#[test]
@@ -1194,4 +1276,37 @@ pub mod tests {
11941276
version_reverse_sorted.sort();
11951277
assert_eq!(version_reverse_sorted, versions);
11961278
}
1279+
1280+
/// Test all error conditions in [`Ranges::try_from`].
1281+
#[test]
1282+
fn from_iter_errors() {
1283+
// Unbounded in not at an end
1284+
let result = Ranges::try_from([
1285+
(Bound::Included(1), Bound::Unbounded),
1286+
(Bound::Included(2), Bound::Unbounded),
1287+
]);
1288+
assert_eq!(result, Err(FromIterError::OverlappingSegments));
1289+
// Not a version in between
1290+
let result = Ranges::try_from([
1291+
(Bound::Included(1), Bound::Excluded(2)),
1292+
(Bound::Included(2), Bound::Unbounded),
1293+
]);
1294+
assert_eq!(result, Err(FromIterError::OverlappingSegments));
1295+
// First segment
1296+
let result = Ranges::try_from([(Bound::Excluded(2), Bound::Included(2))]);
1297+
assert_eq!(result, Err(FromIterError::InvalidSegment));
1298+
// Middle segment
1299+
let result = Ranges::try_from([
1300+
(Bound::Included(1), Bound::Included(2)),
1301+
(Bound::Included(3), Bound::Included(2)),
1302+
(Bound::Included(4), Bound::Included(5)),
1303+
]);
1304+
assert_eq!(result, Err(FromIterError::InvalidSegment));
1305+
// Last segment
1306+
let result = Ranges::try_from([
1307+
(Bound::Included(1), Bound::Included(2)),
1308+
(Bound::Included(3), Bound::Included(2)),
1309+
]);
1310+
assert_eq!(result, Err(FromIterError::InvalidSegment));
1311+
}
11971312
}

0 commit comments

Comments
 (0)