Merge pull request #5292 from storytold/fix/sec-page-range-linear

Page ranges: overlapping intervals skip indices already picked
This commit is contained in:
Brandon Thomas
2026-11-03 00:19:11 -05:00
committed by GitHub
+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));
}
}