Решение на Spell Checker от Катерина Манева
Към профила на Катерина Манева
Резултати
- 12 точки от тестове
- 0 бонус точки
- 12 точки общо
- 9 успешни тест(а)
- 6 неуспешни тест(а)
Код
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
use std::collections::HashMap;
use std::iter::FromIterator;
/// Тази функция премахва всякакви специални символи от низа, освен:
///
/// - Азбучни символи (`char::is_alphabetical`)
/// - Празни символи (`char::is_whitespace`)
/// - Апостроф и тиренце (`'`, `-`)
///
/// Тоест, целта е да сведе един низ до само думи и празни разстояния между тях. Казва се
/// `clean_line`, защото се очаква да бъде викана с по един ред at a time, без нови редове.
///
/// Функцията също се очаква да премахне начален и краен whitespace от низа (Използвайте `.trim`,
/// basically).
///
pub fn clean_line(input: &str) -> String {
input.trim().chars().filter(|c| c.is_alphabetic() || c.is_whitespace() || *c == '\'' || *c == '-').collect()
}
pub struct WordCounter {
corpus: HashMap<String, u32>,
}
impl WordCounter {
/// Конструира нов `WordCounter` без никакви данни.
///
pub fn new() -> Self {
let corpus = HashMap::new();
Self{corpus: corpus}
}
/// Прочита входния низ, ред по ред, обработва всеки ред с `clean_line`, разбива го на думи и
/// конструира нов WordCounter, който ще брои думите на този низ.
///
/// Нормализира думите по същия начин както `add` по-долу.
///
pub fn from_str(input: &str) -> Self {
let mut counter = WordCounter::new();
let lines = input.lines();
for line in lines {
let cleaned_line = clean_line(line);
for word in cleaned_line.split_whitespace() {
counter.add(word);
}
}
counter
}
/// Връща (references към) всички съхранени думи във вектор, сортиран по азбучен ред.
///
pub fn words(&self) -> Vec<&String> {
let mut vec = Vec::from_iter(self.corpus.keys());
vec.sort_unstable();
vec
}
/// Брои думата с WordCounter-а. Очаква се входа да бъде:
///
/// - Изчистен от всякакъв начален и краен whitespace
/// - Сведен до малки букви
///
/// Тоест:
///
/// `counter.add("Foo")` е еквивалентно на
/// `counter.add("foo")` е еквивалентно на
/// `counter.add(" foo ")`
///
pub fn add(&mut self, item: &str) {
let lowercase = item.to_lowercase();
let trimmed = lowercase.trim();
let counter = self.corpus.entry(trimmed.to_string()).or_insert(0);
*counter += 1;
}
/// Връща колко пъти е била преброена дадената дума.
///
pub fn get(&self, word: &str) -> u32 {
let value = self.corpus.get(&word.to_string());
*value.unwrap_or(&0)
}
/// Връща колко общо думи са били преброени. Тоест:
///
/// counter.add("foo");
/// counter.add("foo");
/// counter.add("bar");
///
/// се очаква да ни даде `total_count()` 3.
///
pub fn total_count(&self) -> u32 {
self.corpus.values().sum()
}
}
/// Искаме да можем да напечатаме един `WordCounter` с цел дебъгване.
///
/// - Първи ред: `WordCounter, total count: {}`, форматирано с `total_count`.
/// - Останалите редове: Всяка една дума, изчистена както е описано горе с `add`, с брой на
/// срещането ѝ, примерно: "foo: 13"
///
/// Всеки ред се очаква да завършва с `\n`, включително последния. Думите трябва да са сортирани по
/// брой на срещанията, най-честите трябва да са първи. Примерно:
///
/// WordCounter, total count: 25
/// foo: 13
/// bar: 12
///
impl std::fmt::Display for WordCounter {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "WordCounter, total count: {}\n", self.total_count())?;
let mut entries: Vec<(&String, &u32)> = self.corpus.iter().collect();
entries.sort_unstable_by(|a, b| b.1.cmp(a.1));
for entry in entries {
write!(f, "{}: {}\n", entry.0, entry.1)?;
}
Ok(())
}
}
use std::collections::HashSet;
/// Тези две константи са за удобство -- ще ги използваме в тестовете, свободни сте да ги
/// използвате във вашите.
pub const ALPHABET_EN: &'static str = "abcdefghijklmnopqrstuvwxyz";
pub const ALPHABET_BG: &'static str = "абвгдежзийклмнопрстуфхцчшщъьюя";
pub struct SpellChecker {
corpus: WordCounter,
alphabet: String,
}
impl SpellChecker {
/// Създава нов SpellChecker с дадените параметри:
///
/// - corpus: големия текст, който ще се използва за проверяване на познати думи и тяхната
/// вероятност
/// - alphabet: буквите, които ще добавяме или заместваме, за да получим нови потенциални думи.
/// Примерно, да spell-check-ваме български или английски изисква различни азбуки.
///
pub fn new(corpus: &str, alphabet: &str) -> Self {
Self{corpus: WordCounter::from_str(corpus), alphabet: alphabet.to_string()}
}
/// Най-вероятната поправка на тази дума. Както описахме по-горе:
///
/// - Позната ли е тази дума? Ако да, направо я връщаме, валидна е.
/// - Пробваме всички възможни други думи на една буква разлика. Познати ли са някои от тези
/// думи? Ако да, връщаме тази, която се среща най-често в корпуса.
/// - Пробваме всички възможни други думи на две букви разлика по същия начин.
/// - В краен случай, връщаме оригиналната дума, не знаем как да я коригираме.
///
/// Очакваме trim + downcase на входа, тоест
/// `spell_checker.correction(" Foo ")` е еквивалентно на
/// `spell_checker.correction("foo")`
///
/// (Бележка: Би имало смисъл това да е единствения публичен метод -- всички по-надолу биха
/// могли да бъдат private API което се използва от този метод, но искаме да ти тестваме в
/// отделен файл, so here we are.)
///
pub fn correction(&self, word: &str) -> String {
self.candidates(word)[0].clone()
}
/// Каква е вероятността тази дума да се срещне в оригиналния текст? Броя срещания на тази
/// дума, разделен на броя думи в текста.
///
pub fn probability(&self, word: &str) -> f64 {
let occurances = self.corpus.get(word);
let total = self.corpus.total_count();
occurances as f64 / total as f64
}
/// Кои думи от този Set са познати (присъстват в подадения корпус)?
///
pub fn known<'a> (&self, words: &'a HashSet<String>) -> Vec<&'a String> {
words.into_iter().filter(|w| self.corpus.get(w) > 0).collect()
}
/// Всички познати кандидати за поправка на тази дума:
///
/// - Ако думата е позната, директно връщаме вектор с нея.
/// - Намираме познатите edits1 на тази дума -- ако има такива, връщаме ги.
/// - Намираме познатите edits2 на тази дума -- ако има такива, връщаме ги.
/// - Иначе, връщаме вектор с думата.
///
pub fn candidates(&self, word: &str) -> Vec<String> {
if self.corpus.get(word) > 0 {
vec![word.to_string()]
} else {
let p = self.edits1(word);
let candidates_1 = self.known(&p);
if !candidates_1.is_empty() {
candidates_1.iter().map(|&s| s.clone()).collect()
} else {
let q = self.edits2(word);
let candidates_2 = self.known(&q);
if !candidates_2.is_empty() {
candidates_2.iter().map(|&s| s.clone()).collect()
} else {
vec![word.to_string()]
}
}
}
}
/// Всички думи, които са на една промяна разстояние от дадената дума:
///
/// - Една буква изтрита на коя да е позиция
/// - Две букви разменени (една до друга)
/// - Една буква от азбуката замества коя да е буква от думата
/// - Една буква от азбуката добавена в думата на която и да е позиция
///
/// Изхода е HashSet, понеже две различни промени е възможно да продуцират един и същ резултат,
/// а дубликати не ни интересуват.
///
pub fn edits1(&self, word: &str) -> HashSet<String> {
let mut result = HashSet::new();
for i in 0.. word.len() {
let (first, last) = word.split_at(i);
result.insert([first, &last[1..]].concat());
}
for i in 0 .. word.len() - 1 {
let (first, last) = word.split_at(i);
result.insert([first, &last[1..2], &last[..1], &last[2..]].concat());
}
for i in 0 .. word.len() + 1 {
for c in self.alphabet.chars() {
let (first, last) = word.split_at(i);
let mut buffer = [0; 1];
let res = c.encode_utf8(&mut buffer);
result.insert([first, res, last].concat());
}
}
for i in 0 .. word.len() {
for c in self.alphabet.chars() {
let (first, last) = word.split_at(i);
let mut buffer = [0; 1];
let res = c.encode_utf8(&mut buffer);
result.insert([first, res, &last[1..]].concat());
}
}
result
}
/// Всички думи, които са на две промени разстояние от дадената дума. Вижте инструкциите на
/// edits1 за това какво е "промяна" и направете тези промени по променените веднъж думи.
///
pub fn edits2(&self, word: &str) -> HashSet<String> {
self.edits1(word).iter().flat_map(|word_edit_1| self.edits1(word_edit_1)).collect()
}
}
Лог от изпълнението
Compiling solution v0.1.0 (/tmp/d20200114-2173579-7cihuf/solution) Finished test [unoptimized + debuginfo] target(s) in 4.64s Running target/debug/deps/solution-a73e64ec87929bd0 running 1 test test tests::it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Running target/debug/deps/solution_test-38971695424b36d5 running 15 tests test solution_test::test_best_word_is_returned ... FAILED test solution_test::test_clean_line_removes_punctuation ... ok test solution_test::test_clean_line_trims_the_input ... ok test solution_test::test_correction ... FAILED test solution_test::test_correction_fails_to_produce_new_result ... FAILED test solution_test::test_correction_normalizes_case ... FAILED test solution_test::test_counting ... ok test solution_test::test_display ... ok test solution_test::test_edits1 ... FAILED test solution_test::test_edits2 ... FAILED test solution_test::test_empty_counter ... ok test solution_test::test_from_empty_str ... ok test solution_test::test_from_str ... ok test solution_test::test_known_words ... ok test solution_test::test_probability ... ok failures: ---- solution_test::test_best_word_is_returned stdout ---- thread 'main' panicked at 'assertion failed: `(left == right)` left: `"boat"`, right: `"boot"`', tests/solution_test.rs:213:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace. ---- solution_test::test_correction stdout ---- thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'л' (bytes 0..2) of `либоф`', src/libcore/str/mod.rs:2068:5 ---- solution_test::test_correction_fails_to_produce_new_result stdout ---- thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'Л' (bytes 0..2) of `Либоф`', src/libcore/str/mod.rs:2068:5 ---- solution_test::test_correction_normalizes_case stdout ---- thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'Л' (bytes 0..2) of `Либоф`', src/libcore/str/mod.rs:2068:5 ---- solution_test::test_edits1 stdout ---- thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'т' (bytes 0..2) of `три`', src/libcore/str/mod.rs:2068:5 ---- solution_test::test_edits2 stdout ---- thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'т' (bytes 0..2) of `три`', src/libcore/str/mod.rs:2068:5 failures: solution_test::test_best_word_is_returned solution_test::test_correction solution_test::test_correction_fails_to_produce_new_result solution_test::test_correction_normalizes_case solution_test::test_edits1 solution_test::test_edits2 test result: FAILED. 9 passed; 6 failed; 0 ignored; 0 measured; 0 filtered out error: test failed, to rerun pass '--test solution_test'