use aho_corasick::{AhoCorasick, MatchKind};
use std::borrow::Cow;
use std::sync::LazyLock;
static CACHED_RU_ENGINE: LazyLock<Option<(AhoCorasick, Vec<&'static str>)>> = LazyLock::new(|| {
let patterns: Vec<&'static str> = crate::locale::RU_WEEKDAY_MAPPINGS
.iter()
.map(|(loc, _)| *loc)
.collect();
let replacements: Vec<&'static str> = crate::locale::RU_WEEKDAY_MAPPINGS
.iter()
.map(|(_, eng)| *eng)
.collect();
AhoCorasick::builder()
.match_kind(MatchKind::LeftmostFirst)
.build(&patterns)
.ok()
.map(|ac| (ac, replacements))
});
fn mappings_match_ru(mappings: &[(&str, &str)]) -> bool {
let canonical = crate::locale::RU_WEEKDAY_MAPPINGS;
mappings.len() == canonical.len()
&& mappings
.iter()
.zip(canonical.iter())
.all(|(a, b)| a.0 == b.0 && a.1 == b.1)
}
pub(crate) fn normalize_weekdays<'a>(text: &'a str, mappings: &[(&str, &str)]) -> Cow<'a, str> {
if mappings.is_empty() {
return Cow::Borrowed(text);
}
if mappings_match_ru(mappings) {
if let Some((ac, replacements)) = CACHED_RU_ENGINE.as_ref() {
if !ac.is_match(text) {
return Cow::Borrowed(text);
}
return Cow::Owned(ac.replace_all(text, replacements));
}
}
let patterns: Vec<&str> = mappings.iter().map(|(loc, _)| *loc).collect();
let replacements: Vec<&str> = mappings.iter().map(|(_, eng)| *eng).collect();
let ac = match AhoCorasick::builder()
.match_kind(MatchKind::LeftmostFirst)
.build(&patterns)
{
Ok(ac) => ac,
Err(_) => return Cow::Borrowed(text),
};
if !ac.is_match(text) {
return Cow::Borrowed(text);
}
Cow::Owned(ac.replace_all(text, &replacements))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn returns_borrowed_when_no_match() {
let mappings = [("Понедельник", "Monday")];
let out = normalize_weekdays("English text", &mappings);
assert!(matches!(out, Cow::Borrowed(_)));
}
#[test]
fn substitutes_known_localized_name() {
let mappings = [("Понедельник", "Monday"), ("Пн", "Mon")];
let out = normalize_weekdays("<2024-12-09 Пн>", &mappings);
assert_eq!(out, "<2024-12-09 Mon>");
}
#[test]
fn empty_mappings_passthrough() {
let out = normalize_weekdays("<2024-12-09 Mon>", &[]);
assert_eq!(out, "<2024-12-09 Mon>");
}
#[test]
fn leftmost_first_resolves_overlap() {
let mappings = [("Понедельник", "Monday"), ("По", "Mo")];
let out = normalize_weekdays("<2024-12-09 Понедельник>", &mappings);
assert_eq!(out, "<2024-12-09 Monday>");
}
#[test]
fn substitutes_multiple_distinct_localized_names() {
let mappings = [("Пн", "Mon"), ("Вт", "Tue"), ("Ср", "Wed")];
let out = normalize_weekdays("Пн Вт Ср", &mappings);
assert_eq!(out, "Mon Tue Wed");
}
#[test]
fn cached_ru_engine_produces_same_output_as_uncached() {
let inputs = [
"<2024-12-09 Понедельник>",
"<2024-12-09 Пн>",
"<2024-12-09 Среда 10:00>",
"SCHEDULED: <2024-12-15 Воскресенье>",
"Полностью русский текст без weekday-имён",
"Mixed: Пн и Tuesday в одной строке",
"",
];
let canonical = crate::locale::RU_WEEKDAY_MAPPINGS;
let cloned: Vec<(&str, &str)> = canonical.to_vec();
for text in inputs {
let fast = normalize_weekdays(text, canonical);
let slow = normalize_weekdays(text, &cloned);
assert_eq!(
fast, slow,
"cached and per-call engines must agree on `{text}`"
);
}
}
}