use super::common::PhoneticUnit;
use super::matching::{context_matches, pattern_matches_at};
use super::syllable::evaluate_syllable_expr;
use super::types::{Phone, RewriteRule};
use std::collections::HashSet;
pub use super::types::{
ContextByte, ContextChar, PhoneByte, PhoneChar, RewriteRuleByte, RewriteRuleChar,
};
pub type NormalizationResultByte = NormalizationResult<u8>;
pub type NormalizationResultChar = NormalizationResult<char>;
fn phones_to_string<U: PhoneticUnit>(phones: &[Phone<U>]) -> String {
let mut result = String::with_capacity(phones.len() * 7);
for phone in phones {
match phone {
Phone::Vowel(c) | Phone::Consonant(c) => result.push(U::to_char(*c)),
Phone::Digraph(c1, c2) => {
result.push(U::to_char(*c1));
result.push(U::to_char(*c2));
}
Phone::Trigraph(c1, c2, c3) => {
result.push(U::to_char(*c1));
result.push(U::to_char(*c2));
result.push(U::to_char(*c3));
}
Phone::Tetragraph(c1, c2, c3, c4) => {
result.push(U::to_char(*c1));
result.push(U::to_char(*c2));
result.push(U::to_char(*c3));
result.push(U::to_char(*c4));
}
Phone::Pentagraph(c1, c2, c3, c4, c5) => {
result.push(U::to_char(*c1));
result.push(U::to_char(*c2));
result.push(U::to_char(*c3));
result.push(U::to_char(*c4));
result.push(U::to_char(*c5));
}
Phone::Hexagraph(c1, c2, c3, c4, c5, c6) => {
result.push(U::to_char(*c1));
result.push(U::to_char(*c2));
result.push(U::to_char(*c3));
result.push(U::to_char(*c4));
result.push(U::to_char(*c5));
result.push(U::to_char(*c6));
}
Phone::Heptagraph(c1, c2, c3, c4, c5, c6, c7) => {
result.push(U::to_char(*c1));
result.push(U::to_char(*c2));
result.push(U::to_char(*c3));
result.push(U::to_char(*c4));
result.push(U::to_char(*c5));
result.push(U::to_char(*c6));
result.push(U::to_char(*c7));
}
Phone::Sequence(s) => {
for c in s {
result.push(U::to_char(*c));
}
}
Phone::Silent => {}
}
}
result
}
#[cfg(feature = "perf-instrumentation")]
use std::sync::atomic::{AtomicUsize, Ordering};
#[cfg(feature = "perf-instrumentation")]
static BYTES_COPIED: AtomicUsize = AtomicUsize::new(0);
#[cfg(feature = "perf-instrumentation")]
static ALLOCATIONS: AtomicUsize = AtomicUsize::new(0);
#[cfg(feature = "perf-instrumentation")]
pub fn get_perf_stats() -> (usize, usize) {
(
BYTES_COPIED.load(Ordering::Relaxed),
ALLOCATIONS.load(Ordering::Relaxed),
)
}
#[cfg(feature = "perf-instrumentation")]
pub fn reset_perf_stats() {
BYTES_COPIED.store(0, Ordering::Relaxed);
ALLOCATIONS.store(0, Ordering::Relaxed);
}
pub const MAX_EXPANSION_FACTOR: usize = 20;
pub const MAX_TOTAL_EXPANSION: usize = 100;
#[inline]
pub fn has_position_dependent_rules<U: PhoneticUnit>(rules: &[RewriteRule<U>]) -> bool {
rules.iter().any(|r| r.context.is_position_dependent())
}
#[inline]
pub fn can_apply_at<U: PhoneticUnit>(rule: &RewriteRule<U>, s: &[Phone<U>], pos: usize) -> bool {
if !pattern_matches_at(&rule.pattern, s, pos) {
return false;
}
if !context_matches(&rule.context, s, pos, rule.pattern.len()) {
return false;
}
if let Some(ref syllable_expr) = rule.syllable_condition {
let word = phones_to_string(s);
if !evaluate_syllable_expr(syllable_expr, &word, pos) {
return false;
}
}
true
}
pub fn apply_rule_at<U: PhoneticUnit>(
rule: &RewriteRule<U>,
s: &[Phone<U>],
pos: usize,
) -> Option<Vec<Phone<U>>> {
if !can_apply_at(rule, s, pos) {
return None;
}
let mut result = Vec::with_capacity(s.len() + MAX_EXPANSION_FACTOR);
#[cfg(feature = "perf-instrumentation")]
{
ALLOCATIONS.fetch_add(1, Ordering::Relaxed);
let bytes_copied = pos + rule.replacement.len() + (s.len() - pos - rule.pattern.len());
BYTES_COPIED.fetch_add(bytes_copied, Ordering::Relaxed);
}
result.extend_from_slice(&s[..pos]);
result.extend_from_slice(&rule.replacement);
result.extend_from_slice(&s[(pos + rule.pattern.len())..]);
Some(result)
}
pub fn find_first_match<U: PhoneticUnit>(rule: &RewriteRule<U>, s: &[Phone<U>]) -> Option<usize> {
find_first_match_from(rule, s, 0)
}
#[inline]
pub fn find_first_match_from<U: PhoneticUnit>(
rule: &RewriteRule<U>,
s: &[Phone<U>],
start_pos: usize,
) -> Option<usize> {
for pos in start_pos..=s.len() {
if can_apply_at(rule, s, pos) {
return Some(pos);
}
}
None
}
pub fn apply_rules_seq<U: PhoneticUnit>(
rules: &[RewriteRule<U>],
s: &[Phone<U>],
fuel: usize,
) -> Option<Vec<Phone<U>>> {
let mut current = s.to_vec();
let original_len = s.len();
#[cfg(feature = "perf-instrumentation")]
{
ALLOCATIONS.fetch_add(1, Ordering::Relaxed);
BYTES_COPIED.fetch_add(s.len(), Ordering::Relaxed);
}
let mut remaining_fuel = fuel;
loop {
if remaining_fuel == 0 {
return Some(current);
}
let mut applied = false;
for rule in rules {
if let Some(pos) = find_first_match(rule, ¤t) {
if let Some(new_s) = apply_rule_at(rule, ¤t, pos) {
if new_s.len() > original_len + MAX_TOTAL_EXPANSION {
eprintln!(
"[phonetic] Warning: Normalization exceeded expansion limit \
({} > {} + {}). Returning current state to prevent runaway expansion. \
Consider revising rules to avoid pathological interactions.",
new_s.len(),
original_len,
MAX_TOTAL_EXPANSION
);
return Some(current);
}
current = new_s;
remaining_fuel -= 1;
applied = true;
break; }
}
}
if !applied {
return Some(current);
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NormalizationResult<U: PhoneticUnit> {
FixedPoint(Vec<Phone<U>>),
Cycle(HashSet<Vec<Phone<U>>>),
FuelExhausted(Vec<Phone<U>>),
}
impl<U: PhoneticUnit> NormalizationResult<U> {
pub fn canonical(&self) -> Vec<Phone<U>> {
match self {
NormalizationResult::FixedPoint(s) => s.clone(),
NormalizationResult::FuelExhausted(s) => s.clone(),
NormalizationResult::Cycle(set) => {
set.iter()
.min_by_key(|v| v.len())
.cloned()
.unwrap_or_default()
}
}
}
pub fn all_forms(&self) -> HashSet<Vec<Phone<U>>> {
match self {
NormalizationResult::FixedPoint(s) => {
let mut set = HashSet::new();
set.insert(s.clone());
set
}
NormalizationResult::FuelExhausted(s) => {
let mut set = HashSet::new();
set.insert(s.clone());
set
}
NormalizationResult::Cycle(set) => set.clone(),
}
}
pub fn is_cycle(&self) -> bool {
matches!(self, NormalizationResult::Cycle(_))
}
pub fn is_fixed_point(&self) -> bool {
matches!(self, NormalizationResult::FixedPoint(_))
}
}
pub fn apply_rules_with_cycle_detection<U: PhoneticUnit>(
rules: &[RewriteRule<U>],
s: &[Phone<U>],
fuel: usize,
) -> NormalizationResult<U> {
let mut current = s.to_vec();
let mut seen: HashSet<Vec<Phone<U>>> = HashSet::new();
let mut remaining_fuel = fuel;
seen.insert(current.clone());
loop {
if remaining_fuel == 0 {
return NormalizationResult::FuelExhausted(current);
}
let mut applied = false;
for rule in rules {
if let Some(pos) = find_first_match(rule, ¤t) {
if let Some(new_s) = apply_rule_at(rule, ¤t, pos) {
if seen.contains(&new_s) {
eprintln!(
"[phonetic] Warning: Cycle detected in rule application. \
{} equivalent forms found and will all be indexed. \
Consider revising rules to avoid cycles.",
seen.len()
);
return NormalizationResult::Cycle(seen);
}
seen.insert(new_s.clone());
current = new_s;
remaining_fuel -= 1;
applied = true;
break; }
}
}
if !applied {
return NormalizationResult::FixedPoint(current);
}
}
}
pub fn apply_rules_seq_optimized<U: PhoneticUnit>(
rules: &[RewriteRule<U>],
s: &[Phone<U>],
fuel: usize,
) -> Option<Vec<Phone<U>>> {
debug_assert!(
!has_position_dependent_rules(rules),
"apply_rules_seq_optimized requires no position-dependent rules (Context::Final)"
);
let mut current = s.to_vec();
#[cfg(feature = "perf-instrumentation")]
{
ALLOCATIONS.fetch_add(1, Ordering::Relaxed);
BYTES_COPIED.fetch_add(s.len(), Ordering::Relaxed);
}
let mut remaining_fuel = fuel;
let mut last_pos: usize = 0;
loop {
if remaining_fuel == 0 {
return Some(current);
}
let mut applied = false;
for rule in rules {
if let Some(pos) = find_first_match_from(rule, ¤t, last_pos) {
if let Some(new_s) = apply_rule_at(rule, ¤t, pos) {
current = new_s;
remaining_fuel -= 1;
last_pos = pos; applied = true;
break; }
}
}
if !applied {
return Some(current);
}
}
}
#[inline]
pub fn has_position_dependent_rules_byte(rules: &[RewriteRule<u8>]) -> bool {
has_position_dependent_rules(rules)
}
#[inline]
pub fn can_apply_at_byte(rule: &RewriteRule<u8>, s: &[Phone<u8>], pos: usize) -> bool {
can_apply_at(rule, s, pos)
}
#[inline]
pub fn apply_rule_at_byte(
rule: &RewriteRule<u8>,
s: &[Phone<u8>],
pos: usize,
) -> Option<Vec<Phone<u8>>> {
apply_rule_at(rule, s, pos)
}
#[inline]
pub fn find_first_match_byte(rule: &RewriteRule<u8>, s: &[Phone<u8>]) -> Option<usize> {
find_first_match(rule, s)
}
#[inline]
pub fn find_first_match_from_byte(
rule: &RewriteRule<u8>,
s: &[Phone<u8>],
start_pos: usize,
) -> Option<usize> {
find_first_match_from(rule, s, start_pos)
}
#[inline]
pub fn apply_rules_seq_byte(
rules: &[RewriteRule<u8>],
s: &[Phone<u8>],
fuel: usize,
) -> Option<Vec<Phone<u8>>> {
apply_rules_seq(rules, s, fuel)
}
#[inline]
pub fn apply_rules_with_cycle_detection_byte(
rules: &[RewriteRule<u8>],
s: &[Phone<u8>],
fuel: usize,
) -> NormalizationResult<u8> {
apply_rules_with_cycle_detection(rules, s, fuel)
}
#[inline]
pub fn apply_rules_seq_optimized_byte(
rules: &[RewriteRule<u8>],
s: &[Phone<u8>],
fuel: usize,
) -> Option<Vec<Phone<u8>>> {
apply_rules_seq_optimized(rules, s, fuel)
}
#[inline]
pub fn has_position_dependent_rules_char(rules: &[RewriteRule<char>]) -> bool {
has_position_dependent_rules(rules)
}
#[inline]
pub fn can_apply_at_char(rule: &RewriteRule<char>, s: &[Phone<char>], pos: usize) -> bool {
can_apply_at(rule, s, pos)
}
#[inline]
pub fn apply_rule_at_char(
rule: &RewriteRule<char>,
s: &[Phone<char>],
pos: usize,
) -> Option<Vec<Phone<char>>> {
apply_rule_at(rule, s, pos)
}
#[inline]
pub fn find_first_match_char(rule: &RewriteRule<char>, s: &[Phone<char>]) -> Option<usize> {
find_first_match(rule, s)
}
#[inline]
pub fn find_first_match_from_char(
rule: &RewriteRule<char>,
s: &[Phone<char>],
start_pos: usize,
) -> Option<usize> {
find_first_match_from(rule, s, start_pos)
}
#[inline]
pub fn apply_rules_seq_char(
rules: &[RewriteRule<char>],
s: &[Phone<char>],
fuel: usize,
) -> Option<Vec<Phone<char>>> {
apply_rules_seq(rules, s, fuel)
}
#[inline]
pub fn apply_rules_with_cycle_detection_char(
rules: &[RewriteRule<char>],
s: &[Phone<char>],
fuel: usize,
) -> NormalizationResult<char> {
apply_rules_with_cycle_detection(rules, s, fuel)
}
#[inline]
pub fn apply_rules_seq_optimized_char(
rules: &[RewriteRule<char>],
s: &[Phone<char>],
fuel: usize,
) -> Option<Vec<Phone<char>>> {
apply_rules_seq_optimized(rules, s, fuel)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::phonetic::types::{Context, Phone};
#[test]
fn test_apply_rule_at_success() {
let rule = RewriteRule::<u8> {
rule_id: 1,
rule_name: "gh → f".to_string(),
pattern: vec![Phone::Consonant(b'g'), Phone::Consonant(b'h')],
replacement: vec![Phone::Consonant(b'f')],
context: Context::Anywhere,
weight: 0.15,
syllable_condition: None,
};
let s = vec![
Phone::Vowel(b'e'),
Phone::Consonant(b'g'),
Phone::Consonant(b'h'),
];
let result = apply_rule_at(&rule, &s, 1);
assert_eq!(
result,
Some(vec![Phone::Vowel(b'e'), Phone::Consonant(b'f')])
);
}
#[test]
fn test_apply_rule_at_no_match() {
let rule = RewriteRule::<u8> {
rule_id: 1,
rule_name: "gh → f".to_string(),
pattern: vec![Phone::Consonant(b'g'), Phone::Consonant(b'h')],
replacement: vec![Phone::Consonant(b'f')],
context: Context::Anywhere,
weight: 0.15,
syllable_condition: None,
};
let s = vec![Phone::Vowel(b'e'), Phone::Consonant(b'k')];
let result = apply_rule_at(&rule, &s, 0);
assert_eq!(result, None);
}
#[test]
fn test_apply_rule_at_context_fail() {
let rule = RewriteRule::<u8> {
rule_id: 1,
rule_name: "gh → f (initial only)".to_string(),
pattern: vec![Phone::Consonant(b'g'), Phone::Consonant(b'h')],
replacement: vec![Phone::Consonant(b'f')],
context: Context::Initial,
weight: 0.15,
syllable_condition: None,
};
let s = vec![
Phone::Vowel(b'e'),
Phone::Consonant(b'g'),
Phone::Consonant(b'h'),
];
let result = apply_rule_at(&rule, &s, 1);
assert_eq!(result, None);
let s2 = vec![Phone::Consonant(b'g'), Phone::Consonant(b'h')];
let result2 = apply_rule_at(&rule, &s2, 0);
assert_eq!(result2, Some(vec![Phone::Consonant(b'f')]));
}
#[test]
fn test_find_first_match() {
let rule = RewriteRule::<u8> {
rule_id: 1,
rule_name: "gh → f".to_string(),
pattern: vec![Phone::Consonant(b'g'), Phone::Consonant(b'h')],
replacement: vec![Phone::Consonant(b'f')],
context: Context::Anywhere,
weight: 0.15,
syllable_condition: None,
};
let s = vec![
Phone::Vowel(b'e'),
Phone::Consonant(b'g'),
Phone::Consonant(b'h'),
Phone::Vowel(b'o'),
];
let pos = find_first_match(&rule, &s);
assert_eq!(pos, Some(1));
}
#[test]
fn test_find_first_match_none() {
let rule = RewriteRule::<u8> {
rule_id: 1,
rule_name: "gh → f".to_string(),
pattern: vec![Phone::Consonant(b'g'), Phone::Consonant(b'h')],
replacement: vec![Phone::Consonant(b'f')],
context: Context::Anywhere,
weight: 0.15,
syllable_condition: None,
};
let s = vec![Phone::Vowel(b'e'), Phone::Consonant(b'k')];
let pos = find_first_match(&rule, &s);
assert_eq!(pos, None);
}
#[test]
fn test_apply_rules_seq_single_rule() {
let rule = RewriteRule::<u8> {
rule_id: 1,
rule_name: "gh → f".to_string(),
pattern: vec![Phone::Consonant(b'g'), Phone::Consonant(b'h')],
replacement: vec![Phone::Consonant(b'f')],
context: Context::Anywhere,
weight: 0.15,
syllable_condition: None,
};
let s = vec![
Phone::Vowel(b'e'),
Phone::Consonant(b'g'),
Phone::Consonant(b'h'),
];
let result = apply_rules_seq(&[rule], &s, 100);
assert_eq!(
result,
Some(vec![Phone::Vowel(b'e'), Phone::Consonant(b'f')])
);
}
#[test]
fn test_apply_rules_seq_multiple_applications() {
let rule = RewriteRule::<u8> {
rule_id: 1,
rule_name: "gh → f".to_string(),
pattern: vec![Phone::Consonant(b'g'), Phone::Consonant(b'h')],
replacement: vec![Phone::Consonant(b'f')],
context: Context::Anywhere,
weight: 0.15,
syllable_condition: None,
};
let s = vec![
Phone::Consonant(b'g'),
Phone::Consonant(b'h'),
Phone::Consonant(b'g'),
Phone::Consonant(b'h'),
];
let result = apply_rules_seq(&[rule], &s, 100);
assert_eq!(
result,
Some(vec![Phone::Consonant(b'f'), Phone::Consonant(b'f')])
);
}
#[test]
fn test_apply_rules_seq_fixed_point() {
let rule = RewriteRule::<u8> {
rule_id: 1,
rule_name: "gh → f".to_string(),
pattern: vec![Phone::Consonant(b'g'), Phone::Consonant(b'h')],
replacement: vec![Phone::Consonant(b'f')],
context: Context::Anywhere,
weight: 0.15,
syllable_condition: None,
};
let s = vec![Phone::Vowel(b'e'), Phone::Consonant(b'f')];
let result = apply_rules_seq(&[rule], &s, 100);
assert_eq!(
result,
Some(vec![Phone::Vowel(b'e'), Phone::Consonant(b'f')])
);
}
#[test]
fn test_apply_rule_at_char_success() {
let rule = RewriteRule::<char> {
rule_id: 1,
rule_name: "gh → f".to_string(),
pattern: vec![Phone::Consonant('g'), Phone::Consonant('h')],
replacement: vec![Phone::Consonant('f')],
context: Context::Anywhere,
weight: 0.15,
syllable_condition: None,
};
let s = vec![
Phone::Vowel('e'),
Phone::Consonant('g'),
Phone::Consonant('h'),
];
let result = apply_rule_at(&rule, &s, 1);
assert_eq!(result, Some(vec![Phone::Vowel('e'), Phone::Consonant('f')]));
}
#[test]
fn test_apply_rules_seq_char() {
let rule = RewriteRule::<char> {
rule_id: 1,
rule_name: "gh → f".to_string(),
pattern: vec![Phone::Consonant('g'), Phone::Consonant('h')],
replacement: vec![Phone::Consonant('f')],
context: Context::Anywhere,
weight: 0.15,
syllable_condition: None,
};
let s = vec![
Phone::Vowel('e'),
Phone::Consonant('g'),
Phone::Consonant('h'),
];
let result = apply_rules_seq(&[rule], &s, 100);
assert_eq!(result, Some(vec![Phone::Vowel('e'), Phone::Consonant('f')]));
}
#[test]
fn test_has_position_dependent_rules_empty() {
let rules: Vec<RewriteRule<u8>> = vec![];
assert!(!has_position_dependent_rules(&rules));
}
#[test]
fn test_has_position_dependent_rules_no_final() {
let rules = vec![
RewriteRule::<u8> {
rule_id: 1,
rule_name: "test".to_string(),
pattern: vec![Phone::Consonant(b'g')],
replacement: vec![Phone::Consonant(b'k')],
context: Context::Anywhere,
weight: 1.0,
syllable_condition: None,
},
RewriteRule::<u8> {
rule_id: 2,
rule_name: "test2".to_string(),
pattern: vec![Phone::Consonant(b'c')],
replacement: vec![Phone::Consonant(b's')],
context: Context::Initial,
weight: 1.0,
syllable_condition: None,
},
];
assert!(!has_position_dependent_rules(&rules));
}
#[test]
fn test_has_position_dependent_rules_with_final() {
let rules = vec![
RewriteRule::<u8> {
rule_id: 1,
rule_name: "test".to_string(),
pattern: vec![Phone::Consonant(b'g')],
replacement: vec![Phone::Consonant(b'k')],
context: Context::Anywhere,
weight: 1.0,
syllable_condition: None,
},
RewriteRule::<u8> {
rule_id: 2,
rule_name: "final_rule".to_string(),
pattern: vec![Phone::Vowel(b'e')],
replacement: vec![],
context: Context::Final,
weight: 1.0,
syllable_condition: None,
},
];
assert!(has_position_dependent_rules(&rules));
}
#[test]
fn test_find_first_match_from_start() {
let rule = RewriteRule::<u8> {
rule_id: 1,
rule_name: "gh → f".to_string(),
pattern: vec![Phone::Consonant(b'g'), Phone::Consonant(b'h')],
replacement: vec![Phone::Consonant(b'f')],
context: Context::Anywhere,
weight: 0.15,
syllable_condition: None,
};
let s = vec![
Phone::Vowel(b'e'),
Phone::Consonant(b'g'),
Phone::Consonant(b'h'),
Phone::Vowel(b'o'),
];
assert_eq!(find_first_match_from(&rule, &s, 0), Some(1));
assert_eq!(find_first_match_from(&rule, &s, 1), Some(1));
assert_eq!(find_first_match_from(&rule, &s, 2), None);
}
#[test]
fn test_find_first_match_from_multiple_occurrences() {
let rule = RewriteRule::<u8> {
rule_id: 1,
rule_name: "gh → f".to_string(),
pattern: vec![Phone::Consonant(b'g'), Phone::Consonant(b'h')],
replacement: vec![Phone::Consonant(b'f')],
context: Context::Anywhere,
weight: 0.15,
syllable_condition: None,
};
let s = vec![
Phone::Vowel(b'e'),
Phone::Consonant(b'g'),
Phone::Consonant(b'h'),
Phone::Vowel(b'o'),
Phone::Consonant(b'g'),
Phone::Consonant(b'h'),
Phone::Vowel(b'a'),
];
assert_eq!(find_first_match_from(&rule, &s, 0), Some(1));
assert_eq!(find_first_match_from(&rule, &s, 2), Some(4));
assert_eq!(find_first_match_from(&rule, &s, 5), None);
}
#[test]
fn test_apply_rules_seq_optimized_produces_same_result() {
let rule = RewriteRule::<u8> {
rule_id: 1,
rule_name: "gh → f".to_string(),
pattern: vec![Phone::Consonant(b'g'), Phone::Consonant(b'h')],
replacement: vec![Phone::Consonant(b'f')],
context: Context::Anywhere,
weight: 0.15,
syllable_condition: None,
};
let s = vec![
Phone::Vowel(b'e'),
Phone::Consonant(b'g'),
Phone::Consonant(b'h'),
Phone::Vowel(b'o'),
Phone::Consonant(b'g'),
Phone::Consonant(b'h'),
];
let standard_result = apply_rules_seq(&[rule.clone()], &s, 100);
let optimized_result = apply_rules_seq_optimized(&[rule], &s, 100);
assert_eq!(standard_result, optimized_result);
}
#[test]
fn test_apply_rules_seq_optimized_fixed_point() {
let rule = RewriteRule::<u8> {
rule_id: 1,
rule_name: "gh → f".to_string(),
pattern: vec![Phone::Consonant(b'g'), Phone::Consonant(b'h')],
replacement: vec![Phone::Consonant(b'f')],
context: Context::Anywhere,
weight: 0.15,
syllable_condition: None,
};
let s = vec![Phone::Vowel(b'e'), Phone::Consonant(b'f')];
let result = apply_rules_seq_optimized(&[rule], &s, 100);
assert_eq!(
result,
Some(vec![Phone::Vowel(b'e'), Phone::Consonant(b'f')])
);
}
#[test]
fn test_has_position_dependent_rules_char_empty() {
let rules: Vec<RewriteRule<char>> = vec![];
assert!(!has_position_dependent_rules(&rules));
}
#[test]
fn test_has_position_dependent_rules_char_no_final() {
let rules = vec![RewriteRule::<char> {
rule_id: 1,
rule_name: "test".to_string(),
pattern: vec![Phone::Consonant('g')],
replacement: vec![Phone::Consonant('k')],
context: Context::Anywhere,
weight: 1.0,
syllable_condition: None,
}];
assert!(!has_position_dependent_rules(&rules));
}
#[test]
fn test_has_position_dependent_rules_char_with_final() {
let rules = vec![RewriteRule::<char> {
rule_id: 1,
rule_name: "final_rule".to_string(),
pattern: vec![Phone::Vowel('e')],
replacement: vec![],
context: Context::Final,
weight: 1.0,
syllable_condition: None,
}];
assert!(has_position_dependent_rules(&rules));
}
#[test]
fn test_find_first_match_from_char() {
let rule = RewriteRule::<char> {
rule_id: 1,
rule_name: "gh → f".to_string(),
pattern: vec![Phone::Consonant('g'), Phone::Consonant('h')],
replacement: vec![Phone::Consonant('f')],
context: Context::Anywhere,
weight: 0.15,
syllable_condition: None,
};
let s = vec![
Phone::Vowel('e'),
Phone::Consonant('g'),
Phone::Consonant('h'),
Phone::Vowel('o'),
];
assert_eq!(find_first_match_from(&rule, &s, 0), Some(1));
assert_eq!(find_first_match_from(&rule, &s, 1), Some(1));
assert_eq!(find_first_match_from(&rule, &s, 2), None);
}
#[test]
fn test_apply_rules_seq_optimized_char_produces_same_result() {
let rule = RewriteRule::<char> {
rule_id: 1,
rule_name: "gh → f".to_string(),
pattern: vec![Phone::Consonant('g'), Phone::Consonant('h')],
replacement: vec![Phone::Consonant('f')],
context: Context::Anywhere,
weight: 0.15,
syllable_condition: None,
};
let s = vec![
Phone::Vowel('e'),
Phone::Consonant('g'),
Phone::Consonant('h'),
Phone::Vowel('o'),
Phone::Consonant('g'),
Phone::Consonant('h'),
];
let standard_result = apply_rules_seq(&[rule.clone()], &s, 100);
let optimized_result = apply_rules_seq_optimized(&[rule], &s, 100);
assert_eq!(standard_result, optimized_result);
}
#[test]
fn test_cycle_detection_simple_cycle() {
let rule_ab_to_ba = RewriteRule::<u8> {
rule_id: 1,
rule_name: "ab → ba".to_string(),
pattern: vec![Phone::Vowel(b'a'), Phone::Consonant(b'b')],
replacement: vec![Phone::Consonant(b'b'), Phone::Vowel(b'a')],
context: Context::Anywhere,
weight: 0.0,
syllable_condition: None,
};
let rule_ba_to_ab = RewriteRule::<u8> {
rule_id: 2,
rule_name: "ba → ab".to_string(),
pattern: vec![Phone::Consonant(b'b'), Phone::Vowel(b'a')],
replacement: vec![Phone::Vowel(b'a'), Phone::Consonant(b'b')],
context: Context::Anywhere,
weight: 0.0,
syllable_condition: None,
};
let rules = vec![rule_ab_to_ba, rule_ba_to_ab];
let input = vec![Phone::Vowel(b'a'), Phone::Consonant(b'b')];
let result = apply_rules_with_cycle_detection(&rules, &input, 100);
assert!(
result.is_cycle(),
"Expected cycle detection, got {:?}",
result
);
if let NormalizationResult::Cycle(forms) = &result {
assert!(forms.contains(&vec![Phone::Vowel(b'a'), Phone::Consonant(b'b')]));
assert!(forms.contains(&vec![Phone::Consonant(b'b'), Phone::Vowel(b'a')]));
assert_eq!(forms.len(), 2, "Expected exactly 2 forms in cycle");
}
}
#[test]
fn test_cycle_detection_fixed_point() {
let rule = RewriteRule::<u8> {
rule_id: 1,
rule_name: "ph → f".to_string(),
pattern: vec![Phone::Consonant(b'p'), Phone::Consonant(b'h')],
replacement: vec![Phone::Consonant(b'f')],
context: Context::Anywhere,
weight: 0.15,
syllable_condition: None,
};
let input = vec![
Phone::Consonant(b'p'),
Phone::Consonant(b'h'),
Phone::Vowel(b'o'),
Phone::Consonant(b'n'),
Phone::Vowel(b'e'),
];
let result = apply_rules_with_cycle_detection(&[rule], &input, 100);
assert!(
result.is_fixed_point(),
"Expected fixed point, got {:?}",
result
);
if let NormalizationResult::FixedPoint(form) = &result {
assert_eq!(
form,
&vec![
Phone::Consonant(b'f'),
Phone::Vowel(b'o'),
Phone::Consonant(b'n'),
Phone::Vowel(b'e'),
]
);
}
}
#[test]
fn test_cycle_detection_fuel_exhausted() {
let rule = RewriteRule::<u8> {
rule_id: 1,
rule_name: "a → aa".to_string(),
pattern: vec![Phone::Vowel(b'a')],
replacement: vec![Phone::Vowel(b'a'), Phone::Vowel(b'a')],
context: Context::Anywhere,
weight: 0.0,
syllable_condition: None,
};
let input = vec![Phone::Vowel(b'a')];
let result = apply_rules_with_cycle_detection(&[rule], &input, 3);
assert!(
matches!(result, NormalizationResult::FuelExhausted(_)),
"Expected fuel exhaustion, got {:?}",
result
);
}
#[test]
fn test_cycle_detection_all_forms() {
let rule_a_to_b = RewriteRule::<u8> {
rule_id: 1,
rule_name: "a → b".to_string(),
pattern: vec![Phone::Vowel(b'a')],
replacement: vec![Phone::Consonant(b'b')],
context: Context::Anywhere,
weight: 0.0,
syllable_condition: None,
};
let rule_b_to_c = RewriteRule::<u8> {
rule_id: 2,
rule_name: "b → c".to_string(),
pattern: vec![Phone::Consonant(b'b')],
replacement: vec![Phone::Consonant(b'c')],
context: Context::Anywhere,
weight: 0.0,
syllable_condition: None,
};
let rule_c_to_a = RewriteRule::<u8> {
rule_id: 3,
rule_name: "c → a".to_string(),
pattern: vec![Phone::Consonant(b'c')],
replacement: vec![Phone::Vowel(b'a')],
context: Context::Anywhere,
weight: 0.0,
syllable_condition: None,
};
let rules = vec![rule_a_to_b, rule_b_to_c, rule_c_to_a];
let input = vec![Phone::Vowel(b'a')];
let result = apply_rules_with_cycle_detection(&rules, &input, 100);
assert!(result.is_cycle());
let all_forms = result.all_forms();
assert_eq!(all_forms.len(), 3, "Expected 3 forms in cycle");
assert!(all_forms.contains(&vec![Phone::Vowel(b'a')]));
assert!(all_forms.contains(&vec![Phone::Consonant(b'b')]));
assert!(all_forms.contains(&vec![Phone::Consonant(b'c')]));
}
#[test]
fn test_normalization_result_canonical_shortest() {
let mut forms = HashSet::new();
forms.insert(vec![Phone::<u8>::Vowel(b'a'), Phone::Vowel(b'a')]);
forms.insert(vec![Phone::<u8>::Vowel(b'b')]);
forms.insert(vec![
Phone::<u8>::Vowel(b'c'),
Phone::Vowel(b'c'),
Phone::Vowel(b'c'),
]);
let result = NormalizationResult::Cycle(forms);
assert_eq!(result.canonical(), vec![Phone::Vowel(b'b')]);
}
}