I have written this program to find anagrams in a "pile" of letters (it'd be an effective way to cheat at Scrabble...)
Run it like anagram abcedeede
to get a list of all the words that can be made from the letters abcedeede
(you might want to pipe to head
, since there will be many).
This is my first "major" attempt to write something in Rust--it is an adaptation of a Python program I wrote a while ago. I would love comments both on style and on any "stupid"/non-idiomatic things I've done that lead to problems in Rust.
// main.rs
#![feature(test)]
extern crate test;
use std::collections::HashMap;
use std::env;
use std::fs::File;
use std::io;
use std::io::prelude::*;
use std::io::BufReader;
/// A struct containing a string "word" and its "hash," a map between characters and counts
pub struct CountedWord {
/// The "pretty" version of the word that has been counted
word: String,
/// A map of characters to counts of characters for the word
letter_counter: HashMap<char, u32>,
}
impl CountedWord {
/// Create a `CountedWord` from an input `&str`
pub fn new(word: &str) -> CountedWord {
CountedWord {
word: word.to_string(),
letter_counter: CountedWord::hash(word),
}
}
/// Compute the "hash" for the input word
///
/// The "hash" is the count of each of the letters in the word.
/// i.e. for "attack" the hash is "2 a's, 1 c, 1 k, and 2 t's"
fn hash(word: &str) -> HashMap<char, u32> {
let clean_word = word.trim().to_lowercase();
let mut letter_counter = HashMap::new();
for ch in clean_word.chars().filter(|ch| ch.is_alphabetic()) {
letter_counter
.entry(ch)
.and_modify(|e| *e += 1)
.or_insert(1);
}
letter_counter
}
/// Determine if the other `CountedWord` can be made from the letters in `self`.
/// (That is--is `self` an anagram of `other`, with some letters allowed to be left over in
/// `self`?)
///
/// Returns `true` if it can be made, `false` otherwise.
pub fn is_possibility(&self, other: &CountedWord) -> bool {
let mut iter_other_chars = other.letter_counter.iter();
loop {
let item = iter_other_chars.next();
match item {
Some((character, number_in_other)) => {
let number_in_self = self.letter_counter.get(character);
match number_in_self {
Some(number_in_self) => {
if number_in_self >= number_in_other {
// this letter doesn't rule it out
continue;
} else {
// we don't have enough of this letter, not a possibility
break false;
}
}
// we don't have this letter, not a possiblity
None => break false,
}
}
// we didn't fail above, so this word must be a possibility
None => break true,
}
}
}
}
fn main() {
// process all the words in the dictionary
let f = File::open("/usr/share/dict/words").expect("dictionary not found");
let lines = BufReader::new(f).lines();
let mut dictionary_words = Vec::new();
for word in lines {
match word {
Ok(word) => {
dictionary_words.push(CountedWord::new(&word));
}
Err(_e) => continue,
};
}
// process the "pile" of letters we have to anagram from the arguments
// spaces are ignored--everything except the program name is collected here
let pile = CountedWord::new(&env::args().collect::<Vec<String>>()[1..].join(""));
// determine what words from the dictionary can be made from the letters in the pile
let mut possibilities = Vec::new();
for word in dictionary_words.iter() {
if pile.is_possibility(&word) {
possibilities.push(word)
}
}
// output the possiblities in alphabetical order by length
let mut stdout = io::stdout();
possibilities.sort_unstable_by_key(|k| (-(k.word.len() as i32), k.word.to_lowercase()));
for possibility in possibilities {
// using `if let` and `writeln!` here so if you pipe to (e.g.) head, we can catch the panic when the pipe is closed early
if let Err(_) = writeln!(stdout, "{}", possibility.word) {
break;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use test::Bencher;
/// Test `CountedWord::hash`
#[test]
fn test_counted_word_hash() {
let counter = CountedWord::hash("test");
let mut test_counter = HashMap::new();
test_counter.insert('t', 2);
test_counter.insert('e', 1);
test_counter.insert('s', 1);
assert_eq!(counter, test_counter);
}
/// Test `CountedWord::new`
#[test]
fn test_create_counted_word() {
let test_word = CountedWord::new("test");
assert_eq!(test_word.word, "test");
let mut test_counter = HashMap::new();
test_counter.insert('t', 2);
test_counter.insert('e', 1);
test_counter.insert('s', 1);
assert_eq!(test_word.letter_counter, test_counter);
}
/// Test `CountedWord::is_possibility`
#[test]
fn test_counted_word_is_possiblity() {
let test_word_1 = CountedWord::new("test");
let test_word_2 = CountedWord::new("assessment");
let input_letters = CountedWord::new("tssettioupuqwerwe");
assert!(input_letters.is_possibility(&test_word_1));
assert!(!input_letters.is_possibility(&test_word_2));
}
/// Benchmark `CountedWord::hash`
#[bench]
fn bench_hashing_word(b: &mut Bencher) {
b.iter(|| CountedWord::hash("tssettioupuqwerwe"));
}
/// Benchmark `CountedWord::is_possibility` when `is_possibility` is true
#[bench]
fn bench_checking_possibility_true(b: &mut Bencher) {
let test_word = CountedWord::new("test");
let input_letters = CountedWord::new("tssettioupuqwerwe");
b.iter(|| input_letters.is_possibility(&test_word));
}
/// Benchmark `CountedWord::is_possibility` when `is_possibility` is false
#[bench]
fn bench_checking_possibility_false(b: &mut Bencher) {
let test_word = CountedWord::new("assessment");
let input_letters = CountedWord::new("tssettioupuqwerwe");
b.iter(|| input_letters.is_possibility(&test_word));
}
}