use std::borrow::Cow;
pub fn normalize(raw: &str) -> Cow<'_, str> {
if is_normalized(raw) {
return Cow::Borrowed(raw);
}
let mut out = String::with_capacity(raw.len());
let mut pending_space = false;
for c in raw.chars() {
if is_markdown_space(c) {
pending_space = true;
continue;
}
if pending_space && !out.is_empty() {
out.push(' ');
}
pending_space = false;
if c.is_ascii() {
out.push(c.to_ascii_lowercase());
} else {
out.extend(fold(c));
}
}
Cow::Owned(out)
}
fn is_markdown_space(c: char) -> bool {
matches!(c, ' ' | '\t' | '\n' | '\r')
}
fn fold(c: char) -> impl Iterator<Item = char> {
c.to_lowercase().flat_map(char::to_uppercase).flat_map(char::to_lowercase)
}
fn is_normalized(raw: &str) -> bool {
if raw.starts_with(' ') || raw.ends_with(' ') || raw.contains(" ") {
return false;
}
raw.chars().all(|c| match c {
' ' => true,
c if is_markdown_space(c) => false,
c if c.is_ascii() => !c.is_ascii_uppercase(),
c => {
let mut folded = fold(c);
folded.next() == Some(c) && folded.next().is_none()
}
})
}
#[cfg(test)]
mod tests {
use super::normalize;
use std::borrow::Cow;
#[test]
fn idempotent() {
for raw in [
"",
"a",
"a b",
"toc",
"ß",
"ẞ",
"ss",
"A",
"Толпой",
"толпой",
" a",
"a ",
"a b",
"a\tb",
"a\nb",
"é",
"É",
"漢字",
"ff",
] {
let once = normalize(raw);
let twice = normalize(&once);
assert_eq!(once, twice, "not idempotent for {raw:?}");
assert!(matches!(twice, Cow::Borrowed(_)), "owned twice for {raw:?}");
}
}
#[test]
fn matches_relation() {
assert_eq!(normalize(" A B\t"), "a b");
assert_eq!(normalize("ẞ"), "ss");
assert_eq!(normalize("ß"), "ss");
assert_eq!(normalize("ТОЛПОЙ"), normalize("Толпой"));
assert_eq!(normalize("σ"), normalize("ς"));
assert_eq!(normalize("ff"), normalize("ff"));
assert_ne!(normalize("a\u{3000}b"), normalize("a b"));
assert_eq!(normalize("\u{a0}"), "\u{a0}");
assert!(matches!(normalize("a b"), Cow::Borrowed(_)));
assert!(matches!(normalize("A b"), Cow::Owned(_)));
assert!(matches!(normalize("a b"), Cow::Owned(_)));
assert!(matches!(normalize("ß"), Cow::Owned(_)));
}
}