diff --git a/crates/doc/src/range.rs b/crates/doc/src/range.rs index 87db16f20..7162709bc 100644 --- a/crates/doc/src/range.rs +++ b/crates/doc/src/range.rs @@ -17,7 +17,9 @@ pub fn parse_range(s: &str, count: usize) -> Result, 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, 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, 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, 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)); + } }