mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 07:16:52 +00:00
Merge pull request #5292 from storytold/fix/sec-page-range-linear
Page ranges: overlapping intervals skip indices already picked
This commit is contained in:
+41
-5
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user