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/french/base.llev");
let file = crate::phonetic::llev::parse_str(content)
.expect("Invalid embedded french/base.llev - this is a bug in liblevenshtein");
RuleSetChar::from_llev(&file)
.expect("Failed to compile French base rules - this is a bug in liblevenshtein")
})
}
pub fn standard() -> &'static RuleSetChar {
static RULESET: OnceLock<RuleSetChar> = OnceLock::new();
RULESET.get_or_init(|| {
let content = include_str!("../../../data/rules/french/standard.llev");
let file = crate::phonetic::llev::parse_str(content)
.expect("Invalid embedded french/standard.llev - this is a bug in liblevenshtein");
RuleSetChar::from_llev(&file)
.expect("Failed to compile Standard French rules - this is a bug in liblevenshtein")
})
}
pub fn canadian() -> &'static RuleSetChar {
static RULESET: OnceLock<RuleSetChar> = OnceLock::new();
RULESET.get_or_init(|| {
let content = include_str!("../../../data/rules/french/canadian.llev");
let file = crate::phonetic::llev::parse_str(content)
.expect("Invalid embedded french/canadian.llev - this is a bug in liblevenshtein");
RuleSetChar::from_llev(&file)
.expect("Failed to compile Canadian French rules - this is a bug in liblevenshtein")
})
}
pub fn combined_standard() -> &'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(standard().rules.iter().cloned());
RuleSetChar {
rules,
name: Some("Combined Standard French".to_string()),
version: None,
}
})
}
pub fn combined_canadian() -> &'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(canadian().rules.iter().cloned());
RuleSetChar {
rules,
name: Some("Combined Canadian French".to_string()),
version: None,
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_base_loads() {
let rules = base();
assert!(!rules.is_empty(), "French base rules should not be empty");
assert!(
rules.len() > 50,
"expected >50 base rules, got {}",
rules.len()
);
}
#[test]
fn test_standard_loads() {
let rules = standard();
assert!(
!rules.is_empty(),
"Standard French rules should not be empty"
);
}
#[test]
fn test_canadian_loads() {
let rules = canadian();
assert!(
!rules.is_empty(),
"Canadian French rules should not be empty"
);
}
#[test]
fn test_combined_standard_loads() {
let rules = combined_standard();
assert!(
!rules.is_empty(),
"Combined Standard French rules should not be empty"
);
let total = base().len() + standard().len();
assert_eq!(
rules.len(),
total,
"combined_standard should have all rules"
);
}
#[test]
fn test_combined_canadian_loads() {
let rules = combined_canadian();
assert!(
!rules.is_empty(),
"Combined Canadian French rules should not be empty"
);
let total = base().len() + canadian().len();
assert_eq!(
rules.len(),
total,
"combined_canadian should have all rules"
);
}
#[test]
fn test_silent_h() {
let rules = base();
let result = rules.apply("homme");
assert!(!result.contains('h'), "h should be silent, got: {}", result);
}
#[test]
fn test_ch_digraph() {
let rules = base();
let result = rules.apply("chat");
assert!(result.contains("ʃ"), "ch should become SH, got: {}", result);
}
#[test]
fn test_ph_digraph() {
let rules = base();
let result = rules.apply("photo");
assert!(result.contains('f'), "ph should become f, got: {}", result);
}
#[test]
fn test_nasal_vowels() {
let rules = base();
let result = rules.apply("bon");
assert!(
result.contains("ɔ̃"),
"on should become nasal ɔ̃, got: {}",
result
);
}
#[test]
fn test_oi_digraph() {
let rules = base();
let result = rules.apply("moi");
assert!(
result.contains("wa") || result.contains("va"),
"oi should become wa or va, got: {}",
result
);
}
#[test]
fn test_silent_final_consonants() {
let rules = base();
let result = rules.apply("petit");
assert!(
!result.ends_with('t'),
"final t should be silent, got: {}",
result
);
}
#[test]
fn test_c_softening() {
let rules = base();
let result = rules.apply("centre");
assert!(
result.starts_with('s'),
"c before e should become s, got: {}",
result
);
}
#[test]
fn test_g_softening() {
let rules = base();
let result = rules.apply("geste");
assert!(
result.contains("ʒ"),
"g before e should become ZH, got: {}",
result
);
}
#[test]
fn test_canadian_affrication() {
let rules = combined_canadian();
let result = rules.apply("tu");
assert!(
result.contains("t͡s"),
"t before u should affricate in Québécois, got: {}",
result
);
}
#[test]
fn test_standard_no_affrication() {
let rules = combined_standard();
let result = rules.apply("tu");
assert!(
!result.contains("t͡s"),
"Standard French should not affricate, got: {}",
result
);
}
#[test]
fn test_dialect_differentiation() {
let standard_rules = combined_standard();
let canadian_rules = combined_canadian();
let petit_fr = standard_rules.apply("petit");
let petit_ca = canadian_rules.apply("petit");
assert!(
petit_ca.contains("t͡s") || petit_fr != petit_ca,
"Dialects should normalize 'petit' differently: fr='{}', fr-ca='{}'",
petit_fr,
petit_ca
);
}
#[test]
fn test_qu_digraph() {
let rules = base();
let result = rules.apply("que");
assert!(result.contains('k'), "qu should become k, got: {}", result);
}
#[test]
fn test_gn_digraph() {
let rules = base();
let result = rules.apply("montagne");
assert!(result.contains("ɲ"), "gn should become NY, got: {}", result);
}
#[test]
fn test_eau_trigraph() {
let rules = base();
let result = rules.apply("eau");
assert_eq!(result, "o", "eau should become o, got: {}", result);
}
#[test]
fn test_tion_ending() {
let rules = base();
let result = rules.apply("nation");
assert!(
result.contains("sjɔ̃"),
"tion should become sjɔ̃, got: {}",
result
);
}
}