use crate::plan::US_STATES;
const REFERENCE_DIGITS: usize = 6;
const CODE_LENGTHS: std::ops::RangeInclusive<usize> = 5..=10;
const UNVOICED_CODE_LENGTH: usize = 6;
const VOWELS: &str = "AEIOUY";
#[must_use]
pub fn normalize(text: &str) -> String {
let upper = text.to_ascii_uppercase();
let head = upper
.split_once(',')
.map_or(upper.as_str(), |(head, _)| head);
let mut words: Vec<&str> = head
.split_whitespace()
.flat_map(pieces)
.filter(|word| !word.is_empty() && !is_noise(word))
.enumerate()
.filter(|&(at, word)| at == 0 || !is_code(word))
.map(|(_, word)| word)
.collect();
if words.len() > 1
&& words.last().is_some_and(|w| {
US_STATES
.iter()
.any(|(code, _)| code.eq_ignore_ascii_case(w))
})
{
words.pop();
}
words.join(" ")
}
fn pieces(word: &str) -> std::str::Split<'_, impl FnMut(char) -> bool> {
let is_initial = word.find('*') == Some(1);
word.split(move |c| c == '*' && !is_initial)
}
fn is_code(word: &str) -> bool {
if !word.chars().all(|c| c.is_ascii_alphanumeric()) {
return false;
}
let has_digit = word.chars().any(|c| c.is_ascii_digit());
let has_letter = word.chars().any(|c| c.is_ascii_alphabetic());
let is_mixed = has_digit && has_letter && CODE_LENGTHS.contains(&word.len());
let is_unvoiced = !has_digit
&& word.len() == UNVOICED_CODE_LENGTH
&& !word.chars().any(|c| VOWELS.contains(c));
is_mixed || is_unvoiced
}
#[must_use]
pub(super) fn starred_as_spaces(pattern: &str) -> String {
if !pattern.contains('*') {
return pattern.to_owned();
}
let words: Vec<&str> = pattern
.split_whitespace()
.flat_map(pieces)
.filter(|word| !word.is_empty())
.collect();
words.join(" ")
}
fn is_noise(word: &str) -> bool {
let digits = word.chars().filter(char::is_ascii_digit).count();
word.starts_with('#')
|| digits >= REFERENCE_DIGITS
|| (digits > 0
&& word
.trim_start_matches('X')
.chars()
.all(|c| c.is_ascii_digit()))
}
#[cfg(test)]
mod tests {
use super::normalize;
#[test]
fn a_payee_loses_its_store_number_city_and_references() {
assert_eq!(normalize("Safeway #1234, San Jose"), "SAFEWAY");
assert_eq!(normalize("SAFEWAY #1234 SAN JOSE CA"), "SAFEWAY SAN JOSE");
assert_eq!(
normalize("AMAZON.COM*2K4L9 AMZN.COM/BILL WA"),
"AMAZON.COM AMZN.COM/BILL"
);
assert_eq!(
normalize("CHECKCARD 1018 SHELL OIL 57442831 XXXX4321"),
"CHECKCARD SHELL OIL"
);
assert_eq!(normalize(" pg&e autopay "), "PG&E AUTOPAY");
assert_eq!(normalize("CA"), "CA");
assert_eq!(
normalize("GOOGLE *STORAGE pKQ7ZB g.co/helppay#"),
"GOOGLE STORAGE G.CO/HELPPAY#"
);
assert_eq!(
normalize("TST*BLUE DOOR CAFE AUSTIN TX"),
"TST BLUE DOOR CAFE AUSTIN"
);
assert_eq!(normalize("HOP*04KQZRT"), "HOP");
assert_eq!(normalize("MCDONALD'S F31407 DAYTON"), "MCDONALD'S DAYTON");
assert_eq!(normalize("E*TRADE SECURITIES"), "E*TRADE SECURITIES");
assert_eq!(normalize("7ELEVEN 7ELEVEN"), "7ELEVEN");
assert_eq!(normalize("GOOGLE*SPANISH4KIDS"), "GOOGLE SPANISH4KIDS");
assert_eq!(normalize("GOOGLE *FI XHTVKQ"), "GOOGLE FI");
}
}