use crate::domain::contact::Tel;
use crate::domain::contact::TelType;
pub fn normalize_fn(fn_val: &str) -> (String, Option<String>, Option<String>) {
let cleaned = fn_val.trim();
if cleaned.contains('@') {
return (cleaned.to_string(), None, None);
}
let mut remaining = cleaned.to_string();
let title = extract_title(&remaining);
if let Some(ref t) = title {
remaining = remaining[t.len()..].trim().to_string();
}
let role = extract_role(&remaining);
if let Some(ref r) = role {
let pos = remaining.to_lowercase().rfind(&r.to_lowercase()).unwrap();
remaining = remaining[..pos].trim().to_string();
}
remaining = normalize_capitalization(&remaining);
(remaining.trim().to_string(), title, role)
}
fn extract_title(fn_val: &str) -> Option<String> {
let titles = [
("Ilmo. Sr.", 1),
("Ilma. Sra.", 1),
("Excmo. Sr.", 1),
("Excma. Sra.", 1),
("Ilmo.", 1),
("Ilma.", 1),
("Excmo.", 1),
("Excma.", 1),
("Dr.", 1),
("Dra.", 1),
("D.", 1),
("Dña.", 1),
("Sr.", 1),
("Sra.", 1),
("Don ", 0),
("Doña ", 0),
("Sr. D. ", 2),
("Sra. Dña. ", 2),
];
let lower = fn_val.to_lowercase();
for (title_str, spaces) in &titles {
if lower.starts_with(&title_str.to_lowercase()) {
let after = &fn_val[title_str.len()..];
if after.is_empty() || after.starts_with(' ') {
return Some(title_str.to_string());
}
if *spaces > 0 {
continue;
}
}
}
None
}
fn extract_role(fn_val: &str) -> Option<String> {
let roles = [
"Juez",
"Jueza",
"Fiscal",
"Letrado",
"Letrada",
"Procurador",
"Procuradora",
"Secretario",
"Secretaria",
"Abogado",
"Abogada",
"Notario",
"Notaria",
];
let lower = fn_val.to_lowercase().trim().to_string();
for role in &roles {
let rlower = role.to_lowercase();
if lower.ends_with(&rlower) {
let prefix_len = lower.len() - rlower.len();
if prefix_len == 0 || lower.as_bytes().get(prefix_len - 1) == Some(&b' ') {
return Some(role.to_string());
}
}
}
None
}
fn normalize_capitalization(s: &str) -> String {
let words: Vec<String> = s
.split_whitespace()
.map(|w| {
if w.len() >= 2 && w.len() <= 5 && w.chars().all(|c| c.is_ascii_uppercase()) {
return w.to_string();
}
let mut chars: Vec<char> = w.chars().collect();
if let Some(first) = chars.first_mut() {
*first = first.to_uppercase().next().unwrap_or(*first);
}
for c in chars.iter_mut().skip(1) {
*c = c.to_lowercase().next().unwrap_or(*c);
}
chars.into_iter().collect()
})
.collect();
words.join(" ")
}
pub fn normalize_tel(value: &str, prefijo_pais: &str) -> Tel {
let cleaned: String = value
.chars()
.filter(|c| !c.is_whitespace() && *c != '-' && *c != '.' && *c != '(' && *c != ')')
.collect();
if cleaned.starts_with('+') {
let digits_only: String = cleaned.chars().filter(|c| c.is_ascii_digit()).collect();
if !digits_only.is_empty() {
return Tel {
value: format!("+{}", digits_only),
tel_type: TelType::Other,
normalized: true,
};
}
}
if let Some(rest) = cleaned.strip_prefix("00") {
let digits: String = rest.chars().filter(|c| c.is_ascii_digit()).collect();
if !digits.is_empty() {
return Tel {
value: format!("+{}", digits),
tel_type: TelType::Other,
normalized: true,
};
}
}
if !cleaned.chars().all(|c| c.is_ascii_digit()) {
return Tel {
value: value.to_string(),
tel_type: TelType::Other,
normalized: false,
};
}
if cleaned.len() == 9
&& cleaned
.chars()
.next()
.map(|c| c == '6' || c == '7' || c == '8' || c == '9')
.unwrap_or(false)
{
let prefix = prefijo_pais.trim_start_matches('+');
return Tel {
value: format!("+{}{}", prefix, cleaned),
tel_type: TelType::Other,
normalized: true,
};
}
if cleaned.len() > 9 {
let prefix = prefijo_pais.trim_start_matches('+');
return Tel {
value: format!("+{}{}", prefix, cleaned),
tel_type: TelType::Other,
normalized: true,
};
}
Tel {
value: value.to_string(),
tel_type: TelType::Other,
normalized: false,
}
}
pub fn normalize_org(org: &str) -> (String, Option<String>) {
let legal_forms = [
"S.L.P.", "S.L.U.", "S.A.U.", "S.C.P.", "S.L.", "S.A.", "C.B.", "S.C.",
];
let trimmed = org.trim();
let lower = trimmed.to_lowercase();
for form in &legal_forms {
let flower = form.to_lowercase();
if lower.ends_with(&flower) {
let prefix_len = trimmed.len() - form.len();
if prefix_len > 0 {
let before = &trimmed[..prefix_len];
if before.ends_with(' ') || before.ends_with(", ") {
let org_clean = before.trim_end().trim_end_matches(',').trim().to_string();
let form_clean = form.trim_end_matches('.').to_string();
return (org_clean, Some(form_clean));
}
}
}
}
(trimmed.to_string(), None)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_fn_removes_title_ilmo_sr() {
let (fn_val, title, role) = normalize_fn("Ilmo. Sr. Juan Pérez");
assert_eq!(fn_val, "Juan Pérez");
assert_eq!(title, Some("Ilmo. Sr.".into()));
assert_eq!(role, None);
}
#[test]
fn test_normalize_fn_removes_title_dr() {
let (fn_val, title, _) = normalize_fn("Dr. García López");
assert_eq!(fn_val, "García López");
assert_eq!(title, Some("Dr.".into()));
}
#[test]
fn test_normalize_fn_removes_role_juez() {
let (fn_val, title, role) = normalize_fn("Carlos Ruiz Juez");
assert_eq!(fn_val, "Carlos Ruiz");
assert_eq!(title, None);
assert_eq!(role, Some("Juez".into()));
}
#[test]
fn test_normalize_fn_capitalization() {
let (fn_val, _, _) = normalize_fn("juZGADO INSTRUCCIÓN 9");
assert_eq!(fn_val, "Juzgado Instrucción 9");
}
#[test]
fn test_normalize_fn_respects_acronyms() {
let (fn_val, _, _) = normalize_fn("ICAV turno oficio");
assert_eq!(fn_val, "ICAV Turno Oficio");
}
#[test]
fn test_normalize_fn_respects_acronyms_gva() {
let (fn_val, _, _) = normalize_fn("conselleria GVA innovación");
assert_eq!(fn_val, "Conselleria GVA Innovación");
}
#[test]
fn test_normalize_fn_email_unchanged() {
let (fn_val, title, role) = normalize_fn("info@procuradores.es");
assert_eq!(fn_val, "info@procuradores.es");
assert_eq!(title, None);
assert_eq!(role, None);
}
#[test]
fn test_normalize_fn_combined_title_and_role() {
let (fn_val, title, role) = normalize_fn("Ilmo. Sr. Carlos Ruiz Juez");
assert_eq!(fn_val, "Carlos Ruiz");
assert_eq!(title, Some("Ilmo. Sr.".into()));
assert_eq!(role, Some("Juez".into()));
}
#[test]
fn test_normalize_tel_e164_spanish() {
let tel = normalize_tel("612345678", "+34");
assert_eq!(tel.value, "+34612345678");
assert!(tel.normalized);
}
#[test]
fn test_normalize_tel_already_e164() {
let tel = normalize_tel("+34612345678", "+34");
assert_eq!(tel.value, "+34612345678");
assert!(tel.normalized);
}
#[test]
fn test_normalize_tel_with_spaces_and_dashes() {
let tel = normalize_tel("+34 612-345-678", "+34");
assert_eq!(tel.value, "+34612345678");
assert!(tel.normalized);
}
#[test]
fn test_normalize_tel_double_zero_prefix() {
let tel = normalize_tel("0034612345678", "+34");
assert_eq!(tel.value, "+34612345678");
assert!(tel.normalized);
}
#[test]
fn test_normalize_tel_non_numeric() {
let tel = normalize_tel("AEAT", "+34");
assert_eq!(tel.value, "AEAT");
assert!(!tel.normalized);
}
#[test]
fn test_normalize_tel_uk_number() {
let tel = normalize_tel("+447911123456", "+34");
assert_eq!(tel.value, "+447911123456");
assert!(tel.normalized);
}
#[test]
fn test_normalize_org_sl() {
let (org, form) = normalize_org("Despacho Legal S.L.");
assert_eq!(org, "Despacho Legal");
assert_eq!(form.as_deref(), Some("S.L"));
}
#[test]
fn test_normalize_org_slp() {
let (org, form) = normalize_org("Gráficas Nasve, S.L.P.");
assert_eq!(org, "Gráficas Nasve");
assert_eq!(form.as_deref(), Some("S.L.P"));
}
#[test]
fn test_normalize_org_no_legal_form() {
let (org, form) = normalize_org("Juzgado Instrucción 9");
assert_eq!(org, "Juzgado Instrucción 9");
assert_eq!(form, None);
}
#[test]
fn test_normalize_org_partial_match_avoided() {
let (org, form) = normalize_org("S.Lorenzo Consulting");
assert_eq!(org, "S.Lorenzo Consulting");
assert_eq!(form, None);
}
}