use crate::casing::{py_isupper, py_lower};
use std::collections::HashMap;
pub(crate) struct Token {
pub(crate) lower: String,
pub(crate) is_upper: bool,
}
pub(crate) fn is_py_space(c: char) -> bool {
c.is_whitespace() || matches!(c, '\u{1c}'..='\u{1f}')
}
pub(crate) fn py_strip(s: &str) -> &str {
s.trim_matches(is_py_space)
}
pub(crate) fn translate_emojis(text: &str, emojis: &HashMap<char, String>) -> String {
let mut out = String::with_capacity(text.len());
let mut prev_space = true;
for c in text.chars() {
if let Some(description) = emojis.get(&c) {
if !prev_space {
out.push(' ');
}
out.push_str(description);
prev_space = false;
} else {
out.push(c);
prev_space = c == ' ';
}
}
py_strip(&out).to_string()
}
fn strip_punc_if_word(token: &str) -> &str {
let stripped = token.trim_matches(|c: char| c.is_ascii_punctuation());
if stripped.chars().count() <= 2 {
token
} else {
stripped
}
}
pub(crate) fn words_and_emoticons(text: &str) -> Vec<Token> {
text.split(is_py_space)
.filter(|w| !w.is_empty())
.map(|w| {
let stripped = strip_punc_if_word(w);
Token {
lower: py_lower(stripped),
is_upper: py_isupper(stripped),
}
})
.collect()
}
pub(crate) fn allcap_differential(tokens: &[Token]) -> bool {
tokens.iter().any(|t| !t.is_upper) && tokens.iter().any(|t| t.is_upper)
}
#[cfg(test)]
#[allow(clippy::indexing_slicing)]
mod tests {
use super::{
allcap_differential, is_py_space, py_strip, strip_punc_if_word, translate_emojis,
words_and_emoticons,
};
use std::collections::HashMap;
#[test]
fn python_space_class_covers_separator_controls() {
assert!(is_py_space(' '));
assert!(is_py_space('\t'));
assert!(is_py_space('\n'));
assert!(is_py_space('\u{1c}'));
assert!(is_py_space('\u{1f}'));
assert!(!is_py_space('a'));
assert!(!is_py_space('!'));
}
#[test]
fn strip_trims_both_ends_only() {
assert_eq!(py_strip(" a b "), "a b");
assert_eq!(py_strip("\t\nx\u{1c}"), "x");
assert_eq!(py_strip(""), "");
}
#[test]
fn punctuation_strip_keeps_short_forms() {
assert_eq!(strip_punc_if_word("happy,"), "happy");
assert_eq!(strip_punc_if_word("--good--"), "good");
assert_eq!(strip_punc_if_word(":)"), ":)");
assert_eq!(strip_punc_if_word("8-)"), "8-)");
assert_eq!(strip_punc_if_word("is?"), "is?");
assert_eq!(strip_punc_if_word("so!"), "so!");
assert_eq!(strip_punc_if_word("don't"), "don't");
assert_eq!(strip_punc_if_word("!!!"), "!!!");
}
#[test]
fn tokenizer_splits_on_whitespace_runs() {
let toks = words_and_emoticons(" GOOD \t bad, ");
let lowers: Vec<&str> = toks.iter().map(|t| t.lower.as_str()).collect();
assert_eq!(lowers, ["good", "bad"]);
assert!(toks[0].is_upper);
assert!(!toks[1].is_upper);
assert!(words_and_emoticons("").is_empty());
assert!(words_and_emoticons(" ").is_empty());
}
#[test]
fn emoji_walk_inserts_spaces_like_the_reference() {
let mut emojis = HashMap::new();
emojis.insert('\u{1F498}', String::from("heart with arrow"));
emojis.insert('\u{1F48B}', String::from("kiss mark"));
assert_eq!(
translate_emojis("x\u{1F498}", &emojis),
"x heart with arrow"
);
assert_eq!(
translate_emojis("x \u{1F498}", &emojis),
"x heart with arrow"
);
assert_eq!(
translate_emojis("\u{1F498}\u{1F48B}", &emojis),
"heart with arrow kiss mark"
);
assert_eq!(translate_emojis("\u{1F498}y", &emojis), "heart with arrowy");
assert_eq!(translate_emojis(" plain ", &emojis), "plain");
}
#[test]
fn cap_differential_needs_a_mix() {
let mixed = words_and_emoticons("GOOD bad");
assert!(allcap_differential(&mixed));
let all_caps = words_and_emoticons("GOOD BAD");
assert!(!allcap_differential(&all_caps));
let no_caps = words_and_emoticons("good bad");
assert!(!allcap_differential(&no_caps));
assert!(!allcap_differential(&[]));
}
}