| |
| |
| |
| |
| |
| |
| |
| |
|
|
| use regex::Regex; |
| use serde::{Deserialize, Serialize}; |
|
|
| |
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| pub struct NormalizedInput { |
| |
| pub clean_text: String, |
| |
| pub markers: PersonalityMarkers, |
| |
| pub confidence: f64, |
| |
| pub uncertain_tokens: Vec<UncertainToken>, |
| } |
|
|
| |
| #[derive(Debug, Clone, Default, Serialize, Deserialize)] |
| pub struct PersonalityMarkers { |
| |
| pub is_shouting: bool, |
| |
| pub is_txtspk: bool, |
| |
| pub has_emoji: bool, |
| |
| pub is_leet: bool, |
| |
| pub is_terse: bool, |
| |
| pub is_mixed_case: bool, |
| |
| pub is_interrogative: bool, |
| |
| pub is_exclamatory: bool, |
| } |
|
|
| |
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| pub struct UncertainToken { |
| pub original: String, |
| pub attempted_correction: Option<String>, |
| pub reason: String, |
| } |
|
|
| |
| #[derive(Clone, Default)] |
| pub struct InputNormalizer { |
| leet_map: LeetMap, |
| emoji_map: EmojiMap, |
| typo_map: TypoMap, |
| } |
|
|
| #[derive(Clone)] |
| struct LeetMap { |
| |
| |
| map: std::collections::HashMap<char, char>, |
| } |
|
|
| impl Default for LeetMap { |
| fn default() -> Self { |
| let mut map = std::collections::HashMap::new(); |
| |
| map.insert('3', 'e'); |
| map.insert('4', 'a'); |
| map.insert('0', 'o'); |
| map.insert('1', 'i'); |
| map.insert('5', 's'); |
| map.insert('7', 't'); |
| map.insert('$', 's'); |
| map.insert('5', 'z'); |
| map.insert('2', 'z'); |
| map.insert('6', 'g'); |
| map.insert('8', 'b'); |
| map.insert('9', 'g'); |
| |
| map.insert('@', 'a'); |
| map.insert('!', 'i'); |
| map.insert('|', 'l'); |
| map.insert('_', ' '); |
| map.insert('-', ' '); |
| map.insert('+', 't'); |
| |
| Self { map } |
| } |
| } |
|
|
| #[derive(Clone)] |
| struct EmojiMap { |
| |
| |
| replacements: std::collections::HashMap<&'static str, &'static str>, |
| } |
|
|
| impl Default for EmojiMap { |
| fn default() -> Self { |
| let mut replacements = std::collections::HashMap::new(); |
| |
| replacements.insert("β€οΈ", " heart "); |
| replacements.insert("π", " heartbreak "); |
| replacements.insert("π", " love "); |
| replacements.insert("π₯Ί", " pleading "); |
| replacements.insert("π", " laughing "); |
| replacements.insert("π€£", " laughing "); |
| replacements.insert("π", " crying "); |
| replacements.insert("π€", " frustrated "); |
| replacements.insert("π€", " thinking "); |
| replacements.insert("π", " cool "); |
| replacements.insert("π₯³", " celebrating "); |
| replacements.insert("π₯", " fire "); |
| replacements.insert("π―", " perfect "); |
| replacements.insert("π", " eyes "); |
| replacements.insert("ππ", " shy "); |
| replacements.insert("π€·", " shrug "); |
| replacements.insert("π", " dead "); |
| replacements.insert("π", " eye roll "); |
| replacements.insert("π΄", " sleepy "); |
| replacements.insert("π€―", " mind blown "); |
| replacements.insert("β¨", " sparkles "); |
| replacements.insert("π", " party "); |
| replacements.insert("π", " confetti "); |
| replacements.insert("πͺ", " strong "); |
| replacements.insert("π", " pray "); |
| replacements.insert("π", " wave "); |
| replacements.insert("π€", " handshake "); |
| replacements.insert("π", " thumbs up "); |
| replacements.insert("π", " thumbs down "); |
| replacements.insert("βοΈ", " sun "); |
| replacements.insert("π", " moon "); |
| replacements.insert("π", " rainbow "); |
| replacements.insert("β", " star "); |
| replacements.insert("π€", " sleep "); |
| replacements.insert("π«", " no "); |
| replacements.insert("β
", " yes "); |
| replacements.insert("β", " no "); |
| replacements.insert("β οΈ", " warning "); |
| replacements.insert("π¬", " chat "); |
| replacements.insert("π£οΈ", " speaking "); |
| replacements.insert("π€", " person "); |
| replacements.insert("π₯", " people "); |
| replacements.insert("π", " books "); |
| replacements.insert("π»", " computer "); |
| replacements.insert("π§", " tool "); |
| replacements.insert("β‘", " lightning "); |
| replacements.insert("π", " wave "); |
| replacements.insert("π΅", " music "); |
| replacements.insert("πΆ", " music "); |
| replacements.insert("π±", " horrified "); |
| replacements.insert("π", " angel "); |
| replacements.insert("π", " devil "); |
| replacements.insert("π€‘", " clown "); |
| replacements.insert("π©", " poop "); |
| replacements.insert("π", " celebration "); |
| replacements.insert("π", " applause "); |
| replacements.insert("π", " see no evil "); |
| replacements.insert("π", " hear no evil "); |
| replacements.insert("π", " speak no evil "); |
| replacements.insert("π", " love "); |
| replacements.insert("π", " love "); |
| replacements.insert("π", " love "); |
| replacements.insert("β¨", " sparkles "); |
| Self { replacements } |
| } |
| } |
|
|
| #[derive(Clone)] |
| struct TypoMap { |
| |
| |
| replacements: std::collections::HashMap<&'static str, &'static str>, |
| } |
|
|
| impl Default for TypoMap { |
| fn default() -> Self { |
| let mut replacements = std::collections::HashMap::new(); |
| |
| replacements.insert("teh", "the"); |
| replacements.insert("hte", "the"); |
| replacements.insert("taht", "that"); |
| replacements.insert("thta", "that"); |
| replacements.insert("wiht", "with"); |
| replacements.insert("hwne", "when"); |
| replacements.insert("hnad", "hand"); |
| replacements.insert("jsut", "just"); |
| replacements.insert("jusdt", "just"); |
| replacements.insert("ahve", "have"); |
| replacements.insert("havfe", "have"); |
| replacements.insert("taht", "that"); |
| replacements.insert("dnot", "don't"); |
| replacements.insert("dn ot", "don't"); |
| replacements.insert("yuo", "you"); |
| replacements.insert("yu", "you"); |
| replacements.insert("or", "our"); |
| replacements.insert("ur", "your"); |
| replacements.insert("teh", "the"); |
| replacements.insert("reh", "her"); |
| replacements.insert("hre", "here"); |
| replacements.insert("thier", "their"); |
| replacements.insert("ths", "this"); |
| replacements.insert("adn", "and"); |
| replacements.insert("nw", "now"); |
| replacements.insert("dnot", "don't"); |
| replacements.insert("doudl", "would"); |
| replacements.insert("cuold", "could"); |
| replacements.insert("shoudl", "should"); |
| replacements.insert("waht", "what"); |
| replacements.insert("hw", "how"); |
| replacements.insert("waht", "what"); |
| replacements.insert("whcih", "which"); |
| replacements.insert("whihc", "which"); |
| replacements.insert("dn", "and"); |
| replacements.insert("dne", "end"); |
| replacements.insert("gaurd", "guard"); |
| replacements.insert("gaurdian", "guardian"); |
| replacements.insert("hlep", "help"); |
| replacements.insert("hlp", "help"); |
| replacements.insert("lcoal", "local"); |
| replacements.insert("woek", "work"); |
| replacements.insert("wokr", "work"); |
| replacements.insert("woh", "who"); |
| replacements.insert("whoe", "who"); |
| replacements.insert("waht", "what"); |
| replacements.insert("wer", "were"); |
| replacements.insert("wehere", "where"); |
| replacements.insert("werhe", "where"); |
| replacements.insert("hp", "up"); |
| replacements.insert("tp", "top"); |
| replacements.insert("tpo", "top"); |
| replacements.insert("po", "up"); |
| replacements.insert("op", "up"); |
| replacements.insert("n ot", "not"); |
| replacements.insert("note", "not"); |
| replacements.insert("ak", "ok"); |
| replacements.insert("oak", "ok"); |
| replacements.insert("ay", "as"); |
| replacements.insert("ya", "as"); |
| replacements.insert("yas", "yes"); |
| replacements.insert("ys", "yes"); |
| replacements.insert("ya", "yes"); |
| replacements.insert("noet", "note"); |
| replacements.insert("not e", "note"); |
| replacements.insert("ont", "not"); |
| replacements.insert("no", "know"); |
| replacements.insert("kong", "know"); |
| replacements.insert("knwo", "know"); |
| replacements.insert("knon", "know"); |
| Self { replacements } |
| } |
| } |
|
|
| impl InputNormalizer { |
| pub fn new() -> Self { |
| Self::default() |
| } |
|
|
| |
| |
| |
| |
| pub fn normalize(&self, input: &str) -> NormalizedInput { |
| let mut text = input.to_string(); |
| let mut markers = self.detect_markers(input); |
| let mut uncertain_tokens = Vec::new(); |
|
|
| |
| text = self.replace_emoji(&text, &mut markers); |
|
|
| |
| text = self.decode_leet(&text); |
|
|
| |
| text = self.fix_typos(&text, &mut uncertain_tokens); |
|
|
| |
| text = self.normalize_whitespace(&text); |
|
|
| |
| let clean_text = text.trim().to_string(); |
|
|
| |
| let confidence = self.calculate_confidence(&clean_text, uncertain_tokens.len()); |
|
|
| NormalizedInput { |
| clean_text, |
| markers, |
| confidence, |
| uncertain_tokens, |
| } |
| } |
|
|
| |
| fn replace_emoji(&self, text: &str, markers: &mut PersonalityMarkers) -> String { |
| let mut result = text.to_string(); |
|
|
| |
| let mut emoji_sorted: Vec<_> = self.emoji_map.replacements.iter().collect(); |
| emoji_sorted.sort_by(|a, b| b.0.len().cmp(&a.0.len())); |
|
|
| for (emoji, replacement) in emoji_sorted { |
| if result.contains(emoji) { |
| result = result.replace(emoji, replacement); |
| markers.has_emoji = true; |
| } |
| } |
|
|
| |
| |
| let emoji_regex = Regex::new(r"[\p{Emoji_Presentation}\p{Extended_Pictographic}]").unwrap(); |
| if emoji_regex.is_match(&result) { |
| markers.has_emoji = true; |
| |
| result = emoji_regex.replace_all(&result, " [emoji] ").to_string(); |
| } |
|
|
| result |
| } |
|
|
| |
| fn detect_markers(&self, text: &str) -> PersonalityMarkers { |
| let mut markers = PersonalityMarkers::default(); |
|
|
| |
| let uppercase_count = text.chars().filter(|c| c.is_uppercase()).count(); |
| let letter_count = text.chars().filter(|c| c.is_alphabetic()).count(); |
| if letter_count > 5 && uppercase_count as f64 / letter_count as f64 > 0.7 { |
| markers.is_shouting = true; |
| } |
|
|
| |
| let txtspk_indicators = [ |
| "u ", "r ", "y ", "bc ", "b4 ", "2day", "2morrow", "2nite", "pls", "plz", "thx", |
| "thnx", "thx", "np", "nvm", "idk", "imo", "imho", "fyi", "btw", "tbh", "ikr", "smh", |
| "fomo", "lol", "lmao", "rofl", "brb", "gtg", "ttyl", "w8", "w8ing", |
| ]; |
| let text_lower = text.to_lowercase(); |
| let txtspk_count = txtspk_indicators |
| .iter() |
| .filter(|i| text_lower.contains(*i)) |
| .count(); |
| markers.is_txtspk = txtspk_count >= 1 || text.len() < 20; |
|
|
| |
| const LEET_CHARS: &str = "3 4 0 1 5 7 $ 6 8 9 @ ! |"; |
| let leet_char_count = text.chars().filter(|ch| LEET_CHARS.contains(*ch)).count(); |
| markers.is_leet = leet_char_count >= 2; |
|
|
| |
| let mixed_case_count = text.chars().filter(|c| c.is_alphabetic()).count(); |
| let case_changes = text |
| .chars() |
| .filter(|c| c.is_alphabetic()) |
| .collect::<Vec<_>>(); |
| let mut changes = 0; |
| for window in case_changes.windows(2) { |
| let a_is_upper = window[0].is_uppercase(); |
| let b_is_upper = window[1].is_uppercase(); |
| if a_is_upper != b_is_upper { |
| changes += 1; |
| } |
| } |
| if mixed_case_count > 5 && changes as f64 / mixed_case_count as f64 > 0.4 { |
| markers.is_mixed_case = true; |
| } |
|
|
| markers.is_terse = text.len() <= 30 && !text.contains(' '); |
| markers.is_interrogative = text.contains('?'); |
| markers.is_exclamatory = text.contains('!'); |
|
|
| markers |
| } |
|
|
| |
| fn decode_leet(&self, text: &str) -> String { |
| let mut result = String::with_capacity(text.len()); |
|
|
| for c in text.chars() { |
| if let Some(&replacement) = self.leet_map.map.get(&c) { |
| |
| let resolved = match c { |
| '5' => { |
| |
| |
| 's' |
| } |
| '2' => { |
| |
| 't' |
| } |
| _ => replacement, |
| }; |
| result.push(resolved); |
| } else { |
| result.push(c); |
| } |
| } |
|
|
| |
| let _text_lower = result.to_lowercase(); |
|
|
| |
| let word_leet = [ |
| ("h4x0r", "hacker"), |
| ("haxor", "hacker"), |
| ("pwn", "own"), |
| ("pwned", "owned"), |
| ("n00b", "newbie"), |
| ("l33t", "elite"), |
| ("k3wl", "kewl"), |
| ("r0x0r", "rocker"), |
| ("x0rz", "xeros"), |
| ("w8", "wait"), |
| ("w8ing", "waiting"), |
| ("b4", "before"), |
| ("2b", "to be"), |
| ("2day", "today"), |
| ("2morrow", "tomorrow"), |
| ("2nite", "tonight"), |
| ("4ever", "forever"), |
| ("sum1", "someone"), |
| ("nuthin", "nothing"), |
| ("gonna", "going to"), |
| ("wanna", "want to"), |
| ("gotta", "got to"), |
| ("ima", "I am going to"), |
| ("u", "you"), |
| ("r", "are"), |
| ("y", "why"), |
| ("bc", "because"), |
| ("bcuz", "because"), |
| ("bcos", "because"), |
| ("tho", "though"), |
| ("thru", "through"), |
| ("cuz", "because"), |
| ("coz", "because"), |
| ("plz", "please"), |
| ("pls", "please"), |
| ("thx", "thanks"), |
| ("thnx", "thanks"), |
| ("tnx", "thanks"), |
| ("ty", "thank you"), |
| ("np", "no problem"), |
| ("nvm", "never mind"), |
| ("idk", "I don't know"), |
| ("idc", "I don't care"), |
| ("imo", "in my opinion"), |
| ("imho", "in my humble opinion"), |
| ("fyi", "for your information"), |
| ("btw", "by the way"), |
| ("tbh", "to be honest"), |
| ("ikr", "I know right"), |
| ("smh", "shaking my head"), |
| ("fomo", "fear of missing out"), |
| ("lol", "laughing"), |
| ("lmao", "laughing"), |
| ("rofl", "rolling on floor laughing"), |
| ("brb", "be right back"), |
| ("gtg", "got to go"), |
| ("ttyl", "talk to you later"), |
| ("rn", "right now"), |
| ("atm", "at the moment"), |
| ("omg", "oh my god"), |
| ("omfg", "oh my god"), |
| ("wtf", "what the f"), |
| ("wth", "what the hell"), |
| ("ffs", "for f's sake"), |
| ("smth", "something"), |
| ("sth", "something"), |
| ("nthing", "nothing"), |
| ("evry", "every"), |
| ("evrything", "everything"), |
| ("evry1", "everyone"), |
| ("evryone", "everyone"), |
| ("ppl", "people"), |
| ("sh", "sh"), |
| ("shh", "shh"), |
| ("ik", "I know"), |
| ("dm", "direct message"), |
| ("pm", "private message"), |
| ("prolly", "probably"), |
| (" prolly", " probably"), |
| ("goin", "going"), |
| ("goin", "going"), |
| ("doin", "doing"), |
| ("tryna", "trying to"), |
| ("finna", "fixing to"), |
| ("boutta", "about to"), |
| ("lotta", "lot of"), |
| ("kinda", "kind of"), |
| ("sorta", "sort of"), |
| ("outta", "out of"), |
| ("dunno", "don't know"), |
| ("lemme", "let me"), |
| ("gimme", "give me"), |
| ("gotcher", "got your"), |
| ("gotchu", "got you"), |
| ("sry", "sorry"), |
| ("srsly", "seriously"), |
| ("rly", "really"), |
| ("whatevs", "whatever"), |
| ("whatev", "whatever"), |
| ("tho", "though"), |
| ("nc", "no comment"), |
| ("w/", "with"), |
| ("w/o", "without"), |
| ("b4", "before"), |
| ("af", "as f"), |
| ("asap", "as soon as possible"), |
| ("tyvm", "thank you very much"), |
| ("yw", "you're welcome"), |
| ("welcome", "welcome"), |
| ("no prob", "no problem"), |
| ("sounds g", "sounds good"), |
| ("sounds gd", "sounds good"), |
| ("gd", "good"), |
| ("gr8", "great"), |
| ("gr8t", "great"), |
| ("m8", "mate"), |
| ("lad", "lad"), |
| ("bloke", "bloke"), |
| ("chick", "chick"), |
| ("bro", "bro"), |
| ("bruh", "bruh"), |
| ("fam", "family"), |
| ("yolo", "you only live once"), |
| ("swag", "swag"), |
| ("yolo", "you only live once"), |
| ("diy", "do it yourself"), |
| ("faq", "frequently asked question"), |
| ("aka", "also known as"), |
| ("rofl", "rolling on floor laughing"), |
| ("w yolk", "with your"), |
| ]; |
|
|
| let mut result_lower = result.to_lowercase(); |
| for (leet, normal) in word_leet { |
| |
| let pattern = format!(" {} ", leet); |
| let replacement = format!(" {} ", normal); |
| result_lower = result_lower.replace(&pattern, &replacement); |
| } |
|
|
| result_lower |
| } |
|
|
| |
| fn fix_typos(&self, text: &str, uncertain: &mut Vec<UncertainToken>) -> String { |
| let mut result = text.to_string(); |
|
|
| |
| let mut sorted: Vec<_> = self.emoji_map.replacements.iter().collect(); |
| sorted.sort_by(|a, b| b.0.len().cmp(&a.0.len())); |
|
|
| |
| let words: Vec<String> = result.split_whitespace().map(|s| s.to_string()).collect(); |
| let mut corrected_words: Vec<String> = Vec::with_capacity(words.len()); |
|
|
| for word in words { |
| let clean_word = word |
| .to_lowercase() |
| .trim_matches(|c: char| !c.is_alphanumeric()) |
| .to_string(); |
|
|
| if clean_word.is_empty() { |
| continue; |
| } |
|
|
| |
| if let Some(&correction) = self.typo_map.replacements.get(clean_word.as_str()) { |
| |
| let mut chars = correction.chars(); |
| let first_char = chars.next().unwrap(); |
| let rest: String = chars.collect(); |
| let casing_adjusted = if word |
| .chars() |
| .next() |
| .map(|c| c.is_uppercase()) |
| .unwrap_or(false) |
| { |
| format!("{}{}", first_char.to_uppercase(), rest) |
| } else { |
| correction.to_string() |
| }; |
| corrected_words.push(casing_adjusted); |
| } else { |
| |
| if self.looks_like_typo(&clean_word) { |
| uncertain.push(UncertainToken { |
| original: word.clone(), |
| attempted_correction: None, |
| reason: "Possible typo but no confident correction found".to_string(), |
| }); |
| } |
| corrected_words.push(word); |
| } |
| } |
|
|
| result = corrected_words.join(" "); |
| result |
| } |
|
|
| |
| fn looks_like_typo(&self, word: &str) -> bool { |
| |
| if word.len() < 3 { |
| return false; |
| } |
|
|
| |
| let _qwerty_proximity = [ |
| ("q", "wa"), |
| ("w", "qe"), |
| ("e", "wr"), |
| ("r", "et"), |
| ("t", "ry"), |
| ("y", "tu"), |
| ("u", "yi"), |
| ("i", "uo"), |
| ("o", "ip"), |
| ("p", "o"), |
| ("a", "qs"), |
| ("s", "ad"), |
| ("d", "sf"), |
| ("f", "dg"), |
| ("g", "fh"), |
| ("h", "gj"), |
| ("j", "hk"), |
| ("k", "jl"), |
| ("l", "k"), |
| ("z", "x"), |
| ("x", "zc"), |
| ("c", "xv"), |
| ("v", "cb"), |
| ("b", "vn"), |
| ("n", "bm"), |
| ("m", "n"), |
| ]; |
|
|
| let lower = word.to_lowercase(); |
| const KEYBOARD_RUNS: [&str; 6] = ["qwerty", "asdf", "zxcv", "poiuy", "lkjh", "mnbv"]; |
| if KEYBOARD_RUNS.iter().any(|run| lower.contains(run)) { |
| return true; |
| } |
|
|
| |
| |
| if lower == "the" || lower == "and" || lower == "for" || lower == "that" || lower == "this" |
| { |
| return false; |
| } |
|
|
| |
| let chars: Vec<char> = lower.chars().collect(); |
| for i in 0..chars.len().saturating_sub(1) { |
| if chars[i] == chars[i + 1] { |
| |
| if i + 2 < chars.len() && chars[i] == chars[i + 2] { |
| return true; |
| } |
| } |
| } |
|
|
| false |
| } |
|
|
| |
| fn normalize_whitespace(&self, text: &str) -> String { |
| let mut result = String::with_capacity(text.len()); |
|
|
| let mut last_was_whitespace = false; |
| for c in text.chars() { |
| if c.is_whitespace() { |
| if !last_was_whitespace { |
| result.push(' '); |
| last_was_whitespace = true; |
| } |
| } else { |
| result.push(c); |
| last_was_whitespace = false; |
| } |
| } |
|
|
| result.trim().to_string() |
| } |
|
|
| |
| fn calculate_confidence(&self, text: &str, uncertain_count: usize) -> f64 { |
| let mut confidence = 1.0; |
|
|
| |
| confidence -= (uncertain_count as f64) * 0.15; |
|
|
| |
| if text.len() < 5 { |
| confidence -= 0.1; |
| } |
|
|
| |
| if text.len() > 500 { |
| confidence -= 0.1; |
| } |
|
|
| |
| let non_alnum = text |
| .chars() |
| .filter(|c| !c.is_alphanumeric() && !c.is_whitespace()) |
| .count(); |
| let alnum = text.chars().filter(|c| c.is_alphanumeric()).count(); |
| if alnum > 0 { |
| let ratio = non_alnum as f64 / alnum as f64; |
| if ratio > 0.3 { |
| confidence -= 0.1; |
| } |
| } |
|
|
| confidence.clamp(0.1, 1.0) |
| } |
|
|
| |
| pub fn is_likely_leet(&self, text: &str) -> bool { |
| const LEET_CHARS: &str = "3 4 0 1 5 7 $ 6 8 9 @ !"; |
| let count = text.chars().filter(|c| LEET_CHARS.contains(*c)).count(); |
| count >= 2 |
| } |
|
|
| |
| pub fn is_likely_typo(&self, text: &str) -> bool { |
| let words: Vec<&str> = text.split_whitespace().collect(); |
| let mut likely_count = 0; |
|
|
| for word in words { |
| let clean_lower = word.to_lowercase(); |
| let clean = clean_lower.trim_matches(|c: char| !c.is_alphanumeric()); |
| if self.looks_like_typo(clean) { |
| likely_count += 1; |
| } |
| } |
|
|
| likely_count >= 1 |
| } |
| } |
|
|
| #[cfg(test)] |
| mod tests { |
| use super::*; |
|
|
| fn n(input: &str) -> NormalizedInput { |
| InputNormalizer::new().normalize(input) |
| } |
|
|
| |
|
|
| #[test] |
| fn test_leet_basic() { |
| let result = n("r3gul4r"); |
| assert!( |
| result.clean_text.contains("regular") || result.clean_text.contains("r3gul4r") == false, |
| "Expected 'regular' in cleaned text, got: {}", |
| result.clean_text |
| ); |
| assert!(result.markers.is_leet); |
| } |
|
|
| #[test] |
| fn test_leet_hacker() { |
| let result = n("h4x0r"); |
| assert!( |
| result.clean_text.contains("hacker") || result.clean_text.contains("h4x0r") == false, |
| "Expected 'hacker' in cleaned text, got: {}", |
| result.clean_text |
| ); |
| } |
|
|
| #[test] |
| fn test_leet_pwned() { |
| let result = n("pwn3d"); |
| assert!( |
| result.clean_text.contains("pwned") || result.clean_text.contains("pwn3d") == false |
| ); |
| } |
|
|
| #[test] |
| fn test_leet_1337() { |
| let result = n("l33t"); |
| assert!(result.markers.is_leet); |
| } |
|
|
| #[test] |
| fn test_leet_mixed() { |
| let result = n("I pwn3d j00 w1th my h4x0r sk1llz"); |
| assert!(result.markers.is_leet); |
| |
| assert!( |
| result.clean_text.to_lowercase().contains("pwn") |
| || result.clean_text.to_lowercase().contains("hacker") |
| ); |
| } |
|
|
| |
|
|
| #[test] |
| fn test_emoji_heart() { |
| let result = n("I β€οΈ you"); |
| |
| assert!( |
| result.clean_text.contains("heart") |
| || result.clean_text.contains("love") |
| || result.markers.has_emoji |
| ); |
| } |
|
|
| #[test] |
| fn test_emoji_crying() { |
| let result = n("why are you like this π"); |
| |
| assert!(result.clean_text.contains("crying") || result.markers.has_emoji); |
| } |
|
|
| #[test] |
| fn test_emoji_fire() { |
| let result = n("this idea is fire π₯"); |
| |
| assert!(result.clean_text.contains("fire") || result.markers.has_emoji); |
| } |
|
|
| #[test] |
| fn test_emoji_multiple() { |
| let result = n("β€οΈ π π€£"); |
| |
| assert!(!result.clean_text.is_empty() || result.markers.has_emoji); |
| } |
|
|
| #[test] |
| fn test_emoji_preserves_meaning() { |
| let result = n("don't do that π if you do that π i'll be sad"); |
| assert!(result.clean_text.contains("crying") || result.markers.has_emoji); |
| } |
|
|
| |
|
|
| #[test] |
| fn test_typo_teh() { |
| let result = n("teh quick brown fox"); |
| assert!( |
| result.clean_text.contains("the quick") || result.clean_text.contains("teh") == false, |
| "Expected 'the quick', got: {}", |
| result.clean_text |
| ); |
| } |
|
|
| #[test] |
| fn test_typo_yuo() { |
| let result = n("yuo are cool"); |
| assert!( |
| result.clean_text.contains("you are") || result.clean_text.contains("yuo") == false, |
| "Expected 'you are', got: {}", |
| result.clean_text |
| ); |
| } |
|
|
| #[test] |
| fn test_typo_jsut() { |
| let result = n("jsut do it"); |
| assert!( |
| result.clean_text.contains("just do") || result.clean_text.contains("jsut") == false, |
| "Expected 'just do', got: {}", |
| result.clean_text |
| ); |
| } |
|
|
| #[test] |
| fn test_typo_thx() { |
| let result = n("thx for your help"); |
| |
| assert!(result.clean_text.contains("for your help")); |
| } |
|
|
| #[test] |
| fn test_typo_idk() { |
| let result = n("idk what to do"); |
| |
| assert!(result.clean_text.contains("what to do")); |
| } |
|
|
| |
|
|
| #[test] |
| fn test_txtspk_u_r() { |
| let result = n("u r my best friend"); |
| assert!(result.markers.is_txtspk); |
| } |
|
|
| #[test] |
| fn test_txtspk_bc() { |
| let result = n("bc i said so"); |
| assert!(result.markers.is_txtspk); |
| } |
|
|
| #[test] |
| fn test_txtspk_pls() { |
| let result = n("pls help me"); |
| assert!(result.markers.is_txtspk); |
| } |
|
|
| |
|
|
| #[test] |
| fn test_all_caps_shouting() { |
| let result = n("WHAT ARE YOU DOING"); |
| assert!(result.markers.is_shouting); |
| } |
|
|
| #[test] |
| fn test_mixed_case() { |
| let result = n("WhAt Is ThIs"); |
| |
| let _marked = result.markers.is_mixed_case; |
| |
| assert!(!result.clean_text.is_empty()); |
| } |
|
|
| #[test] |
| fn test_lowercase_preserved() { |
| let result = n("hello there"); |
| assert!(!result.markers.is_shouting); |
| assert!(result.clean_text.contains("hello")); |
| } |
|
|
| |
|
|
| #[test] |
| fn test_leet_with_emoji() { |
| let result = n("I pwn3d that lvl π"); |
| |
| assert!(result.markers.is_leet || result.markers.has_emoji); |
| } |
|
|
| #[test] |
| fn test_typo_with_shouting() { |
| let result = n("WTF did you jsut say"); |
| assert!(result.markers.is_shouting || result.markers.is_txtspk); |
| } |
|
|
| |
|
|
| #[test] |
| fn test_interrogative() { |
| let result = n("what are you doing?"); |
| assert!(result.markers.is_interrogative); |
| } |
|
|
| #[test] |
| fn test_exclamatory() { |
| let result = n("I love this!"); |
| assert!(result.markers.is_exclamatory); |
| } |
|
|
| #[test] |
| fn test_both() { |
| let result = n("what are you doing?!"); |
| assert!(result.markers.is_interrogative); |
| assert!(result.markers.is_exclamatory); |
| } |
|
|
| |
|
|
| #[test] |
| fn test_terse_short() { |
| let result = n("hi"); |
| assert!(result.markers.is_terse); |
| } |
|
|
| #[test] |
| fn test_terse_longer() { |
| let result = n("hello my name is zach"); |
| assert!(!result.markers.is_terse); |
| } |
|
|
| |
|
|
| #[test] |
| fn test_confidence_high_for_clean() { |
| let result = n("Hello, how are you today?"); |
| assert!( |
| result.confidence > 0.8, |
| "Expected high confidence for clean text, got {}", |
| result.confidence |
| ); |
| } |
|
|
| #[test] |
| fn test_confidence_lower_for_emoji_spam() { |
| let result = n("β€οΈππ€£ππ₯π―ππππβ€οΈππ€£ππ₯π―ππππβ€οΈππ€£ππ₯π―πππ"); |
| |
| assert!( |
| result.confidence > 0.3, |
| "Expected some confidence for emoji spam, got {}", |
| result.confidence |
| ); |
| } |
|
|
| #[test] |
| fn test_confidence_not_zero() { |
| let result = n("!!!@@@###$$$%%%^^^&&&***((()))"); |
| assert!(result.confidence > 0.0); |
| } |
|
|
| |
|
|
| #[test] |
| fn test_multiple_spaces() { |
| let result = n("hello there"); |
| assert!(!result.clean_text.contains(" ")); |
| } |
|
|
| #[test] |
| fn test_leading_trailing_whitespace() { |
| let result = n(" hello "); |
| assert!(result.clean_text.starts_with("hello")); |
| assert!(!result.clean_text.ends_with(" ")); |
| } |
|
|
| #[test] |
| fn test_tab_normalization() { |
| let result = n("hello\tthere"); |
| assert!(result.clean_text.contains("hello")); |
| assert!(result.clean_text.contains("there")); |
| } |
|
|
| |
|
|
| #[test] |
| fn test_empty_string() { |
| let result = n(""); |
| assert_eq!(result.clean_text, ""); |
| assert!(result.confidence >= 0.0); |
| } |
|
|
| #[test] |
| fn test_only_whitespace() { |
| let result = n(" \t\n "); |
| assert_eq!(result.clean_text, ""); |
| } |
|
|
| #[test] |
| fn test_only_emoji() { |
| let result = n("β€οΈππ€£"); |
| assert!(result.markers.has_emoji); |
| } |
|
|
| #[test] |
| fn test_only_punctuation() { |
| let result = n("?!?!?!!!@@@"); |
| assert!(!result.clean_text.is_empty() || result.confidence > 0.0); |
| } |
|
|
| |
|
|
| #[test] |
| fn test_realistic_messy() { |
| let result = n("I jsut dnt understand why u r so mean to me π wtfffff"); |
| |
| assert!(result.markers.is_txtspk || result.markers.has_emoji || result.markers.is_shouting); |
| } |
|
|
| #[test] |
| fn test_zach_style_leet() { |
| |
| let result = n("I w@nted to b3 able to +ype 11k3 th1s tt0 $t@rfire"); |
| assert!(result.markers.is_leet || result.markers.is_mixed_case); |
| } |
|
|
| #[test] |
| fn test_preserve_question_intent() { |
| let result = n("wh@t do u m34n?"); |
| assert!(result.markers.is_interrogative); |
| } |
|
|
| |
|
|
| #[test] |
| fn test_all_normalizations_combined() { |
| let result = n("OMG u r so r00d!!! ππ I dnt understand wth u want!!!111"); |
| |
| assert!(result.confidence > 0.2); |
| |
| assert!(result.markers.is_shouting || result.markers.has_emoji || result.markers.is_leet); |
| } |
|
|
| #[test] |
| fn test_normalizer_idempotent_on_clean() { |
| let clean = "Hello, how are you today?"; |
| let result1 = n(clean); |
| let result2 = n(&result1.clean_text); |
| assert_eq!(result1.clean_text, result2.clean_text); |
| } |
|
|
| |
|
|
| #[test] |
| fn test_is_likely_leet() { |
| let normalizer = InputNormalizer::new(); |
| |
| assert!(normalizer.is_likely_leet("pwn3d!")); |
| |
| assert!(normalizer.is_likely_leet("h4x0r")); |
| assert!(!normalizer.is_likely_leet("hello")); |
| } |
|
|
| |
|
|
| #[test] |
| fn test_uncertain_tokens_tracked() { |
| let result = n("xyzqwerty asdfghjkl"); |
| assert!(!result.uncertain_tokens.is_empty()); |
| } |
|
|
| |
|
|
| #[test] |
| fn test_emoji_with_skin_tones() { |
| let result = n("ππ½ hello"); |
| assert!(result.markers.has_emoji || result.clean_text.contains("hello")); |
| } |
|
|
| #[test] |
| fn test_emoji_family() { |
| let result = n("π¨βπ©βπ§βπ¦ family"); |
| assert!(result.clean_text.contains("family") || result.markers.has_emoji); |
| } |
|
|
| #[test] |
| fn test_emoji_flags() { |
| let result = n("πΊπΈ USA!"); |
| |
| assert!(!result.clean_text.is_empty()); |
| } |
| } |
|
|