pub fn normalize_ar(text: &str) -> String {
let mut out = String::with_capacity(text.len());
for ch in text.chars() {
match ch {
'\u{0610}'..='\u{061A}'
| '\u{064B}'..='\u{065F}'
| '\u{0640}'
| '\u{0670}'
| '\u{06D6}'..='\u{06ED}' => {}
'آ' | 'أ' | 'إ' | 'ٱ' => out.push('ا'),
'ى' => out.push('ي'), 'ة' => out.push('ه'), 'ؤ' => out.push('و'),
'ئ' => out.push('ي'),
'\u{0660}'..='\u{0669}' => out.push((b'0' + (ch as u32 - 0x0660) as u8) as char),
'\u{06F0}'..='\u{06F9}' => out.push((b'0' + (ch as u32 - 0x06F0) as u8) as char),
'\u{066B}' => out.push('.'),
other => out.extend(other.to_lowercase()),
}
}
out.split_whitespace().collect::<Vec<_>>().join(" ")
}
#[cfg(test)]
mod tests {
use super::normalize_ar;
#[test]
fn folds_alef_and_hamza_carriers() {
assert_eq!(normalize_ar("أ"), "ا");
assert_eq!(normalize_ar("إ"), "ا");
assert_eq!(normalize_ar("آ"), "ا");
assert_eq!(normalize_ar("ٱ"), "ا");
assert_eq!(normalize_ar("مصطفى"), "مصطفي"); assert_eq!(normalize_ar("مسؤول"), "مسوول"); }
#[test]
fn taa_marbuta_becomes_haa() {
let r = normalize_ar("طماطة");
assert!(!r.contains('ة'));
assert!(r.ends_with('ه'));
}
#[test]
fn strips_tatweel_and_diacritics() {
assert_eq!(normalize_ar("سـلام"), "سلام"); assert_eq!(normalize_ar("سَلاَم"), "سلام"); }
#[test]
fn digits_and_decimal_separator() {
assert_eq!(normalize_ar("١٢٣"), "123"); assert_eq!(normalize_ar("۱۲۳"), "123"); assert_eq!(normalize_ar("١٢٫٥"), "12.5"); }
#[test]
fn ascii_lowercased_and_whitespace_collapsed() {
assert_eq!(normalize_ar("Tomato"), "tomato");
assert_eq!(normalize_ar(" a b "), "a b");
}
#[test]
fn idempotent() {
let samples = ["أحمد", "طماطة ١٢٫٥ جنيه", "Hello World", "سـلامٌ عليكم"];
for s in samples {
let once = normalize_ar(s);
assert_eq!(normalize_ar(&once), once, "not idempotent on {s:?}");
}
}
}