mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-09 20:12:57 +00:00
Hunspell: no empty forms, and a circumfix suffix inside a chain needs its prefix
Two more cases the forms() checks missed. - With FULLSTRIP a suffix can strip a stem down to nothing, so forms() could list an empty form that contains() never accepts. Empty forms are no longer listed, at the first suffix, in a chain or with a prefix. - A circumfix suffix at the start of a suffix chain was only checked for the outermost rule, so work/A, SFX A able/Bc, SFX B s was accepted as "workables" without the prefix. A circumfix suffix under another suffix is no longer a way to a word without its prefix, and forms() does not chain a form that ends in one. New tests cover both. Over the whole hunspell-lv lv_LV dictionary (66,480 stems, 4.5 million forms) every form forms() lists is still accepted by the checker. Ported from storytold/wordcraft#5012 (d655b3188) onto craft-proof. Adapted to flag ids; the form_has_flag circumfix filter already came in with the first port. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
This commit is contained in:
1 parent
2bf2285577
commit
425ae605ca
1 file changed
+44
-3
@@ -485,6 +485,11 @@ fn cap_try(s: &str) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
/// `form`, unless it is empty: no word is, and `FULLSTRIP` can strip a stem down to nothing.
|
||||
fn non_empty(form: String) -> Option<String> {
|
||||
Some(form).filter(|f| !f.is_empty())
|
||||
}
|
||||
|
||||
/// `0` means empty in Hunspell's strip and append fields.
|
||||
fn zero_empty(s: &str) -> String {
|
||||
if s == "0" { String::new() } else { s.to_string() }
|
||||
@@ -1553,11 +1558,15 @@ impl AffixDict {
|
||||
continue;
|
||||
}
|
||||
let form = format!("{base}{}", r.add);
|
||||
if form.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let circ = self.cont_holds(r, self.circumfix);
|
||||
if r.cross {
|
||||
cross_sfx.push((form.clone(), circ));
|
||||
}
|
||||
if r.cont != 0 && queued.insert((form.clone(), r.cont)) {
|
||||
// A circumfix suffix needs its prefix, so what it ends is never chained without one.
|
||||
if r.cont != 0 && !circ && queued.insert((form.clone(), r.cont)) {
|
||||
chain.push((form.clone(), r.cont));
|
||||
}
|
||||
// A circumfix suffix or one that needs another affix is no word on its own.
|
||||
@@ -1589,6 +1598,9 @@ impl AffixDict {
|
||||
continue;
|
||||
}
|
||||
let next = format!("{base}{}", r.add);
|
||||
if next.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if r.cont != 0 && queued.insert((next.clone(), r.cont)) {
|
||||
chain.push((next.clone(), r.cont));
|
||||
}
|
||||
@@ -1613,7 +1625,7 @@ impl AffixDict {
|
||||
}
|
||||
let circ = self.cont_holds(r, self.circumfix);
|
||||
if !circ && !self.cont_holds(r, self.need_affix) {
|
||||
out.push(format!("{}{rest}", r.add));
|
||||
out.extend(non_empty(format!("{}{rest}", r.add)));
|
||||
}
|
||||
if r.cross {
|
||||
// Every suffix form looked at counts against the work budget, matching or not.
|
||||
@@ -1626,7 +1638,7 @@ impl AffixDict {
|
||||
continue;
|
||||
}
|
||||
if let Some(rest) = s.strip_prefix(&*r.strip) {
|
||||
out.push(format!("{}{rest}", r.add));
|
||||
out.extend(non_empty(format!("{}{rest}", r.add)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3113,4 +3125,33 @@ x/12
|
||||
assert_eq!(forms, ["x", "xy", "z"]);
|
||||
assert!(d.contains("z"));
|
||||
}
|
||||
|
||||
/// `forms()` never lists an empty form, which `FULLSTRIP` can produce when a suffix replaces
|
||||
/// the whole stem with nothing, at the first suffix or further along a chain.
|
||||
#[test]
|
||||
fn forms_never_list_an_empty_word() {
|
||||
let aff = "SET UTF-8\nFULLSTRIP\nSFX A Y 2\nSFX A x 0 .\nSFX A 0 y/B .\nSFX B Y 1\nSFX B xy 0 .\n";
|
||||
let d = AffixDict::from_hunspell(aff, "1\nx/A\n", &[]).unwrap();
|
||||
let mut forms = d.forms("x");
|
||||
forms.sort();
|
||||
assert_eq!(forms, ["x", "xy"]);
|
||||
assert!(!d.contains(""));
|
||||
}
|
||||
|
||||
/// A circumfix suffix in the middle of a suffix chain still needs its prefix: `workable`
|
||||
/// carries the circumfix, so neither it nor `workables` is a word without `ne`.
|
||||
#[test]
|
||||
fn circumfix_suffix_inside_a_chain_still_needs_its_prefix() {
|
||||
let aff = "SET UTF-8\nCIRCUMFIX c\nPFX P Y 1\nPFX P 0 ne/c .\nSFX A Y 1\nSFX A 0 able/Bc .\nSFX B Y 1\nSFX B 0 s .\n";
|
||||
let d = AffixDict::from_hunspell(aff, "1\nwork/AP\n", &[]).unwrap();
|
||||
let mut forms = d.forms("work");
|
||||
forms.sort();
|
||||
assert_eq!(forms, ["neworkable", "work"]);
|
||||
for f in &forms {
|
||||
assert!(d.contains(f), "{f}");
|
||||
}
|
||||
for w in ["workable", "workables", "nework"] {
|
||||
assert!(!d.contains(w), "{w}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user