From 1af4efa31df0d42d04885a9b8327dc5f0caf6d7e Mon Sep 17 00:00:00 2001
From: Truong Nhan Nguyen <80200848+sozelfist@users.noreply.github.com>
Date: Sat, 26 Oct 2024 21:03:11 +0700
Subject: [PATCH] Refactor Anagram (#825)

* ref: refactor anagram

* chore: rename `char_frequency` to `char_count`

* tests: add some edge tests

* style: rename local variable

* docs: remove frequency from doc-str

---------

Co-authored-by: Piotr Idzik <65706193+vil02@users.noreply.github.com>
---
 src/string/anagram.rs | 116 ++++++++++++++++++++++++++++++++++++------
 1 file changed, 100 insertions(+), 16 deletions(-)

diff --git a/src/string/anagram.rs b/src/string/anagram.rs
index b81b7804707..9ea37dc4f6f 100644
--- a/src/string/anagram.rs
+++ b/src/string/anagram.rs
@@ -1,10 +1,68 @@
-pub fn check_anagram(s: &str, t: &str) -> bool {
-    sort_string(s) == sort_string(t)
+use std::collections::HashMap;
+
+/// Custom error type representing an invalid character found in the input.
+#[derive(Debug, PartialEq)]
+pub enum AnagramError {
+    NonAlphabeticCharacter,
 }
 
-fn sort_string(s: &str) -> Vec<char> {
-    let mut res: Vec<char> = s.to_ascii_lowercase().chars().collect::<Vec<_>>();
-    res.sort_unstable();
+/// Checks if two strings are anagrams, ignoring spaces and case sensitivity.
+///
+/// # Arguments
+///
+/// * `s` - First input string.
+/// * `t` - Second input string.
+///
+/// # Returns
+///
+/// * `Ok(true)` if the strings are anagrams.
+/// * `Ok(false)` if the strings are not anagrams.
+/// * `Err(AnagramError)` if either string contains non-alphabetic characters.
+pub fn check_anagram(s: &str, t: &str) -> Result<bool, AnagramError> {
+    let s_cleaned = clean_string(s)?;
+    let t_cleaned = clean_string(t)?;
+
+    Ok(char_count(&s_cleaned) == char_count(&t_cleaned))
+}
+
+/// Cleans the input string by removing spaces and converting to lowercase.
+/// Returns an error if any non-alphabetic character is found.
+///
+/// # Arguments
+///
+/// * `s` - Input string to clean.
+///
+/// # Returns
+///
+/// * `Ok(String)` containing the cleaned string (no spaces, lowercase).
+/// * `Err(AnagramError)` if the string contains non-alphabetic characters.
+fn clean_string(s: &str) -> Result<String, AnagramError> {
+    s.chars()
+        .filter(|c| !c.is_whitespace())
+        .map(|c| {
+            if c.is_alphabetic() {
+                Ok(c.to_ascii_lowercase())
+            } else {
+                Err(AnagramError::NonAlphabeticCharacter)
+            }
+        })
+        .collect()
+}
+
+/// Computes the histogram of characters in a string.
+///
+/// # Arguments
+///
+/// * `s` - Input string.
+///
+/// # Returns
+///
+/// * A `HashMap` where the keys are characters and values are their count.
+fn char_count(s: &str) -> HashMap<char, usize> {
+    let mut res = HashMap::new();
+    for c in s.chars() {
+        *res.entry(c).or_insert(0) += 1;
+    }
     res
 }
 
@@ -12,16 +70,42 @@ fn sort_string(s: &str) -> Vec<char> {
 mod tests {
     use super::*;
 
-    #[test]
-    fn test_check_anagram() {
-        assert!(check_anagram("", ""));
-        assert!(check_anagram("A", "a"));
-        assert!(check_anagram("anagram", "nagaram"));
-        assert!(check_anagram("abcde", "edcba"));
-        assert!(check_anagram("sIlEnT", "LiStEn"));
-
-        assert!(!check_anagram("", "z"));
-        assert!(!check_anagram("a", "z"));
-        assert!(!check_anagram("rat", "car"));
+    macro_rules! test_cases {
+        ($($name:ident: $test_case:expr,)*) => {
+            $(
+                #[test]
+                fn $name() {
+                    let (s, t, expected) = $test_case;
+                    assert_eq!(check_anagram(s, t), expected);
+                    assert_eq!(check_anagram(t, s), expected);
+                }
+            )*
+        }
+    }
+
+    test_cases! {
+        empty_strings: ("", "", Ok(true)),
+        empty_and_non_empty: ("", "Ted Morgan", Ok(false)),
+        single_char_same: ("z", "Z", Ok(true)),
+        single_char_diff: ("g", "h", Ok(false)),
+        valid_anagram_lowercase: ("cheater", "teacher", Ok(true)),
+        valid_anagram_with_spaces: ("madam curie", "radium came", Ok(true)),
+        valid_anagram_mixed_cases: ("Satan", "Santa", Ok(true)),
+        valid_anagram_with_spaces_and_mixed_cases: ("Anna Madrigal", "A man and a girl", Ok(true)),
+        new_york_times: ("New York Times", "monkeys write", Ok(true)),
+        church_of_scientology: ("Church of Scientology", "rich chosen goofy cult", Ok(true)),
+        mcdonalds_restaurants: ("McDonald's restaurants", "Uncle Sam's standard rot", Err(AnagramError::NonAlphabeticCharacter)),
+        coronavirus: ("coronavirus", "carnivorous", Ok(true)),
+        synonym_evil: ("evil", "vile", Ok(true)),
+        synonym_gentleman: ("a gentleman", "elegant man", Ok(true)),
+        antigram: ("restful", "fluster", Ok(true)),
+        sentences: ("William Shakespeare", "I am a weakish speller", Ok(true)),
+        part_of_speech_adj_to_verb: ("silent", "listen", Ok(true)),
+        anagrammatized: ("Anagrams", "Ars magna", Ok(true)),
+        non_anagram: ("rat", "car", Ok(false)),
+        invalid_anagram_with_special_char: ("hello!", "world", Err(AnagramError::NonAlphabeticCharacter)),
+        invalid_anagram_with_numeric_chars: ("test123", "321test", Err(AnagramError::NonAlphabeticCharacter)),
+        invalid_anagram_with_symbols: ("check@anagram", "check@nagaram", Err(AnagramError::NonAlphabeticCharacter)),
+        non_anagram_length_mismatch: ("abc", "abcd", Ok(false)),
     }
 }