diff --git a/Cargo.lock b/Cargo.lock index a9c8a1d..589abdd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 3 [[package]] name = "anabot" -version = "1.0.0" +version = "1.0.1" dependencies = [ "anagrambot", "clap", @@ -12,7 +12,7 @@ dependencies = [ [[package]] name = "anagrambot" -version = "1.0.0" +version = "1.0.1" [[package]] name = "atty" diff --git a/anabot/Cargo.toml b/anabot/Cargo.toml index eefac5c..3073d34 100644 --- a/anabot/Cargo.toml +++ b/anabot/Cargo.toml @@ -7,11 +7,11 @@ readme = "README.md" repository = "https://github.com/generic-user1/anagrambot" keywords = ["anagram", "anagrams", "anagrambot"] categories = ["command-line-utilities", "text-processing"] -version = "1.0.0" +version = "1.0.1" edition = "2021" [dependencies] -anagrambot = {path = "../anagrambot", version = "1.0.0"} +anagrambot = {path = "../anagrambot", version = "1.0.1"} clap = {version = "3.2.12", features = ["derive"]} \ No newline at end of file diff --git a/anabot/src/arg.rs b/anabot/src/arg.rs index 7749d06..bb4ff87 100644 --- a/anabot/src/arg.rs +++ b/anabot/src/arg.rs @@ -1,29 +1,26 @@ -use clap::{Parser, clap_derive::ArgEnum, Subcommand}; +use clap::{clap_derive::ArgEnum, Parser, Subcommand}; -#[derive(Debug, Copy, Clone, PartialEq, ArgEnum)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, ArgEnum)] pub enum AnagramType { Standard, Proper, Loose } -#[derive(Debug, PartialEq, Subcommand)] +#[derive(Debug, PartialEq, Eq, Subcommand)] pub enum ActionType { /// Test if two words are anagrams - Test { - word_a: String, - word_b: String - }, + Test { word_a: String, word_b: String }, /// Find and print anagrams for a word, up to a given limit Find { - word: String, + word: String, /// The maximum number of anagrams to find - /// + /// /// The actual number of anagrams found may be under this limit, but never above. #[clap(short, long, default_value_t = 100)] limit: usize, /// The minimum length of each sub-word (only used with loose anagrams) - /// + /// /// For example, with this set to 3, no 1 or 2 letter words will appear in the results. #[clap(short, long, default_value_t = 1)] min_word_length: usize @@ -33,18 +30,17 @@ pub enum ActionType { #[derive(Parser, Debug)] #[clap(author, version, about, long_about = None)] pub struct CliArgs { - #[clap(short = 'i', long)] /// Ignore case when testing or finding anagrams pub case_insensitive: bool, /// Type of anagrams to search for - /// + /// /// `standard`: every letter in word A appears in word B the same number of times. - /// - /// `proper`: word A and word B both appear in the word list and are + /// + /// `proper`: word A and word B both appear in the word list and are /// standard anagrams of each other (requires a word list) - /// + /// /// `loose`: word A and word B are proper anagrams but may have a different number of /// spaces. For example, "racecar" and "arc care" are loose anagrams but not proper anagrams /// (requires a word list) @@ -64,5 +60,4 @@ pub struct CliArgs { #[clap(subcommand)] pub action: ActionType - -} \ No newline at end of file +} diff --git a/anabot/src/main.rs b/anabot/src/main.rs index fe097ae..71986c9 100644 --- a/anabot/src/main.rs +++ b/anabot/src/main.rs @@ -1,10 +1,13 @@ -use anagrambot::{wordlist::{OwnedWordList, Wordlist}, anagram, default_wordlist}; +use anagrambot::{ + anagram, default_wordlist, + wordlist::{OwnedWordList, Wordlist} +}; use clap::Parser; use std::path::Path; mod arg; -use arg::{CliArgs, AnagramType, ActionType}; +use arg::{ActionType, AnagramType, CliArgs}; const REASON_DUPLICATES: &str = "a word cannot be an anagram of itself"; const REASON_FIRST_NOT_WORD: &str = "first provided word is not a valid word"; @@ -17,18 +20,21 @@ fn main() -> Result<(), String> { } /// main arg handling function -/// +/// /// includes full handling for standard anagrams and delegates other types of anagrams to do_action -fn handle_args(args: CliArgs) -> Result<(), String> -{ +fn handle_args(args: CliArgs) -> Result<(), String> { // handle Standard first, as it requires no wordlist and thus no wordlist handling - if &args.anagram_type == &AnagramType::Standard { + if args.anagram_type == AnagramType::Standard { match &args.action { - ActionType::Find{word, limit, min_word_length:_} => { + ActionType::Find { + word, + limit, + min_word_length: _ + } => { let limit = *limit; - let mut iter = anagram::find_anagrams(word); + let iter = anagram::find_anagrams(word); let mut index: usize = 0; - while let Some(word) = iter.next(){ + for word in iter { if index >= limit { break; } @@ -36,27 +42,25 @@ fn handle_args(args: CliArgs) -> Result<(), String> index += 1; } - if !args.simple_output{ + if !args.simple_output { println!("found {} standard anagrams", index); } - }, - ActionType::Test {word_a, word_b } => { - if anagram::are_anagrams(word_a, word_b, !args.case_insensitive){ + } + ActionType::Test { word_a, word_b } => { + if anagram::are_anagrams(word_a, word_b, !args.case_insensitive) { if args.simple_output { println!("true") } else { println!("\"{}\" is standard anagram of \"{}\"", word_a, word_b); } + } else if args.simple_output { + println!("false"); } else { - if args.simple_output{ - println!("false"); + println!("\"{}\" is not standard anagram of \"{}\"", word_a, word_b); + if word_a == word_b { + println!("Reason: {}", REASON_DUPLICATES); } else { - println!("\"{}\" is not standard anagram of \"{}\"", word_a, word_b); - if word_a == word_b { - println!("Reason: {}", REASON_DUPLICATES); - } else { - println!("Reason: {}", REASON_CHARS_DIFFERENT); - } + println!("Reason: {}", REASON_CHARS_DIFFERENT); } } } @@ -66,9 +70,7 @@ fn handle_args(args: CliArgs) -> Result<(), String> // if this fails, return Err(message) // if this succeeds, call do_action to perform whatever action if let Some(wordlist_path) = &args.wordlist_path { - let wordlist = match OwnedWordList::from_file( - &Path::new(wordlist_path)) - { + let wordlist = match OwnedWordList::from_file(Path::new(wordlist_path)) { Ok(wordlist) => wordlist, Err(_) => { return Err(format!("Failed to read word list file {}", wordlist_path)); @@ -80,9 +82,9 @@ fn handle_args(args: CliArgs) -> Result<(), String> let wordlist = match default_wordlist::default_wordlist() { Some(wordlist) => wordlist, None => { - let errmsg = String::from("No word list was provided, ") + - "but no default wordlist could be found. Please provide a word list " + - "file (text file, one word per line) using the `-w` option"; + let errmsg = String::from("No word list was provided, ") + + "but no default wordlist could be found. Please provide a word list " + + "file (text file, one word per line) using the `-w` option"; return Err(errmsg); } }; @@ -94,30 +96,33 @@ fn handle_args(args: CliArgs) -> Result<(), String> } /// used to handle actions involving a wordlist in a common manner independant of wordlist type -/// -/// called after a wordlist is determined to be needed and has been successfully resolved. -/// +/// +/// called after a wordlist is determined to be needed and has been successfully resolved. +/// ///# Panics -/// +/// /// this function panics if args.anagram_type is `Standard`, as this is meant to be handled /// before this function is called (due to the lack of requirement of a wordlist) -fn do_action<'a>(args: &CliArgs, wordlist: &'a impl Wordlist<'a>) -{ +fn do_action<'a>(args: &CliArgs, wordlist: &'a impl Wordlist<'a>) { const PANIC_MSG: &str = "Logic Error! Used do_action for standard anagram"; - + let case_sensitive = !args.case_insensitive; match &args.action { - ActionType::Test {word_a, word_b } => { + ActionType::Test { word_a, word_b } => { let (are_anagrams, anagram_name) = match &args.anagram_type { AnagramType::Standard => panic!("{}", PANIC_MSG), AnagramType::Proper => { - let are_anagrams = - anagram::are_proper_anagrams(&word_a, &word_b, wordlist, case_sensitive); + let are_anagrams = + anagram::are_proper_anagrams(word_a, word_b, wordlist, case_sensitive); (are_anagrams, "proper") - }, + } AnagramType::Loose => { - let are_anagrams = - anagram::are_loose_anagrams_strict(&word_a, &word_b, wordlist, case_sensitive); + let are_anagrams = anagram::are_loose_anagrams_strict( + word_a, + word_b, + wordlist, + case_sensitive + ); (are_anagrams, "loose") } }; @@ -128,33 +133,45 @@ fn do_action<'a>(args: &CliArgs, wordlist: &'a impl Wordlist<'a>) } else { println!("false"); } + } else if are_anagrams { + println!( + "\"{}\" is {} anagram of \"{}\"", + word_a, anagram_name, word_b + ); } else { - if are_anagrams{ - println!("\"{}\" is {} anagram of \"{}\"",word_a, anagram_name, word_b); + println!( + "\"{}\" is not {} anagram of \"{}\"", + word_a, anagram_name, word_b + ); + if word_a == word_b { + println!("Reason: {}", REASON_DUPLICATES); } else { - println!("\"{}\" is not {} anagram of \"{}\"",word_a, anagram_name, word_b); - if word_a == word_b { - println!("Reason: {}", REASON_DUPLICATES); - } else { - let word_a_real = wordlist.includes_word(&word_a); - let word_b_real = wordlist.includes_word(&word_b); - if !word_a_real { - println!("Reason: {}", REASON_FIRST_NOT_WORD); - } - if !word_b_real { - println!("Reason: {}", REASON_SECOND_NOT_WORD); - } - if word_a_real && word_b_real { - println!("Reason: {}", REASON_CHARS_DIFFERENT); - } - } + let word_a_real = wordlist.includes_word(word_a); + let word_b_real = wordlist.includes_word(word_b); + if !word_a_real { + println!("Reason: {}", REASON_FIRST_NOT_WORD); + } + if !word_b_real { + println!("Reason: {}", REASON_SECOND_NOT_WORD); + } + if word_a_real && word_b_real { + println!("Reason: {}", REASON_CHARS_DIFFERENT); + } } } - }, - ActionType::Find { word, limit, min_word_length} => { - fn print_fn(args: &CliArgs, mut iter: impl Iterator, limit: usize) { + } + ActionType::Find { + word, + limit, + min_word_length + } => { + fn print_fn( + args: &CliArgs, + iter: impl Iterator, + limit: usize + ) { let mut index: usize = 0; - while let Some(word) = iter.next(){ + for word in iter { if index >= limit { break; } @@ -162,8 +179,8 @@ fn do_action<'a>(args: &CliArgs, wordlist: &'a impl Wordlist<'a>) index += 1; } - if !args.simple_output{ - let anagram_type = match args.anagram_type{ + if !args.simple_output { + let anagram_type = match args.anagram_type { AnagramType::Standard => panic!("{}", PANIC_MSG), AnagramType::Proper => "proper", AnagramType::Loose => "loose" @@ -174,10 +191,23 @@ fn do_action<'a>(args: &CliArgs, wordlist: &'a impl Wordlist<'a>) match &args.anagram_type { AnagramType::Standard => panic!("{}", PANIC_MSG), AnagramType::Proper => { - print_fn(&args, anagram::find_proper_anagrams(&word, wordlist, case_sensitive), *limit); - }, + print_fn( + args, + anagram::find_proper_anagrams(word, wordlist, case_sensitive), + *limit + ); + } AnagramType::Loose => { - print_fn(&args, anagram::find_loose_anagrams(&word, wordlist, *min_word_length, case_sensitive), *limit); + print_fn( + args, + anagram::find_loose_anagrams( + word, + wordlist, + *min_word_length, + case_sensitive + ), + *limit + ); } } } diff --git a/anagrambot/Cargo.toml b/anagrambot/Cargo.toml index 378bd8c..9f4903c 100644 --- a/anagrambot/Cargo.toml +++ b/anagrambot/Cargo.toml @@ -7,7 +7,7 @@ readme = "README.md" repository = "https://github.com/generic-user1/anagrambot" keywords = ["anagram", "anagrams"] categories = ["text-processing"] -version = "1.0.0" +version = "1.0.1" edition = "2021" [features] diff --git a/anagrambot/src/anagram.rs b/anagrambot/src/anagram.rs index 37f4d4e..7367ec8 100644 --- a/anagrambot/src/anagram.rs +++ b/anagrambot/src/anagram.rs @@ -1,19 +1,19 @@ //! Utilities for finding and verifying anagrams -//! +//! //! For the purposes of this module, an anagram is defined as such: //! two strings are "anagrams" of each other if they both contain the same characters, //! but in a different order. -//! +//! //! For example: //! - "race" and "care" are anagrams, because they contain the same characters and are not identical //! - "race" and "race" are not anagrams, because words cannot be anagrams of themselves //! - "asdf" and "safd" are anagrams, because they contain the same characters and are not identical -//! +//! //! This last point introduces the need for a more strict form of anagram, which this //! module calls a "proper anagram": proper anagrams must be actual words. That is, //! anagrams can only be considered proper anagrams if they appear in a list of defined words. //! Each proper anagram related function requires a [Wordlist] to check if a given string is a word. -//! +//! //! For example: //! - "race" and "care" are proper anagrams because they are anagrams and both words //! - "race" and "reca" are not proper anagrams because "reca" is not a word @@ -23,7 +23,7 @@ use crate::wordlist::Wordlist; use std::collections::BTreeMap; /// Type representing the set of characters a word contains -/// +/// /// Each key is a character that appears in the word. Each value /// is the number of times that character appears type Charmap = BTreeMap; @@ -32,35 +32,32 @@ pub mod loose_anagram; pub use loose_anagram::{are_loose_anagrams, are_loose_anagrams_strict, find_loose_anagrams}; /// Returns a [Charmap] with the number of times each character appears in `word` -/// +/// /// The resulting [Charmap] has a key for each character in `word`, with the value /// being the number of times that character appears in `word` -/// +/// /// If `ignore_spaces` is true, space characters `' '` will be entirely skipped over -/// +/// /// If `case_sensitive` is true, characters of different case will be treated as different. /// If `case_sensitive` is false, characters of different case will be treated as the same. -fn get_charcount_map(word: &str, ignore_spaces: bool, case_sensitive: bool) -> Charmap -{ +fn get_charcount_map(word: &str, ignore_spaces: bool, case_sensitive: bool) -> Charmap { let mut lettercount_map = Charmap::new(); - let mut insert_closure = |letter|{ - match lettercount_map.get_mut(&letter) { - None => {lettercount_map.insert(letter, 1);}, - Some(count) => {*count+=1} + let mut insert_closure = |letter| match lettercount_map.get_mut(&letter) { + None => { + lettercount_map.insert(letter, 1); } + Some(count) => *count += 1 }; - for letter in word.chars(){ - if ignore_spaces && letter == ' '{ + for letter in word.chars() { + if ignore_spaces && letter == ' ' { continue; + } else if case_sensitive { + insert_closure(letter); } else { - if case_sensitive{ - insert_closure(letter); - } else { - for lower_letter in letter.to_lowercase(){ - insert_closure(lower_letter); - } + for lower_letter in letter.to_lowercase() { + insert_closure(lower_letter); } } } @@ -69,23 +66,24 @@ fn get_charcount_map(word: &str, ignore_spaces: bool, case_sensitive: bool) -> C } /// Caching object for word charmaps, do not use directly -struct WordWithCharmap<'a>{ +struct WordWithCharmap<'a> { word: &'a str, word_charmap: Option, case_sensitive: bool } -impl<'a> WordWithCharmap<'a>{ - pub fn new(word: &'a str, case_sensitive: bool) -> Self - { - Self{word, word_charmap: None, case_sensitive} +impl<'a> WordWithCharmap<'a> { + pub fn new(word: &'a str, case_sensitive: bool) -> Self { + Self { + word, + word_charmap: None, + case_sensitive + } } - pub fn get_word(&self) -> &'a str - { - return self.word + pub fn get_word(&self) -> &'a str { + self.word } - pub fn get_charmap(&mut self) -> &Charmap - { + pub fn get_charmap(&mut self) -> &Charmap { if self.word_charmap == None { self.word_charmap = Some(get_charcount_map(self.word, false, self.case_sensitive)); } @@ -95,53 +93,50 @@ impl<'a> WordWithCharmap<'a>{ } /// Returns true if two words are anagrams -/// +/// /// `word_a` and `word_b` are the two words to check -/// +/// /// If `case_sensitive` is `true`, uppercase and lowercase forms of the /// same letter will be considered different. If `case_sensitive` is `false`, /// uppercase and lowercase forms of th same letter will be considered the same. -/// +/// /// This tests for standard anagrams, not proper anagrams. This means /// that non-word character sequences that nonetheless contain the same /// characters in a different order will result in `true` -/// +/// /// Note that two identical words are not considered anagrams -/// +/// ///# Examples /// ``` /// use anagrambot::anagram::are_anagrams; -/// +/// /// const CASE_SENSITIVE: bool = true; -/// +/// /// //proper anagram /// assert!(are_anagrams("race", "care", CASE_SENSITIVE)); /// //non-proper anagram /// assert!(are_anagrams("aabc", "caab", CASE_SENSITIVE)); -/// +/// /// //non-anagram due to different letters /// assert!(!are_anagrams("race", "cow", CASE_SENSITIVE)); /// //non-anagram due to being identical /// assert!(!are_anagrams("race", "race", CASE_SENSITIVE)); /// ``` -pub fn are_anagrams(word_a: &str, word_b: &str, case_sensitive: bool) -> bool -{ +pub fn are_anagrams(word_a: &str, word_b: &str, case_sensitive: bool) -> bool { let mut word_a = WordWithCharmap::new(word_a, case_sensitive); let mut word_b = WordWithCharmap::new(word_b, case_sensitive); - + are_anagrams_internal(&mut word_a, &mut word_b) } /// internal body of [are_anagrams]; do not use directly -/// +/// /// takes in WordWithCharmap structs instead of words -/// +/// ///# Panics -/// +/// /// This function panics if the `case_sensitive` members of both words don't match -fn are_anagrams_internal(word_a: &mut WordWithCharmap, word_b: &mut WordWithCharmap) -> bool -{ - +fn are_anagrams_internal(word_a: &mut WordWithCharmap, word_b: &mut WordWithCharmap) -> bool { assert_eq!(word_a.case_sensitive, word_b.case_sensitive); let word_a_internal = word_a.get_word(); @@ -150,10 +145,10 @@ fn are_anagrams_internal(word_a: &mut WordWithCharmap, word_b: &mut WordWithChar //words can't be anagrams if their lengths are different //it's ok to use byte length here when case sensitivity is enabled //and we can skip checking word_b case sensitivity because we already asserted they were equal - if word_a.case_sensitive && word_a_internal.len() != word_b_internal.len(){ - return false; + if word_a.case_sensitive && word_a_internal.len() != word_b_internal.len() //two identical words are not anagrams - } else if word_a_internal == word_b_internal { + || word_a_internal == word_b_internal + { return false; } @@ -168,28 +163,28 @@ fn are_anagrams_internal(word_a: &mut WordWithCharmap, word_b: &mut WordWithChar } /// Similar to [are_anagrams] but checks that both words are real words -/// +/// /// This function will return false if either `word_a`, `word_b`, or both /// are not found in the specified `wordlist`. -/// -/// `wordlist` must implement the [Wordlist] trait (for example, the +/// +/// `wordlist` must implement the [Wordlist] trait (for example, the /// [default wordlist](crate::default_wordlist::default_wordlist) if present) ///# Examples /// ``` /// use anagrambot::anagram::are_proper_anagrams; /// use anagrambot::wordlist::BorrowedWordList; -/// +/// /// const CASE_SENSITIVE: bool = true; -/// +/// /// // you can use anagrambot::default_wordlist::default_wordlist() /// // to get the default Wordlist instead of generating your own, /// // as long as the `no-default-wordlist` feature is not enabled /// const TEST_WORD_SET: [&str; 3] = ["race", "care", "cow"]; /// let wordlist: BorrowedWordList = TEST_WORD_SET.into_iter().collect(); -/// +/// /// //proper anagram /// assert!(are_proper_anagrams("race", "care", &wordlist, CASE_SENSITIVE)); -/// +/// /// //non-proper anagram /// assert!(!are_proper_anagrams("aabc", "caab", &wordlist, CASE_SENSITIVE)); /// //non-anagram due to different letters @@ -197,13 +192,14 @@ fn are_anagrams_internal(word_a: &mut WordWithCharmap, word_b: &mut WordWithChar /// //non-anagram due to being identical /// assert!(!are_proper_anagrams("race", "race", &wordlist, CASE_SENSITIVE)); /// ``` -pub fn are_proper_anagrams<'a>(word_a: &str, word_b: &str, wordlist: &impl Wordlist<'a>, - case_sensitive: bool) -> bool -{ +pub fn are_proper_anagrams<'a>( + word_a: &str, + word_b: &str, + wordlist: &impl Wordlist<'a>, + case_sensitive: bool +) -> bool { //return false if either word is not found in wordlist - if !wordlist.includes_word(word_a){ - return false; - } else if !wordlist.includes_word(word_b){ + if !wordlist.includes_word(word_a) || !wordlist.includes_word(word_b) { return false; } @@ -212,7 +208,7 @@ pub fn are_proper_anagrams<'a>(word_a: &str, word_b: &str, wordlist: &impl Wordl } /// An iterator over all standard anagrams of a word -/// +/// /// The return value of [find_anagrams] pub struct AnagramsIter { chars: Vec, @@ -221,12 +217,15 @@ pub struct AnagramsIter { } impl AnagramsIter { - pub fn new(word: &str) -> Self - { + pub fn new(word: &str) -> Self { let chars: Vec = word.chars().collect(); let stack_state = vec![0; chars.len()]; - Self { chars, stack_state, i: 1 } + Self { + chars, + stack_state, + i: 1 + } } } @@ -238,7 +237,7 @@ impl Iterator for AnagramsIter { // https://en.wikipedia.org/wiki/Heap's_algorithm fn next(&mut self) -> Option { let seq_len = self.chars.len(); - if seq_len <= 1{ + if seq_len <= 1 { return None; } while self.i < seq_len { @@ -246,7 +245,6 @@ impl Iterator for AnagramsIter { if *k < self.i { if (self.i & 1) == 0 { self.chars.swap(0, self.i); - } else { self.chars.swap(*k, self.i); } @@ -267,27 +265,27 @@ impl Iterator for AnagramsIter { } /// Returns an [AnagramsIter] over all the standard anagrams of a word -/// +/// /// Effectively returns an iterator over all permutations of word's characters, /// except the original permutation (which is skipped because a word cannot be an anagram /// of itself) -/// +/// ///# Notes -/// +/// /// For a word of length `n`, there are `n! - 1` standard anagrams (`n!` meaning `factorial(n)`). /// Factorials get up to extremely high output values for relatively low input values. -/// Be mindful of this if you plan to fill a vector with standard anagrams: +/// Be mindful of this if you plan to fill a vector with standard anagrams: /// storing ***all*** standard anagrams of a word may require multiple gigabytes of memory. -pub fn find_anagrams(word: &str) -> impl Iterator -{ +pub fn find_anagrams(word: &str) -> impl Iterator { AnagramsIter::new(word) } /// An iterator over all the proper anagrams of a word -/// +/// /// The return value of [find_proper_anagrams] pub struct ProperAnagramsIter<'a, 'b, T> -where T: Iterator +where + T: Iterator { word: WordWithCharmap<'b>, wordlist_iter: T, @@ -295,13 +293,14 @@ where T: Iterator } impl<'a, 'b, T> Iterator for ProperAnagramsIter<'a, 'b, T> -where T: Iterator +where + T: Iterator { type Item = &'a str; fn next(&mut self) -> Option { - while let Some(next_word) = self.wordlist_iter.next() { + for next_word in self.wordlist_iter.by_ref() { let mut next_word_with_charmap = WordWithCharmap::new(next_word, self.case_sensitive); - if are_anagrams_internal(&mut self.word, &mut next_word_with_charmap){ + if are_anagrams_internal(&mut self.word, &mut next_word_with_charmap) { return Some(next_word); } } @@ -309,38 +308,45 @@ where T: Iterator } } - /// Returns a [ProperAnagramsIter] over all proper anagrams of `word` -/// +/// /// Note that this method does not check if `word` is present in `wordlist`; /// this is the responsibility of the caller (if desired) -/// +/// ///# Examples /// ``` /// use anagrambot::anagram::find_proper_anagrams; -/// use anagrambot::wordlist::BorrowedWordList; -/// +/// use anagrambot::wordlist::BorrowedWordList; +/// /// const CASE_SENSITIVE: bool = true; -/// +/// /// // you can use anagrambot::default_wordlist::default_wordlist() /// // to get the default Wordlist instead of generating your own, /// // as long as the `no-default-wordlist` feature is not enabled /// const TEST_WORD_SET: [&str; 5] = ["aster", "taser", "tears", "race", "cow"]; /// let wordlist: BorrowedWordList = TEST_WORD_SET.into_iter().collect(); -/// +/// /// let mut proper_anagrams = find_proper_anagrams("tears", &wordlist, CASE_SENSITIVE); -/// +/// /// assert_eq!(proper_anagrams.next(), Some("aster")); /// assert_eq!(proper_anagrams.next(), Some("taser")); /// assert_eq!(proper_anagrams.next(), None); -/// +/// /// // note that the original word "tears" is not included because /// // two identical words are not considered anagrams /// ``` -pub fn find_proper_anagrams<'a, 'b, T>(word: &'b str, wordlist: &'a T, case_sensitive: bool) - -> ProperAnagramsIter<'a, 'b, impl Iterator> -where T: Wordlist<'a> +pub fn find_proper_anagrams<'a, 'b, T>( + word: &'b str, + wordlist: &'a T, + case_sensitive: bool +) -> ProperAnagramsIter<'a, 'b, impl Iterator> +where + T: Wordlist<'a> { let word_with_charmap = WordWithCharmap::new(word, case_sensitive); - ProperAnagramsIter { word:word_with_charmap, wordlist_iter: wordlist.iter(), case_sensitive} -} \ No newline at end of file + ProperAnagramsIter { + word: word_with_charmap, + wordlist_iter: wordlist.iter(), + case_sensitive + } +} diff --git a/anagrambot/src/anagram/loose_anagram.rs b/anagrambot/src/anagram/loose_anagram.rs index 0682b26..63200a4 100644 --- a/anagrambot/src/anagram/loose_anagram.rs +++ b/anagrambot/src/anagram/loose_anagram.rs @@ -169,14 +169,12 @@ pub fn find_loose_anagrams<'a, T>(target_word: &str, // vector containing the words to test fit into target word // this is where created words will be stored before verification // once verified, they are moved to result_vec - let words_to_try: Vec<(Vec<&str>, Charmap)>; + let words_to_try: Vec<(Vec<&str>, Charmap)> = //tuple member 1 is the words that combine to make this word //tuple member 2 is the charmap of this word - //tuple member 3 is the reduced charmap of this word's parent, - //which was used to find this word // initially fill words_to_try with the candidate set - words_to_try = full_candidate_set.iter().map(|item|{ + full_candidate_set.iter().map(|item|{ (vec![*item.0], item.1.clone()) }).collect(); @@ -245,7 +243,7 @@ impl<'a> Iterator for LooseAnagramsIterator<'a> { let allowed_words = self.full_candidate_set.iter() .filter_map(|item|{ - if word_fits(&reduced_map, &item.1){ + if word_fits(&reduced_map, item.1){ Some((*item.0, item.1.clone())) } else { None @@ -284,7 +282,7 @@ impl<'a> Iterator for LooseAnagramsIterator<'a> { subword_vec.push(subword); let summed_map = - add_charmaps(&word_charmap, &submap); + add_charmaps(&word_charmap, submap); self.words_to_try.push((subword_vec, summed_map)); } } @@ -413,18 +411,14 @@ fn get_fitting_charmap(word: &str, bigger_charmap: &Charmap, for letter in word.chars(){ if ignore_spaces && letter == ' '{ continue; + } else if case_sensitive{ + if insert_closure(letter) == Err(()){ + return None; + } } else { - if case_sensitive{ - match insert_closure(letter){ - Err(_) => {return None;}, - _ => {} - } - } else { - for lower_letter in letter.to_lowercase(){ - match insert_closure(lower_letter){ - Err(_) => {return None;}, - _ => {} - } + for lower_letter in letter.to_lowercase(){ + if insert_closure(lower_letter) == Err(()){ + return None; } } } diff --git a/anagrambot/src/default_wordlist.rs b/anagrambot/src/default_wordlist.rs index 099eb9e..5540411 100644 --- a/anagrambot/src/default_wordlist.rs +++ b/anagrambot/src/default_wordlist.rs @@ -1,26 +1,25 @@ //! Utilities for dealing with the default wordlist //! //! The default wordlist is a list of words built into the anagrambot project, -//! used when a wordlist is needed but no external wordlist file is provided. +//! used when a wordlist is needed but no external wordlist file is provided. //! It may be excluded by using the `no-default-wordlist` feature. -//! -//! The default wordlist was extracted from the Ubuntu +//! +//! The default wordlist was extracted from the Ubuntu //! [wamerican](https://packages.ubuntu.com/jammy/wamerican) package. -//! A copy of the copyright document distributed with the wamerican package is included +//! A copy of the copyright document distributed with the wamerican package is included //! in source distributions of the anagrambot project as `WORDLIST-LICENSE` or can be viewed -//! [online](http://changelogs.ubuntu.com/changelogs/pool/main/s/scowl/scowl_2020.12.07-2/copyright). +//! [online](http://changelogs.ubuntu.com/changelogs/pool/main/s/scowl/scowl_2020.12.07-2/copyright). use crate::wordlist::BorrowedWordList; /// Returns the default wordlist content as a string literal, if present -/// +/// /// If the project was built normally (i.e. without the `no-default-wordlist` feature), -/// this function will return `Some` containing the wordlist content. -/// +/// this function will return `Some` containing the wordlist content. +/// /// If the project was built with the `no-default-wordlist` feature, /// this function will return `None`. -pub const fn default_wordlist_content() -> Option<&'static str> -{ +pub const fn default_wordlist_content() -> Option<&'static str> { #[cfg(feature = "no-default-wordlist")] return None; @@ -29,16 +28,12 @@ pub const fn default_wordlist_content() -> Option<&'static str> } /// Returns the default wordlist as a [BorrowedWordList], if present. -/// +/// /// If the project was built normally (i.e. without the `no-default-wordlist` feature), -/// this function will return `Some` containing the wordlist. -/// +/// this function will return `Some` containing the wordlist. +/// /// If the project was built with the `no-default-wordlist` feature, /// this function will return `None`. -pub fn default_wordlist() -> Option> -{ - match default_wordlist_content(){ - None => None, - Some(wordlist_content) => {Some(wordlist_content.lines().collect())} - } -} \ No newline at end of file +pub fn default_wordlist() -> Option> { + default_wordlist_content().map(|wordlist_content| wordlist_content.lines().collect()) +} diff --git a/anagrambot/src/lib.rs b/anagrambot/src/lib.rs index 5d11548..888356a 100644 --- a/anagrambot/src/lib.rs +++ b/anagrambot/src/lib.rs @@ -1,6 +1,5 @@ - pub mod default_wordlist; pub mod wordlist; -pub mod anagram; \ No newline at end of file +pub mod anagram; diff --git a/anagrambot/src/wordlist.rs b/anagrambot/src/wordlist.rs index f3d5941..5b69cc5 100644 --- a/anagrambot/src/wordlist.rs +++ b/anagrambot/src/wordlist.rs @@ -1,19 +1,22 @@ //! The `Wordlist` trait and some implementations -use std::{io::{self, BufReader, BufRead}, fs, path::Path}; +use std::{ + fs, + io::{self, BufRead, BufReader}, + path::Path +}; /// A list of words -/// +/// /// A `Wordlist` is a list of words (each word being a `&str`). -pub trait Wordlist<'a> -{ +pub trait Wordlist<'a> { /// The type of iterator that the `iter` method returns - /// + /// /// Must be an [Iterator] yielding `&str` type IterType: Iterator; /// Returns an an iterator that returns all words - /// + /// /// Unlike the IntoIterator trait, does not consume the `Wordlist` fn iter(&'a self) -> Self::IterType; @@ -22,7 +25,7 @@ pub trait Wordlist<'a> } /// A [Wordlist] implementor that borrows its words -/// +/// /// Useful for creating a `Wordlist` from data that already exists /// (such as a `&'static str` or pre-existing `String`) pub struct BorrowedWordList<'a> { @@ -31,71 +34,71 @@ pub struct BorrowedWordList<'a> { impl<'a> BorrowedWordList<'a> { /// Construct a new `BorrowedWordList` from an iterator of `&str` - pub fn new(word_iter: impl IntoIterator) -> Self - { - Self { word_vec: word_iter.into_iter().collect() } + pub fn new(word_iter: impl IntoIterator) -> Self { + Self { + word_vec: word_iter.into_iter().collect() + } } } -impl<'a> FromIterator<&'a str> for BorrowedWordList<'a>{ +impl<'a> FromIterator<&'a str> for BorrowedWordList<'a> { fn from_iter>(iter: T) -> Self { Self::new(iter) } } -impl<'a> Wordlist<'a> for BorrowedWordList<'a>{ - type IterType = std::iter::Map, fn(&&'a str) -> &'a str>; +impl<'a> Wordlist<'a> for BorrowedWordList<'a> { + type IterType = std::iter::Copied>; fn includes_word(&self, word: &str) -> bool { self.word_vec.contains(&word) } fn iter(&'a self) -> Self::IterType { - self.word_vec.iter().map(|d: &&'a str|->&'a str{*d}) + self.word_vec.iter().copied() } } #[cfg(test)] -mod borrowedwordlist_tests{ +mod borrowedwordlist_tests { use super::{BorrowedWordList, Wordlist}; #[test] - fn test_includes_word() - { - let list = BorrowedWordList::new(["a","b","c"]); + fn test_includes_word() { + let list = BorrowedWordList::new(["a", "b", "c"]); assert!(list.includes_word("a")); assert!(!list.includes_word("not in list")) } #[test] - fn test_to_from_iter() - { + fn test_to_from_iter() { const WORD_ARRAY: [&str; 3] = ["a", "b", "c"]; use core::iter; let list_from_iter: BorrowedWordList = WORD_ARRAY.into_iter().collect(); let list_from_new = BorrowedWordList::new(WORD_ARRAY); - + let first_iter = list_from_iter.iter(); let second_iter = list_from_new.iter(); - for (first, second) in iter::zip(first_iter, second_iter){ + for (first, second) in iter::zip(first_iter, second_iter) { assert_eq!(first, second); } } } /// A [Wordlist] implementor that owns its words -/// +/// /// Useful for creating a `Wordlist` from new data (such as from a file) pub struct OwnedWordList { word_vec: Vec } -impl OwnedWordList{ +impl OwnedWordList { /// Construct a new `OwnedWordList` from an iterator of [String](std::string::String) - pub fn new(word_iter: impl IntoIterator) -> Self - { - Self{word_vec: word_iter.into_iter().collect()} + pub fn new(word_iter: impl IntoIterator) -> Self { + Self { + word_vec: word_iter.into_iter().collect() + } } /// Construct a new `OwnedWordList` from the contents of a text file @@ -103,8 +106,7 @@ impl OwnedWordList{ /// `word_file` must be a [Path] to a text file containing words. /// /// Each line of the text file is considered a single word. - pub fn from_file(word_file: &Path) -> io::Result - { + pub fn from_file(word_file: &Path) -> io::Result { let word_file = fs::File::open(word_file)?; let mut word_vec: Vec = Vec::new(); @@ -119,13 +121,13 @@ impl OwnedWordList{ } } -impl FromIterator for OwnedWordList{ +impl FromIterator for OwnedWordList { fn from_iter>(iter: T) -> Self { OwnedWordList::new(iter) } } -impl<'a> Wordlist<'a> for OwnedWordList{ +impl<'a> Wordlist<'a> for OwnedWordList { // this long type has to be written out because impl trait syntax // cannot be used for associated types type IterType = std::iter::Map, fn(&String) -> &str>; @@ -136,19 +138,18 @@ impl<'a> Wordlist<'a> for OwnedWordList{ } fn iter(&'a self) -> Self::IterType { - self.word_vec.iter().map(|p|{p.as_str()}) + self.word_vec.iter().map(|p| p.as_str()) } } #[cfg(test)] -mod ownedwordlist_tests{ +mod ownedwordlist_tests { use super::{OwnedWordList, Wordlist}; use crate::default_wordlist::default_wordlist; use std::path::Path; #[test] - fn test_includes_word() - { + fn test_includes_word() { let word_strings: [String; 3] = [String::from("a"), String::from("b"), String::from("c")]; let list = OwnedWordList::new(word_strings); @@ -157,40 +158,36 @@ mod ownedwordlist_tests{ } #[test] - fn test_to_from_iter() - { - + fn test_to_from_iter() { // two are needed because the first OwnedWordList will take ownership of the first let word_strings_a: [String; 3] = [String::from("a"), String::from("b"), String::from("c")]; let word_strings_b: [String; 3] = [String::from("a"), String::from("b"), String::from("c")]; - use core::iter; let list_from_iter: OwnedWordList = word_strings_a.into_iter().collect(); let list_from_new = OwnedWordList::new(word_strings_b); - + let first_iter = list_from_iter.iter(); let second_iter = list_from_new.iter(); - for (first, second) in iter::zip(first_iter, second_iter){ + for (first, second) in iter::zip(first_iter, second_iter) { assert_eq!(first, second); } } - #[test] - fn test_default_vs_file(){ - - let default_wordlist = match default_wordlist(){ + fn test_default_vs_file() { + let default_wordlist = match default_wordlist() { Some(wordlist) => wordlist, - None => {return;} //end test if default wordlist isn't present + None => { + return; + } //end test if default wordlist isn't present }; - let wordlist_from_file = - OwnedWordList::from_file(Path::new("words.txt")).unwrap(); + let wordlist_from_file = OwnedWordList::from_file(Path::new("words.txt")).unwrap(); - for (defword, ownedword) in default_wordlist.iter().zip(wordlist_from_file.iter()){ + for (defword, ownedword) in default_wordlist.iter().zip(wordlist_from_file.iter()) { assert_eq!(defword, ownedword); } } -} \ No newline at end of file +} diff --git a/anagrambot/words.txt b/anagrambot/words.txt index f7f217f..1f9c2a8 100644 --- a/anagrambot/words.txt +++ b/anagrambot/words.txt @@ -51639,9 +51639,6 @@ googles googling gooier gooiest -gook -gook's -gooks goon goon's goons @@ -68565,19 +68562,12 @@ nieces niftier niftiest nifty -nigga niggard niggardliness niggardliness's niggardly niggard's niggards -nigga's -niggas -niggaz -nigger -nigger's -niggers niggle niggled niggle's @@ -103190,29 +103180,6 @@ xenophobic xerographic xerography xerography's -xiii -xref -xrefs -xterm -xterm's -xvii -xviii -xxii -xxiii -xxiv -xxix -xxvi -xxvii -xxviii -xxxi -xxxii -xxxiii -xxxiv -xxxix -xxxv -xxxvi -xxxvii -xxxviii xylem xylem's xylophone diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..6243397 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,4 @@ +#requires nightly: use `cargo +nightly fmt` to run +#if nightly isn't installed, use `rustup toolchain install nightly` to install it +trailing_comma = "Never" +match_block_trailing_comma = false \ No newline at end of file