14. Terminal project: a word-frequency counter
The same project as C §9.2.
Compare the two: the Rust version has no hand-written hash table, no
strdup, no free, and no fixed-size buffer.
// src/bin/wordfreq.rs
// usage: cargo run --bin wordfreq -- [FILE] [-n TOP] (reads stdin with no FILE)
use std::collections::HashMap;
use std::io::{self, BufRead, BufReader, Read};
fn count_words(reader: impl BufRead) -> io::Result<HashMap<String, usize>> {
let mut counts = HashMap::new();
for line in reader.lines() {
let line = line?; // propagate I/O and invalid-UTF-8 errors
for word in line
.split(|c: char| !c.is_alphanumeric() && c != '\'')
.filter(|w| !w.is_empty())
{
*counts.entry(word.to_lowercase()).or_insert(0) += 1;
}
}
Ok(counts)
}
fn main() -> io::Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
let top = args.iter().position(|a| a == "-n")
.and_then(|i| args.get(i + 1))
.and_then(|n| n.parse().ok())
.unwrap_or(10);
let path = args.iter().find(|a| !a.starts_with('-') && a.parse::<usize>().is_err());
let input: Box<dyn Read> = match path {
Some(p) => Box::new(std::fs::File::open(p)?),
None => Box::new(io::stdin()),
};
let counts = count_words(BufReader::new(input))?;
let mut ranked: Vec<(&String, &usize)> = counts.iter().collect();
ranked.sort_unstable_by(|a, b| b.1.cmp(a.1).then_with(|| a.0.cmp(b.0)));
for (word, n) in ranked.into_iter().take(top) {
println!("{n:>7} {word}");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn counts_case_insensitively() {
let c = count_words("The cat. the CAT's hat!".as_bytes()).unwrap();
assert_eq!(c["the"], 2);
assert_eq!(c["cat's"], 1);
assert_eq!(c.get("dog"), None);
}
}
cargo run --release --bin wordfreq -- -n 5 /usr/share/dict/words
cargo test --bin wordfreq
Stretch goals:
- Avoid allocating a
Stringper word. Count&strslices of a buffer that holds the whole file. You will meet lifetimes (Ch 19), because the map now borrows from the buffer. - Process files in parallel with scoped threads (Ch 28) and merge the maps.
- Compare
HashMapwithBTreeMaptimings on a 100 MB file.