use crate::phonetic::llev::RuleSetChar;
use std::sync::OnceLock;
pub fn base() -> &'static RuleSetChar {
static RULESET: OnceLock<RuleSetChar> = OnceLock::new();
RULESET.get_or_init(|| {
let content = include_str!("../../../data/rules/portuguese/base.llev");
let file = crate::phonetic::llev::parse_str(content)
.expect("Invalid embedded portuguese/base.llev - this is a bug in liblevenshtein");
RuleSetChar::from_llev(&file)
.expect("Failed to compile Portuguese base rules - this is a bug in liblevenshtein")
})
}
pub fn european() -> &'static RuleSetChar {
static RULESET: OnceLock<RuleSetChar> = OnceLock::new();
RULESET.get_or_init(|| {
let content = include_str!("../../../data/rules/portuguese/european.llev");
let file = crate::phonetic::llev::parse_str(content)
.expect("Invalid embedded portuguese/european.llev - this is a bug in liblevenshtein");
RuleSetChar::from_llev(&file)
.expect("Failed to compile European Portuguese rules - this is a bug in liblevenshtein")
})
}
pub fn brazilian() -> &'static RuleSetChar {
static RULESET: OnceLock<RuleSetChar> = OnceLock::new();
RULESET.get_or_init(|| {
let content = include_str!("../../../data/rules/portuguese/brazilian.llev");
let file = crate::phonetic::llev::parse_str(content)
.expect("Invalid embedded portuguese/brazilian.llev - this is a bug in liblevenshtein");
RuleSetChar::from_llev(&file).expect(
"Failed to compile Brazilian Portuguese rules - this is a bug in liblevenshtein",
)
})
}
pub fn combined_european() -> &'static RuleSetChar {
static RULESET: OnceLock<RuleSetChar> = OnceLock::new();
RULESET.get_or_init(|| {
let mut rules = Vec::new();
rules.extend(base().rules.iter().cloned());
rules.extend(european().rules.iter().cloned());
RuleSetChar {
rules,
name: Some("Combined European Portuguese".to_string()),
version: None,
}
})
}
pub fn combined_brazilian() -> &'static RuleSetChar {
static RULESET: OnceLock<RuleSetChar> = OnceLock::new();
RULESET.get_or_init(|| {
let mut rules = Vec::new();
rules.extend(base().rules.iter().cloned());
rules.extend(brazilian().rules.iter().cloned());
RuleSetChar {
rules,
name: Some("Combined Brazilian Portuguese".to_string()),
version: None,
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_base_loads() {
let rules = base();
assert!(
!rules.is_empty(),
"Portuguese base rules should not be empty"
);
assert!(
rules.len() > 40,
"expected >40 base rules, got {}",
rules.len()
);
}
#[test]
fn test_european_loads() {
let rules = european();
assert!(
!rules.is_empty(),
"European Portuguese rules should not be empty"
);
}
#[test]
fn test_brazilian_loads() {
let rules = brazilian();
assert!(
!rules.is_empty(),
"Brazilian Portuguese rules should not be empty"
);
}
#[test]
fn test_combined_european_loads() {
let rules = combined_european();
assert!(
!rules.is_empty(),
"Combined European Portuguese rules should not be empty"
);
let total = base().len() + european().len();
assert_eq!(
rules.len(),
total,
"combined_european should have all rules"
);
}
#[test]
fn test_combined_brazilian_loads() {
let rules = combined_brazilian();
assert!(
!rules.is_empty(),
"Combined Brazilian Portuguese rules should not be empty"
);
let total = base().len() + brazilian().len();
assert_eq!(
rules.len(),
total,
"combined_brazilian should have all rules"
);
}
#[test]
fn test_ch_digraph() {
let rules = base();
let result = rules.apply("chave");
assert!(result.contains("ʃ"), "ch should become SH, got: {}", result);
}
#[test]
fn test_lh_digraph() {
let rules = base();
let result = rules.apply("filho");
assert!(result.contains("ʎ"), "lh should become LY, got: {}", result);
}
#[test]
fn test_nh_digraph() {
let rules = base();
let result = rules.apply("senhor");
assert!(result.contains("ɲ"), "nh should become NY, got: {}", result);
}
#[test]
fn test_nasal_ao() {
let rules = base();
let result = rules.apply("não");
assert!(
result.contains("ɐ̃") || result.contains("a~") || result.contains("ã"),
"ão should be nasal, got: {}",
result
);
}
#[test]
fn test_c_cedilla() {
let rules = base();
let result = rules.apply("coração");
assert!(
result.contains('s') || result.contains('ʃ'),
"ç should become s, got: {}",
result
);
}
#[test]
fn test_j_pronunciation() {
let rules = base();
let result = rules.apply("janeiro");
assert!(result.contains("ʒ"), "j should become zh, got: {}", result);
}
#[test]
fn test_brazilian_l_vocalization() {
let rules = combined_brazilian();
let result = rules.apply("brasil");
assert!(
result.ends_with('w'),
"final l should become w in Brazilian, got: {}",
result
);
}
#[test]
fn test_brazilian_t_palatalization() {
let rules = combined_brazilian();
let result = rules.apply("tipo");
assert!(
result.contains("t͡ʃ") || result.contains("tsh") || result.contains("tʃ"),
"ti should palatalize in Brazilian, got: {}",
result
);
}
#[test]
fn test_brazilian_d_palatalization() {
let rules = combined_brazilian();
let result = rules.apply("dia");
assert!(
result.contains("d͡ʒ"),
"di should palatalize in Brazilian, got: {}",
result
);
}
#[test]
fn test_brazilian_r_pronunciation() {
let rules = combined_brazilian();
let result = rules.apply("rio");
assert!(
result == "iu" || result.starts_with('h') || result.starts_with('H'),
"Brazilian rio should become 'iu' (r->h->silent, o->u) or start with h, got: {}",
result
);
}
#[test]
fn test_european_s_pattern() {
let rules = combined_european();
let result = rules.apply("esta");
assert!(
result.contains("ʃ"),
"s before t should become sh in European, got: {}",
result
);
}
#[test]
fn test_dialect_differentiation() {
let european_rules = combined_european();
let brazilian_rules = combined_brazilian();
let brasil_pt = european_rules.apply("brasil");
let brasil_br = brazilian_rules.apply("brasil");
assert_ne!(
brasil_pt, brasil_br,
"Dialects should normalize 'brasil' differently: pt='{}', br='{}'",
brasil_pt, brasil_br
);
}
#[test]
fn test_dialect_r_difference() {
let european_rules = combined_european();
let brazilian_rules = combined_brazilian();
let rio_pt = european_rules.apply("rio");
let rio_br = brazilian_rules.apply("rio");
assert!(
rio_br == "iu" || rio_br.contains('h') || rio_br.contains('H'),
"Brazilian rio should become 'iu' (r silenced) or contain h, got: {}",
rio_br
);
assert_ne!(
rio_pt, rio_br,
"European and Brazilian should differ: pt='{}', br='{}'",
rio_pt, rio_br
);
}
#[test]
fn test_silent_h() {
let rules = base();
let result = rules.apply("hora");
assert!(!result.contains('h'), "h should be silent, got: {}", result);
}
#[test]
fn test_c_to_k() {
let rules = base();
let result = rules.apply("casa");
assert!(
result.starts_with('k'),
"c before a should become k, got: {}",
result
);
}
#[test]
fn test_g_softening() {
let rules = base();
let result_giro = rules.apply("giro");
assert!(
result_giro.contains("ʒ"),
"gi should become ʒi, got: {}",
result_giro
);
let result_gente = rules.apply("gente");
assert!(
result_gente.contains('ʒ') || result_gente.contains('ẽ'),
"gente should show G softening or nasalization, got: {}",
result_gente
);
}
}