Page ranges: overlapping intervals skip indices already picked

parse_range walked every index of every interval, so a range that names
the same pages many times ("1-,1-,1-,...") cost the number of parts
times the page count. Picked indices now link to the next unpicked one,
so each page is visited about once whatever the overlap.
This commit is contained in:
Dylan M. Taylor
2026-11-02 20:33:30 -05:00
parent 11862f6119
commit 1ad4233846
+41 -5
View File
@@ -17,7 +17,9 @@ pub fn parse_range(s: &str, count: usize) -> Result<Vec<usize>, String> {
.ok_or_else(|| format!("range `{s}`: `{t}` is not a number from 1 to {count}"))
};
let mut out = vec![];
let mut seen = std::collections::HashSet::new();
// Past each run of indices picked so far, the index after it: an interval skips the runs it
// overlaps instead of walking them again, so repeating `1-` costs no more than writing it once.
let mut next = std::collections::HashMap::new();
for part in s.split(',').map(str::trim).filter(|p| !p.is_empty()) {
let (a, b) = match part.replace('\u{2013}', "-").split_once('-') {
Some((a, b)) if a.trim().is_empty() && b.trim().is_empty() => return Err(format!("range `{s}`: `{part}` names no number")),
@@ -30,10 +32,11 @@ pub fn parse_range(s: &str, count: usize) -> Result<Vec<usize>, String> {
if a > b {
return Err(format!("range `{s}`: `{part}` runs backwards"));
}
for i in a - 1..b {
if seen.insert(i) {
out.push(i);
}
let mut i = unpicked(&mut next, a - 1);
while i < b {
out.push(i);
next.insert(i, i + 1);
i = unpicked(&mut next, i + 1);
}
}
if out.is_empty() {
@@ -42,6 +45,23 @@ pub fn parse_range(s: &str, count: usize) -> Result<Vec<usize>, String> {
Ok(out)
}
/// The first index from `i` on not picked yet, `next` linking each picked index onward (the links
/// followed are then pointed straight at it).
fn unpicked(next: &mut std::collections::HashMap<usize, usize>, i: usize) -> usize {
let mut end = i;
while let Some(&j) = next.get(&end) {
end = j;
}
let mut at = i;
while at != end {
match next.insert(at, end) {
Some(j) => at = j,
None => break,
}
}
end
}
#[cfg(test)]
mod tests {
use super::parse_range;
@@ -53,4 +73,20 @@ mod tests {
assert!(parse_range("-+2", 5).is_err());
assert_eq!(parse_range("1-3, 5", 5), Ok(vec![0, 1, 2, 4]));
}
#[test]
fn repeats_and_overlaps_are_dropped_in_order() {
assert_eq!(parse_range("3-5, 1-4, 2, 6-, -1", 7), Ok(vec![2, 3, 4, 0, 1, 5, 6]));
assert_eq!(parse_range("2,2,1-2", 4), Ok(vec![1, 0]));
assert_eq!(parse_range("4, 2, 1-", 5), Ok(vec![3, 1, 0, 2, 4]));
}
#[test]
fn a_range_repeated_many_times_is_read_once() {
// 100 000 pages named 50 000 times: walking the pages for each would take billions of steps.
let s = "1-,".repeat(50_000);
let pages = parse_range(&s, 100_000).unwrap();
assert_eq!(pages.len(), 100_000);
assert!(pages.iter().enumerate().all(|(i, p)| i == *p));
}
}