mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-11 16:31:54 -06:00
Proofing: look up unchecked runs by binary search, not a scan per issue
paragraph_issues dropped issues touching a run left unchecked by testing every run of the paragraph against every issue. A paragraph can have many runs and many issues within the proofing size limit, so the filter was quadratic in the paragraph's size. The unchecked ranges are now merged and sorted once, each issue is looked up by binary search, and paragraphs without unchecked runs skip the filter.
This commit is contained in:
@@ -46,8 +46,37 @@ pub fn paragraph_issues(
|
||||
}
|
||||
let text = proof_text(p);
|
||||
let spans: Vec<(std::ops::Range<usize>, wordcraft_proof::ProofLang)> = runs.iter().map(|r| (r.range.clone(), r.lang)).collect();
|
||||
let skip = |a: usize, b: usize| runs.iter().any(|r| r.skip && r.range.start < b && a < r.range.end);
|
||||
wordcraft_proof::check_text_with(&text, &spans, spelling, grammar, list_item).into_iter().filter(|i| !skip(i.start, i.end)).collect()
|
||||
let skipped = skipped_ranges(runs);
|
||||
let issues = wordcraft_proof::check_text_with(&text, &spans, spelling, grammar, list_item);
|
||||
if skipped.is_empty() {
|
||||
return issues;
|
||||
}
|
||||
issues.into_iter().filter(|i| !overlaps(&skipped, i.start, i.end)).collect()
|
||||
}
|
||||
|
||||
/// The byte ranges of the runs left unchecked, in order, those that overlap merged: their
|
||||
/// starts and ends both rise, so [`overlaps`] finds an issue's by binary search instead of
|
||||
/// looking at every run for every issue.
|
||||
fn skipped_ranges(runs: &[ProofRun]) -> Vec<(usize, usize)> {
|
||||
let mut ranges: Vec<(usize, usize)> = runs.iter().filter(|r| r.skip).map(|r| (r.range.start, r.range.end)).collect();
|
||||
ranges.sort_unstable();
|
||||
let mut merged: Vec<(usize, usize)> = Vec::with_capacity(ranges.len());
|
||||
for (s, e) in ranges {
|
||||
crate::work::steps(1);
|
||||
match merged.last_mut() {
|
||||
Some(last) if s < last.1 => last.1 = last.1.max(e),
|
||||
_ => merged.push((s, e)),
|
||||
}
|
||||
}
|
||||
merged
|
||||
}
|
||||
|
||||
/// Whether bytes `a..b` overlap one of `skipped` ([`skipped_ranges`]): the first range ending
|
||||
/// after `a` is the only one that can, as every later one starts no earlier.
|
||||
fn overlaps(skipped: &[(usize, usize)], a: usize, b: usize) -> bool {
|
||||
crate::work::steps(1);
|
||||
let k = skipped.partition_point(|&(_, e)| e <= a);
|
||||
skipped.get(k).is_some_and(|&(s, _)| s < b)
|
||||
}
|
||||
|
||||
/// Whether the paragraph is a list item: its own numbering, or its style's, names a list (not 0).
|
||||
@@ -101,3 +130,26 @@ pub fn proof_text(p: &wordcraft_doc::Paragraph) -> String {
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn run(range: std::ops::Range<usize>, skip: bool) -> ProofRun {
|
||||
ProofRun { range, lang: Default::default(), skip }
|
||||
}
|
||||
|
||||
/// The lookup finds what a look at every run finds, overlapping and empty runs included.
|
||||
#[test]
|
||||
fn skipped_lookup_matches_a_scan_of_every_run() {
|
||||
let runs = [run(0..4, false), run(4..9, true), run(5..6, true), run(9..9, true), run(9..12, false), run(12..20, true), run(14..30, true)];
|
||||
let skipped = skipped_ranges(&runs);
|
||||
for a in 0..32 {
|
||||
for b in a..32 {
|
||||
let scan = runs.iter().any(|r| r.skip && r.range.start < b && a < r.range.end);
|
||||
assert_eq!(overlaps(&skipped, a, b), scan, "{a}..{b}");
|
||||
}
|
||||
}
|
||||
assert!(skipped_ranges(&[run(0..5, false)]).is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user