mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-08 19:42:12 +00:00
storytold/wordcraft#3657: Russian hyphenation and hyphenated output linear in word length
Audit findings 10 (storytold/wordcraft#3657) and 15 (storytold/wordcraft#3692). - ru::hyphen::points: the cuts come in increasing order, so a repeat can only be the last one; compare with `out.last()` instead of `out.contains` per syllable (O(V^2) on a long Cyrillic word, during layout). - hyphenate_word_in: walk the sorted break points alongside the chars instead of searching the point list for every char. Same output. Test: russian_hyphenation_and_hyphenated_output_are_linear (8x the letters costs ~8x; the old code measured 31x and 30x). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
f0561b7fb0
commit
eb37158db4
2 files changed
+30
-4
No files matched your search
@@ -320,10 +320,14 @@ pub fn hyphenate_word(word: &str, lim: &Limits) -> String {
|
||||
|
||||
/// `word`, a word in `lang`, with `-` at every hyphenation point (tests and diagnostics).
|
||||
pub fn hyphenate_word_in(word: &str, lim: &Limits, lang: ProofLang) -> String {
|
||||
let pts = hyphen_points_in(word, lim, lang);
|
||||
let mut s = String::with_capacity(word.len() + pts.len());
|
||||
let mut pts = hyphen_points_in(word, lim, lang);
|
||||
pts.sort_unstable();
|
||||
// Walk the points alongside the chars rather than searching them for each char.
|
||||
let mut next = pts.into_iter().peekable();
|
||||
let mut s = String::with_capacity(word.len() + 8);
|
||||
for (i, ch) in word.chars().enumerate() {
|
||||
if pts.contains(&i) {
|
||||
if next.next_if_eq(&i).is_some() {
|
||||
while next.next_if_eq(&i).is_some() {}
|
||||
s.push('-');
|
||||
}
|
||||
s.push(ch);
|
||||
@@ -706,6 +710,26 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn russian_hyphenation_and_hyphenated_output_are_linear() {
|
||||
// A Russian word breaks at every syllable: each break used to be checked against all the
|
||||
// earlier ones (#3657), and the hyphenated output searched every break for each char.
|
||||
let word = |k: usize| "мама".repeat(k);
|
||||
assert_eq!(word_breaks_in(&word(2), ProofLang::Ru).0, [2, 4, 6]);
|
||||
assert_eq!(hyphenate_word_in(&word(2), &Limits::default(), ProofLang::Ru), "ма-ма-ма-ма");
|
||||
// The breaks' rescan is a cheap one per break: it takes longer words to stand out.
|
||||
for (what, k, repeat, f) in [
|
||||
("points", 1000, 3, (|w: &str| drop(hyphen_points_in(w, &Limits::default(), ProofLang::Ru))) as fn(&str)),
|
||||
("hyphenated", 400, 20, |w: &str| drop(hyphenate_word_in(w, &Limits::default(), ProofLang::Ru))),
|
||||
] {
|
||||
let (small, big) = (word(k), word(8 * k));
|
||||
let time = |w: &str| (0..repeat).for_each(|_| f(w));
|
||||
let (t1, t8, ratio) = cost_ratio(8, || time(&small), || time(&big));
|
||||
eprintln!("{what}: t(n) {t1:.4} s, t(8n) {t8:.4} s, ratio {ratio:.1}");
|
||||
assert!(ratio < 24.0, "8x the letters, not linear: {what} {ratio:.1}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn heuristic_fallback() {
|
||||
let c: Vec<char> = "happen".chars().collect();
|
||||
|
||||
@@ -57,7 +57,9 @@ pub fn points(c: &[char]) -> Vec<usize> {
|
||||
while c.get(cut).is_some_and(|&x| sticks_left(x)) && cut < end {
|
||||
cut += 1;
|
||||
}
|
||||
if cut >= 2 && n - cut >= 2 && !out.contains(&cut) {
|
||||
// Cuts come in increasing order (each lies after the previous pair's vowel), so a repeat
|
||||
// can only be the last one: no scan of the whole list per syllable (#3657).
|
||||
if cut >= 2 && n - cut >= 2 && out.last().is_none_or(|&l| l < cut) {
|
||||
out.push(cut);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user