sclass QAHandler { QA findQAWithTypos(S s, S language) { Lowest qa = findClosestQA(s, language); if (!qa.has()) null; if (qa.score() > maxTypos) { print("Rejecting " + nTypos((int) qa.score()) + ": " + s + " / " + qa->patterns); null; } print("Accepting " + nTypos((int) qa.score()) + ": " + s + " / " + qa->patterns); ret qa!; } QA findMatchingQA(S s, S language, bool allowTypos) { try object QA qa = findMatchingQA(s, conceptsWhere(QA, +language)); try object QA qa = findMatchingQA(s, filter(list(QA), q -> neq(q.language, language))); if (allowTypos) try object QA qa = findQAWithTypos(s, language); null; } static Lowest findClosestQA(S s, S language) { time "findClosestQA" { new Lowest qa; findClosestQA(s, qa, conceptsWhere(QA, +language)); findClosestQA(s, qa, filter(list(QA), q -> neq(q.language, language))); } ret qa; } static L sortQAs(Cl qas) { Map categoryToIndex = fieldToFieldIndex('name, 'index, list(Category)); ret sortedByCalculatedField(qas, q -> pair(categoryToIndex.get(q.category), q.index)); } svoid reindexQAs() { for (Language lang) { int index = 1; for (QA qa : sortQAs(conceptsWhere(QA, language := lang.code))) cset(qa, index := index++); } } static QA findMatchingQA(S s, Cl qas) { for (QA qa : sortQAs(qas)) if (mmo2_match(qa.parsedPattern(), s)) ret qa; null; } svoid findClosestQA(S s, Lowest best, Cl qas) { for (QA qa : sortQAs(qas)) { Int score = mmo2_levenWithSwapsScore(qa.parsedPattern(), s); if (score != null) best.put(qa, score); } } }