use crate::phonetic::types::{PhoneChar, RewriteRuleChar};
pub fn expand_phonetic_alternatives_char(input: &str, rules: &[RewriteRuleChar]) -> String {
let reverse_map = build_reverse_map(rules);
let chars: Vec<char> = input.chars().collect();
let n = chars.len();
if n == 0 {
return String::new();
}
let mut dp: Vec<Vec<String>> = vec![Vec::new(); n + 1];
dp[0].push(String::new());
for i in 0..n {
if dp[i].is_empty() {
continue; }
let remaining = &input[char_byte_index(input, i)..];
let mut matches_at_i: Vec<(usize, Vec<String>)> = Vec::new();
for (replacement, originals) in &reverse_map {
if remaining.starts_with(replacement.as_str()) {
let len = replacement.chars().count();
let mut alternatives: Vec<String> =
originals.iter().map(|s| regex_escape(s)).collect();
let escaped_replacement = regex_escape(replacement);
if !alternatives.contains(&escaped_replacement) {
alternatives.push(escaped_replacement);
}
matches_at_i.push((len, alternatives));
}
}
let single_char = regex_escape_char(chars[i]);
let mut has_single = false;
for (len, _) in &matches_at_i {
if *len == 1 {
has_single = true;
break;
}
}
if !has_single {
matches_at_i.push((1, vec![single_char]));
}
let prefixes_at_i: Vec<String> = dp[i].clone();
for (len, alternatives) in matches_at_i {
let next_pos = i + len;
if next_pos > n {
continue;
}
let segment = if alternatives.len() > 1 {
format!("({})", alternatives.join("|"))
} else {
alternatives[0].clone()
};
for prefix in &prefixes_at_i {
let new_expansion = format!("{}{}", prefix, segment);
dp[next_pos].push(new_expansion);
}
}
}
let mut final_patterns: Vec<String> = dp[n].clone();
final_patterns.sort();
final_patterns.dedup();
if final_patterns.is_empty() {
return regex_escape(input);
}
if final_patterns.len() == 1 {
return final_patterns
.into_iter()
.next()
.expect("len==1 checked above");
}
format!("({})", final_patterns.join("|"))
}
type ReverseMap = Vec<(String, Vec<String>)>;
fn build_reverse_map(rules: &[RewriteRuleChar]) -> ReverseMap {
use std::collections::HashMap;
let mut map: HashMap<String, Vec<String>> = HashMap::new();
for rule in rules {
let original = phones_to_string(&rule.pattern);
let replacement = phones_to_string(&rule.replacement);
if original != replacement && !replacement.is_empty() {
map.entry(replacement).or_default().push(original);
}
}
let mut entries: Vec<(String, Vec<String>)> = map.into_iter().collect();
entries.sort_by(|a, b| b.0.len().cmp(&a.0.len()));
entries
}
fn phones_to_string(phones: &[PhoneChar]) -> String {
let mut result = String::new();
for phone in phones {
match phone {
PhoneChar::Vowel(c) | PhoneChar::Consonant(c) => result.push(*c),
PhoneChar::Digraph(c1, c2) => {
result.push(*c1);
result.push(*c2);
}
PhoneChar::Trigraph(c1, c2, c3) => {
result.push(*c1);
result.push(*c2);
result.push(*c3);
}
PhoneChar::Tetragraph(c1, c2, c3, c4) => {
result.push(*c1);
result.push(*c2);
result.push(*c3);
result.push(*c4);
}
PhoneChar::Pentagraph(c1, c2, c3, c4, c5) => {
result.push(*c1);
result.push(*c2);
result.push(*c3);
result.push(*c4);
result.push(*c5);
}
PhoneChar::Hexagraph(c1, c2, c3, c4, c5, c6) => {
result.push(*c1);
result.push(*c2);
result.push(*c3);
result.push(*c4);
result.push(*c5);
result.push(*c6);
}
PhoneChar::Heptagraph(c1, c2, c3, c4, c5, c6, c7) => {
result.push(*c1);
result.push(*c2);
result.push(*c3);
result.push(*c4);
result.push(*c5);
result.push(*c6);
result.push(*c7);
}
PhoneChar::Sequence(s) => {
for c in s {
result.push(*c);
}
}
PhoneChar::Silent => {}
}
}
result
}
fn char_byte_index(s: &str, char_index: usize) -> usize {
s.char_indices()
.nth(char_index)
.map(|(i, _)| i)
.unwrap_or(s.len())
}
fn regex_escape(s: &str) -> String {
let mut escaped = String::with_capacity(s.len() * 2);
for c in s.chars() {
escaped.push_str(®ex_escape_char(c));
}
escaped
}
fn regex_escape_char(c: char) -> String {
match c {
'.' | '*' | '+' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\' => {
format!("\\{}", c)
}
_ => c.to_string(),
}
}
pub fn expand_with_costs(input: &str, rules: &[RewriteRuleChar]) -> (String, f64) {
let reverse_map = build_reverse_map_with_costs(rules);
let mut pattern = String::new();
let mut total_cost = 0.0;
let chars: Vec<char> = input.chars().collect();
let mut i = 0;
while i < chars.len() {
let remaining = &input[char_byte_index(input, i)..];
let mut matched = false;
for (replacement, originals_with_costs) in &reverse_map {
if remaining.starts_with(replacement.as_str()) {
let mut alternatives: Vec<&str> = originals_with_costs
.iter()
.map(|(s, _)| s.as_str())
.collect();
let max_cost = originals_with_costs
.iter()
.map(|(_, cost)| *cost)
.fold(0.0_f64, f64::max);
total_cost += max_cost;
if !alternatives.contains(&replacement.as_str()) {
alternatives.push(replacement);
}
if alternatives.len() > 1 {
pattern.push('(');
for (j, alt) in alternatives.iter().enumerate() {
if j > 0 {
pattern.push('|');
}
pattern.push_str(®ex_escape(alt));
}
pattern.push(')');
} else {
pattern.push_str(®ex_escape(alternatives[0]));
}
i += replacement.chars().count();
matched = true;
break;
}
}
if !matched {
pattern.push_str(®ex_escape_char(chars[i]));
i += 1;
}
}
(pattern, total_cost)
}
type ReverseMapWithCosts = Vec<(String, Vec<(String, f64)>)>;
fn build_reverse_map_with_costs(rules: &[RewriteRuleChar]) -> ReverseMapWithCosts {
use std::collections::HashMap;
let mut map: HashMap<String, Vec<(String, f64)>> = HashMap::new();
for rule in rules {
let original = phones_to_string(&rule.pattern);
let replacement = phones_to_string(&rule.replacement);
if original != replacement && !replacement.is_empty() {
map.entry(replacement)
.or_default()
.push((original, rule.weight));
}
}
let mut entries: Vec<(String, Vec<(String, f64)>)> = map.into_iter().collect();
entries.sort_by(|a, b| b.0.len().cmp(&a.0.len()));
entries
}
#[cfg(test)]
mod tests {
use super::*;
use crate::phonetic::types::{ContextChar, PhoneChar, RewriteRuleChar};
fn make_rule(id: usize, pattern: &str, replacement: &str, weight: f64) -> RewriteRuleChar {
RewriteRuleChar {
rule_id: id,
rule_name: format!("{} -> {}", pattern, replacement),
pattern: pattern.chars().map(|c| PhoneChar::Consonant(c)).collect(),
replacement: replacement
.chars()
.map(|c| PhoneChar::Consonant(c))
.collect(),
context: ContextChar::Anywhere,
weight,
syllable_condition: None,
}
}
#[test]
fn test_expand_single_rule() {
let rules = vec![make_rule(1, "ph", "f", 0.1)];
let pattern = expand_phonetic_alternatives_char("fone", &rules);
assert!(pattern.contains("(ph|f)") || pattern.contains("(f|ph)"));
assert!(pattern.ends_with("one"));
}
#[test]
fn test_expand_no_rules() {
let rules: Vec<RewriteRuleChar> = vec![];
let pattern = expand_phonetic_alternatives_char("hello", &rules);
assert_eq!(pattern, "hello");
}
#[test]
fn test_expand_multiple_alternatives() {
let rules = vec![make_rule(1, "ph", "f", 0.1), make_rule(2, "gh", "f", 0.2)];
let pattern = expand_phonetic_alternatives_char("f", &rules);
assert!(pattern.contains("ph"));
assert!(pattern.contains("gh"));
assert!(pattern.contains('|'));
}
#[test]
fn test_expand_with_special_chars() {
let rules: Vec<RewriteRuleChar> = vec![];
let pattern = expand_phonetic_alternatives_char("a.b*c?", &rules);
assert_eq!(pattern, "a\\.b\\*c\\?");
}
#[test]
fn test_expand_longer_replacement_first() {
let rules = vec![
make_rule(1, "tion", "shun", 0.1),
make_rule(2, "ti", "sh", 0.1),
];
let pattern = expand_phonetic_alternatives_char("shun", &rules);
assert!(pattern.contains("(tion|shun)") || pattern.contains("(shun|tion)"));
}
#[test]
fn test_expand_with_costs() {
let rules = vec![
make_rule(1, "ph", "f", 0.1),
make_rule(2, "tion", "shun", 0.2),
];
let (pattern, cost) = expand_with_costs("fashun", &rules);
assert!(pattern.contains("(ph|f)") || pattern.contains("(f|ph)"));
assert!(pattern.contains("shun"));
assert!(cost > 0.0);
}
#[test]
fn test_expand_identity_rule_excluded() {
let rules = vec![make_rule(1, "f", "f", 0.1)];
let pattern = expand_phonetic_alternatives_char("fone", &rules);
assert_eq!(pattern, "fone");
}
#[test]
fn test_phones_to_string() {
let phones = vec![
PhoneChar::Consonant('p'),
PhoneChar::Consonant('h'),
PhoneChar::Vowel('o'),
PhoneChar::Consonant('n'),
PhoneChar::Vowel('e'),
];
assert_eq!(phones_to_string(&phones), "phone");
}
#[test]
fn test_phones_to_string_with_digraph() {
let phones = vec![
PhoneChar::Digraph('s', 'h'),
PhoneChar::Vowel('i'),
PhoneChar::Consonant('p'),
];
assert_eq!(phones_to_string(&phones), "ship");
}
#[test]
fn test_phones_to_string_with_silent() {
let phones = vec![
PhoneChar::Consonant('k'),
PhoneChar::Silent,
PhoneChar::Consonant('n'),
PhoneChar::Vowel('o'),
PhoneChar::Consonant('w'),
];
assert_eq!(phones_to_string(&phones), "know");
}
#[test]
fn test_regex_escape() {
assert_eq!(regex_escape("."), "\\.");
assert_eq!(regex_escape("*"), "\\*");
assert_eq!(regex_escape("+"), "\\+");
assert_eq!(regex_escape("?"), "\\?");
assert_eq!(regex_escape("("), "\\(");
assert_eq!(regex_escape(")"), "\\)");
assert_eq!(regex_escape("["), "\\[");
assert_eq!(regex_escape("]"), "\\]");
assert_eq!(regex_escape("{"), "\\{");
assert_eq!(regex_escape("}"), "\\}");
assert_eq!(regex_escape("|"), "\\|");
assert_eq!(regex_escape("^"), "\\^");
assert_eq!(regex_escape("$"), "\\$");
assert_eq!(regex_escape("\\"), "\\\\");
assert_eq!(regex_escape("abc"), "abc");
}
#[test]
fn test_char_byte_index() {
assert_eq!(char_byte_index("hello", 0), 0);
assert_eq!(char_byte_index("hello", 2), 2);
assert_eq!(char_byte_index("hello", 5), 5);
let s = "héllo";
assert_eq!(char_byte_index(s, 0), 0); assert_eq!(char_byte_index(s, 1), 1); assert_eq!(char_byte_index(s, 2), 3); }
}