use std::collections::HashMap;
use std::fs::File;
use std::io::{BufRead, BufReader, Result};
use std::path::Path;
#[derive(Debug, Clone)]
pub struct BigTxtCorpus {
pub frequencies: HashMap<String, usize>,
pub total: usize,
}
impl BigTxtCorpus {
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut frequencies = HashMap::new();
let mut total = 0;
for line in reader.lines() {
let line = line?;
let word = line.trim().to_lowercase();
if !word.is_empty() {
*frequencies.entry(word).or_insert(0) += 1;
total += 1;
}
}
Ok(Self { frequencies, total })
}
#[inline]
pub fn unique_words(&self) -> usize {
self.frequencies.len()
}
#[inline]
pub fn total_tokens(&self) -> usize {
self.total
}
#[inline]
pub fn frequency(&self, word: &str) -> usize {
self.frequencies.get(word).copied().unwrap_or(0)
}
pub fn words_by_frequency(&self) -> Vec<(&str, usize)> {
let mut words: Vec<_> = self
.frequencies
.iter()
.map(|(w, &f)| (w.as_str(), f))
.collect();
words.sort_unstable_by(|a, b| b.1.cmp(&a.1));
words
}
pub fn words_sorted(&self) -> Vec<&str> {
let mut words: Vec<_> = self.frequencies.keys().map(|s| s.as_str()).collect();
words.sort_unstable();
words
}
}
#[derive(Debug, Clone)]
pub struct MittonCorpus {
pub errors: HashMap<String, Vec<(String, usize)>>,
}
impl MittonCorpus {
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut errors: HashMap<String, Vec<(String, usize)>> = HashMap::new();
let mut current_correct: Option<String> = None;
for line in reader.lines() {
let line = line?;
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if let Some(correct) = trimmed.strip_prefix('$') {
current_correct = Some(correct.to_string());
errors.entry(correct.to_string()).or_default();
} else if let Some(correct) = ¤t_correct {
let parts: Vec<&str> = trimmed.split_whitespace().collect();
if parts.is_empty() {
continue;
}
let misspelling = parts[0].to_string();
let frequency = if parts.len() > 1 {
parts[1].parse().unwrap_or(1)
} else {
1
};
errors
.entry(correct.clone())
.or_default()
.push((misspelling, frequency));
}
}
Ok(Self { errors })
}
#[inline]
pub fn num_correct_words(&self) -> usize {
self.errors.len()
}
pub fn total_misspellings(&self) -> usize {
self.errors
.values()
.flat_map(|v| v.iter().map(|(_, freq)| freq))
.sum()
}
pub fn unique_misspellings(&self) -> usize {
self.errors.values().map(|v| v.len()).sum()
}
pub fn correct_words_sorted(&self) -> Vec<&str> {
let mut words: Vec<_> = self.errors.keys().map(|s| s.as_str()).collect();
words.sort_unstable();
words
}
pub fn all_errors(&self) -> Vec<(&str, &str, usize)> {
self.errors
.iter()
.flat_map(|(correct, misspellings)| {
misspellings
.iter()
.map(move |(misspelling, freq)| (misspelling.as_str(), correct.as_str(), *freq))
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_big_txt_corpus() {
let mut file = NamedTempFile::new().expect("test fixture: tempfile must be Ok");
writeln!(file, "the").expect("test fixture: write must succeed");
writeln!(file, "the").expect("test fixture: write must succeed");
writeln!(file, "the").expect("test fixture: write must succeed");
writeln!(file, "quick").expect("test fixture: write must succeed");
writeln!(file, "brown").expect("test fixture: write must succeed");
writeln!(file, "").expect("test fixture: write must succeed"); file.flush().expect("test fixture: flush must succeed");
let corpus = BigTxtCorpus::load(file.path()).expect("test fixture: load must be Ok");
assert_eq!(corpus.unique_words(), 3);
assert_eq!(corpus.total_tokens(), 5);
assert_eq!(corpus.frequency("the"), 3);
assert_eq!(corpus.frequency("quick"), 1);
assert_eq!(corpus.frequency("missing"), 0);
let by_freq = corpus.words_by_frequency();
assert_eq!(by_freq[0], ("the", 3));
}
#[test]
fn test_mitton_corpus() {
let mut file = NamedTempFile::new().expect("test fixture: tempfile must be Ok");
writeln!(file, "$hello").expect("test fixture: write must succeed");
writeln!(file, "helo 2").expect("test fixture: write must succeed");
writeln!(file, "hllo 1").expect("test fixture: write must succeed");
writeln!(file, "").expect("test fixture: write must succeed"); writeln!(file, "$world").expect("test fixture: write must succeed");
writeln!(file, "wrld").expect("test fixture: write must succeed"); file.flush().expect("test fixture: flush must succeed");
let corpus = MittonCorpus::load(file.path()).expect("test fixture: load must be Ok");
assert_eq!(corpus.num_correct_words(), 2);
assert_eq!(corpus.unique_misspellings(), 3);
assert_eq!(corpus.total_misspellings(), 4);
let hello_errors = &corpus.errors["hello"];
assert_eq!(hello_errors.len(), 2);
assert!(hello_errors.contains(&("helo".to_string(), 2)));
assert!(hello_errors.contains(&("hllo".to_string(), 1)));
let world_errors = &corpus.errors["world"];
assert_eq!(world_errors.len(), 1);
assert!(world_errors.contains(&("wrld".to_string(), 1)));
let all = corpus.all_errors();
assert_eq!(all.len(), 3);
}
#[test]
fn test_mitton_corpus_missing_frequency() {
let mut file = NamedTempFile::new().expect("test fixture: tempfile must be Ok");
writeln!(file, "$test").expect("test fixture: write must succeed");
writeln!(file, "tset").expect("test fixture: write must succeed");
file.flush().expect("test fixture: flush must succeed");
let corpus = MittonCorpus::load(file.path()).expect("test fixture: load must be Ok");
let errors = &corpus.errors["test"];
assert_eq!(errors.len(), 1);
assert_eq!(errors[0], ("tset".to_string(), 1));
}
}