mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-08 19:42:12 +00:00
Proofing: a candidate found again at a lower cost ranks at that cost
The suggestion rounds don't run in rising cost: a MAP diacritic drop (11) comes before a REP confusion (10, 9 for Russian). The engine checked each candidate once, at the first cost it was offered, so the same word found again more cheaply kept the dearer cost and could rank one place too low. Czech's engine always did this, and the merged engine took it over. The candidate set is now a map to the cheapest cost offered so far, and an offer that is no cheaper is skipped as before. Found by CodeRabbit on storytold/wordcraft#4191. Tests: a_cheaper_later_offer_wins; skipping every repeat offer again fails it. Proof: 288 tests.
This commit is contained in:
1 parent
140c7041e1
commit
b06aece4eb
1 file changed
+23
-3
@@ -102,7 +102,8 @@ struct Collector<'a> {
|
||||
d: &'a AffixDict,
|
||||
/// The word, lower case.
|
||||
input: String,
|
||||
tested: HashSet<String>,
|
||||
/// Candidate → the cheapest cost it was offered at (rounds don't run in rising cost).
|
||||
tested: HashMap<String, u32>,
|
||||
/// Suggestion → (cost, order found).
|
||||
found: HashMap<String, (u32, usize)>,
|
||||
seq: usize,
|
||||
@@ -112,9 +113,16 @@ impl Collector<'_> {
|
||||
/// Check a lower-case candidate (a word, or two separated by a space) and keep it if every
|
||||
/// word is in the dictionary, as written or capitalized (names).
|
||||
fn offer(&mut self, cand: &str, cost: u32) {
|
||||
if cand.is_empty() || cand == self.input || !self.tested.insert(cand.to_string()) {
|
||||
if cand.is_empty() || cand == self.input {
|
||||
return;
|
||||
}
|
||||
match self.tested.get_mut(cand) {
|
||||
Some(prev) if *prev <= cost => return,
|
||||
Some(prev) => *prev = cost,
|
||||
None => {
|
||||
self.tested.insert(cand.to_string(), cost);
|
||||
}
|
||||
}
|
||||
let mut words = Vec::new();
|
||||
let mut cost = cost;
|
||||
for part in cand.split(' ') {
|
||||
@@ -270,7 +278,7 @@ pub(crate) fn suggest_in(d: &AffixDict, word: &str, max: usize, t: &Tuning) -> V
|
||||
let case = casing(word);
|
||||
let input = word.to_lowercase();
|
||||
let chars: Vec<char> = input.chars().collect();
|
||||
let mut col = Collector { d, input: input.clone(), tested: HashSet::new(), found: HashMap::new(), seq: 0 };
|
||||
let mut col = Collector { d, input: input.clone(), tested: HashMap::new(), found: HashMap::new(), seq: 0 };
|
||||
// 1. Letter case.
|
||||
if d.contains(&input) && word != input {
|
||||
col.keep(input.clone(), COST_CASE);
|
||||
@@ -463,6 +471,18 @@ mod tests {
|
||||
assert!(suggestions.contains(&"počítač z".into()), "{suggestions:?}");
|
||||
}
|
||||
|
||||
/// A candidate offered again at a lower cost (a REP confusion after a MAP diacritic drop)
|
||||
/// ranks at the lower one.
|
||||
#[test]
|
||||
fn a_cheaper_later_offer_wins() {
|
||||
let d = AffixDict::from_hunspell("", "1\ncafe\n", &[]).unwrap();
|
||||
let mut col = Collector { d: &d, input: "cafè".into(), tested: HashMap::new(), found: HashMap::new(), seq: 0 };
|
||||
col.offer("cafe", COST_DROP_DIACRITIC);
|
||||
col.offer("cafe", COST_REP);
|
||||
col.offer("cafe", COST_SCAN);
|
||||
assert_eq!(col.found.get("cafe").map(|e| e.0), Some(COST_REP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distance_weighs_map_slips_half() {
|
||||
let map = vec![vec!['a', 'ą'], vec!['o', 'ó', 'u']];
|
||||
|
||||
Reference in new issue
Block a user