use super::state_set::StateSet;
use super::types::StateId;
use super::{NFAChar, NFA};
use crate::transducer::articulatory_costs::ArticulatoryCosts;
use crate::transducer::Algorithm;
use rustc_hash::FxHashSet;
use std::collections::VecDeque;
#[derive(Debug, Clone)]
pub struct ProductAutomatonChar {
nfa: NFAChar,
max_cost: f64,
phonetic_weight: f64,
algorithm: Algorithm,
articulatory_costs: Option<ArticulatoryCosts>,
}
#[derive(Debug, Clone)]
pub struct ProductStateChar {
pub nfa_states: Vec<StateId>,
pub accumulated_cost: f64,
}
impl ProductStateChar {
pub fn new(nfa_states: FxHashSet<StateId>, accumulated_cost: f64) -> Self {
let mut states: Vec<StateId> = nfa_states.into_iter().collect();
states.sort(); Self {
nfa_states: states,
accumulated_cost,
}
}
pub fn with_edit_distance(nfa_states: FxHashSet<StateId>, edit_distance: u8) -> Self {
Self::new(nfa_states, edit_distance as f64)
}
pub fn edit_distance(&self) -> u8 {
self.accumulated_cost.ceil() as u8
}
}
impl PartialEq for ProductStateChar {
fn eq(&self, other: &Self) -> bool {
self.nfa_states == other.nfa_states
&& (self.accumulated_cost - other.accumulated_cost).abs() < 1e-9
}
}
impl Eq for ProductStateChar {}
impl std::hash::Hash for ProductStateChar {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.nfa_states.hash(state);
let cost_bits = (self.accumulated_cost * 1_000_000.0).round() as i64;
cost_bits.hash(state);
}
}
impl ProductAutomatonChar {
pub fn new(nfa: NFAChar, max_distance: u8) -> Self {
Self {
nfa,
max_cost: max_distance as f64,
phonetic_weight: 0.0,
algorithm: Algorithm::Standard,
articulatory_costs: None,
}
}
pub fn with_algorithm(nfa: NFAChar, max_distance: u8, algorithm: Algorithm) -> Self {
Self {
nfa,
max_cost: max_distance as f64,
phonetic_weight: 0.0,
algorithm,
articulatory_costs: None,
}
}
pub fn with_phonetic_weight(nfa: NFAChar, max_distance: u8, phonetic_weight: f64) -> Self {
Self {
nfa,
max_cost: max_distance as f64,
phonetic_weight,
algorithm: Algorithm::Standard,
articulatory_costs: None,
}
}
pub fn with_algorithm_and_weight(
nfa: NFAChar,
max_distance: u8,
algorithm: Algorithm,
phonetic_weight: f64,
) -> Self {
Self {
nfa,
max_cost: max_distance as f64,
phonetic_weight,
algorithm,
articulatory_costs: None,
}
}
pub fn with_articulatory_costs(
nfa: NFAChar,
max_cost: f64,
algorithm: Algorithm,
articulatory_costs: ArticulatoryCosts,
) -> Self {
Self {
nfa,
max_cost,
phonetic_weight: 0.0,
algorithm,
articulatory_costs: Some(articulatory_costs),
}
}
pub fn max_cost(&self) -> f64 {
self.max_cost
}
pub fn max_distance(&self) -> u8 {
self.max_cost.ceil() as u8
}
pub fn articulatory_costs(&self) -> Option<&ArticulatoryCosts> {
self.articulatory_costs.as_ref()
}
pub fn phonetic_weight(&self) -> f64 {
self.phonetic_weight
}
pub fn algorithm(&self) -> Algorithm {
self.algorithm
}
pub fn initial_state(&self) -> ProductStateChar {
let initial_closure: FxHashSet<StateId> =
self.nfa.epsilon_closure_single(self.nfa.start()).into();
ProductStateChar::new(initial_closure, 0.0)
}
pub fn is_accepting(&self, state: &ProductStateChar) -> bool {
if state.accumulated_cost > self.max_cost {
return false;
}
state.nfa_states.iter().any(|&s| self.nfa.is_final(s))
}
#[inline]
fn substitution_cost(&self, input_char: char, pattern_char: Option<char>) -> f64 {
match (&self.articulatory_costs, pattern_char) {
(Some(costs), Some(pc)) => costs.substitution_cost(input_char, pc),
_ => 1.0, }
}
#[inline]
fn insertion_cost(&self) -> f64 {
match &self.articulatory_costs {
Some(costs) => costs.insertion_cost(),
None => 1.0,
}
}
pub fn transition(&self, state: &ProductStateChar, c: char) -> Vec<ProductStateChar> {
let mut successors = Vec::new();
let current_states: FxHashSet<StateId> = state.nfa_states.iter().copied().collect();
let match_states = self.nfa_step(¤t_states, c);
if !match_states.is_empty() {
successors.push(ProductStateChar::new(match_states, state.accumulated_cost));
}
if state.accumulated_cost < self.max_cost {
let mut subst_entries: Vec<(FxHashSet<StateId>, f64)> = Vec::new();
for &nfa_state in &state.nfa_states {
for trans in self.nfa.transitions_from(nfa_state) {
if trans.label.consumes_input() {
let pattern_char = trans.label.expected_char();
let sub_cost = self.substitution_cost(c, pattern_char);
let new_cost = state.accumulated_cost + sub_cost;
if new_cost <= self.max_cost {
let closure = self.nfa.epsilon_closure_single(trans.to);
let closure_set: FxHashSet<StateId> = closure.into();
subst_entries.push((closure_set, new_cost));
}
}
}
}
for (subst_states, cost) in subst_entries {
if !subst_states.is_empty() {
let subst_state = ProductStateChar::new(subst_states, cost);
if !successors.contains(&subst_state) {
successors.push(subst_state);
}
}
}
let ins_cost = state.accumulated_cost + self.insertion_cost();
if ins_cost <= self.max_cost {
successors.push(ProductStateChar::new(current_states.clone(), ins_cost));
}
}
successors
}
fn nfa_step(&self, states: &FxHashSet<StateId>, c: char) -> FxHashSet<StateId> {
let mut next_states = StateSet::new();
for &state in states {
for trans in self.nfa.transitions_from(state) {
if trans.label.matches(c) && trans.label.consumes_input() {
next_states.insert(trans.to);
}
}
}
self.nfa.epsilon_closure(&next_states).into()
}
pub fn accepts(&self, input: &str) -> bool {
if self.nfa.is_empty() {
return input.is_empty() || input.len() <= self.max_distance() as usize;
}
let input_chars: Vec<char> = input.chars().collect();
let n = input_chars.len();
let max_errors = self.max_distance();
let initial_closure: FxHashSet<StateId> =
self.nfa.epsilon_closure_single(self.nfa.start()).into();
let mut visited: FxHashSet<(usize, Vec<StateId>, u8)> = FxHashSet::default();
let mut queue: VecDeque<(usize, FxHashSet<StateId>, u8)> = VecDeque::new();
queue.push_back((0, initial_closure, 0));
while let Some((pos, nfa_states, errors)) = queue.pop_front() {
let mut states_vec: Vec<StateId> = nfa_states.iter().copied().collect();
states_vec.sort();
if !visited.insert((pos, states_vec.clone(), errors)) {
continue;
}
if errors > max_errors {
continue;
}
if pos == n {
if self.can_reach_final(&nfa_states, errors, max_errors) {
return true;
}
continue;
}
let c = input_chars[pos];
let match_states = self.nfa_step(&nfa_states, c);
if !match_states.is_empty() {
queue.push_back((pos + 1, match_states, errors));
}
if errors < max_errors {
let subst_states = self.nfa_advance(&nfa_states);
if !subst_states.is_empty() {
queue.push_back((pos + 1, subst_states, errors + 1));
}
queue.push_back((pos + 1, nfa_states.clone(), errors + 1));
let del_states = self.nfa_advance(&nfa_states);
if !del_states.is_empty() {
queue.push_back((pos, del_states, errors + 1));
}
if self.algorithm.supports_transposition() && pos + 1 < n {
let next_c = input_chars[pos + 1];
let trans_states = self.nfa_step_transposed(&nfa_states, c, next_c);
if !trans_states.is_empty() {
queue.push_back((pos + 2, trans_states, errors + 1));
}
}
if self.algorithm.supports_merge_split() && pos + 1 < n {
let next_c = input_chars[pos + 1];
let merge_states = self.nfa_step_merged(&nfa_states, c, next_c);
if !merge_states.is_empty() {
queue.push_back((pos + 2, merge_states, errors + 1));
}
}
if self.algorithm.supports_merge_split() {
let split_states = self.nfa_step_split(&nfa_states, c);
if !split_states.is_empty() {
queue.push_back((pos + 1, split_states, errors + 1));
}
}
}
}
false
}
fn nfa_advance(&self, states: &FxHashSet<StateId>) -> FxHashSet<StateId> {
let mut next_states = StateSet::new();
for &state in states {
for trans in self.nfa.transitions_from(state) {
if trans.label.consumes_input() {
next_states.insert(trans.to);
}
}
}
self.nfa.epsilon_closure(&next_states).into()
}
fn nfa_step_transposed(
&self,
states: &FxHashSet<StateId>,
c1: char,
c2: char,
) -> FxHashSet<StateId> {
let after_c2 = self.nfa_step(states, c2);
self.nfa_step(&after_c2, c1)
}
fn nfa_step_merged(
&self,
_states: &FxHashSet<StateId>,
_c1: char,
_c2: char,
) -> FxHashSet<StateId> {
self.nfa_advance(_states)
}
fn nfa_step_split(&self, states: &FxHashSet<StateId>, _c: char) -> FxHashSet<StateId> {
let after_first = self.nfa_advance(states);
self.nfa_advance(&after_first)
}
fn can_reach_final(
&self,
states: &FxHashSet<StateId>,
current_errors: u8,
max_errors: u8,
) -> bool {
if states.iter().any(|&s| self.nfa.is_final(s)) {
return true;
}
let remaining = max_errors.saturating_sub(current_errors);
if remaining == 0 {
return false;
}
let mut visited: FxHashSet<Vec<StateId>> = FxHashSet::default();
let mut queue: VecDeque<(FxHashSet<StateId>, u8)> = VecDeque::new();
queue.push_back((states.clone(), 0));
while let Some((current, dist)) = queue.pop_front() {
let mut states_vec: Vec<StateId> = current.iter().copied().collect();
states_vec.sort();
if !visited.insert(states_vec) {
continue;
}
if dist > remaining {
continue;
}
if current.iter().any(|&s| self.nfa.is_final(s)) {
return true;
}
let next = self.nfa_advance(¤t);
if !next.is_empty() {
queue.push_back((next, dist + 1));
}
}
false
}
pub fn min_distance(&self, input: &str) -> Option<u8> {
if self.nfa.is_empty() {
return if input.is_empty() {
Some(0)
} else if input.len() <= self.max_distance() as usize {
Some(input.len() as u8)
} else {
None
};
}
let input_chars: Vec<char> = input.chars().collect();
let n = input_chars.len();
let initial_closure: FxHashSet<StateId> =
self.nfa.epsilon_closure_single(self.nfa.start()).into();
let mut min_dist: Option<u8> = None;
let mut visited: FxHashSet<(usize, Vec<StateId>, u8)> = FxHashSet::default();
let mut queue: VecDeque<(usize, FxHashSet<StateId>, u8)> = VecDeque::new();
queue.push_back((0, initial_closure, 0));
while let Some((pos, nfa_states, errors)) = queue.pop_front() {
let mut states_vec: Vec<StateId> = nfa_states.iter().copied().collect();
states_vec.sort();
if !visited.insert((pos, states_vec.clone(), errors)) {
continue;
}
if errors > self.max_distance() {
continue;
}
if let Some(min) = min_dist {
if errors >= min {
continue;
}
}
if pos == n {
if let Some(final_dist) = self.distance_to_final(&nfa_states, errors) {
match min_dist {
None => min_dist = Some(final_dist),
Some(current) if final_dist < current => min_dist = Some(final_dist),
_ => {}
}
}
continue;
}
let c = input_chars[pos];
let match_states = self.nfa_step(&nfa_states, c);
if !match_states.is_empty() {
queue.push_back((pos + 1, match_states, errors));
}
if errors < self.max_distance() {
let subst_states = self.nfa_advance(&nfa_states);
if !subst_states.is_empty() {
queue.push_back((pos + 1, subst_states, errors + 1));
}
queue.push_back((pos + 1, nfa_states.clone(), errors + 1));
let del_states = self.nfa_advance(&nfa_states);
if !del_states.is_empty() {
queue.push_back((pos, del_states, errors + 1));
}
if self.algorithm.supports_transposition() && pos + 1 < n {
let next_c = input_chars[pos + 1];
let trans_states = self.nfa_step_transposed(&nfa_states, c, next_c);
if !trans_states.is_empty() {
queue.push_back((pos + 2, trans_states, errors + 1));
}
}
if self.algorithm.supports_merge_split() && pos + 1 < n {
let next_c = input_chars[pos + 1];
let merge_states = self.nfa_step_merged(&nfa_states, c, next_c);
if !merge_states.is_empty() {
queue.push_back((pos + 2, merge_states, errors + 1));
}
}
if self.algorithm.supports_merge_split() {
let split_states = self.nfa_step_split(&nfa_states, c);
if !split_states.is_empty() {
queue.push_back((pos + 1, split_states, errors + 1));
}
}
}
}
min_dist
}
fn distance_to_final(&self, states: &FxHashSet<StateId>, base_dist: u8) -> Option<u8> {
if states.iter().any(|&s| self.nfa.is_final(s)) {
return Some(base_dist);
}
let remaining = self.max_distance().saturating_sub(base_dist);
if remaining == 0 {
return None;
}
let mut visited: FxHashSet<Vec<StateId>> = FxHashSet::default();
let mut queue: VecDeque<(FxHashSet<StateId>, u8)> = VecDeque::new();
queue.push_back((states.clone(), 0));
while let Some((current, dist)) = queue.pop_front() {
let mut states_vec: Vec<StateId> = current.iter().copied().collect();
states_vec.sort();
if !visited.insert(states_vec) {
continue;
}
if dist > remaining {
continue;
}
if current.iter().any(|&s| self.nfa.is_final(s)) {
return Some(base_dist + dist);
}
let next = self.nfa_advance(¤t);
if !next.is_empty() {
queue.push_back((next, dist + 1));
}
}
None
}
}
#[derive(Debug, Clone)]
pub struct ProductAutomaton {
nfa: NFA,
max_distance: u8,
phonetic_weight: f64,
algorithm: Algorithm,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ProductState {
pub nfa_states: Vec<StateId>,
pub edit_distance: u8,
}
impl ProductState {
pub fn new(nfa_states: FxHashSet<StateId>, edit_distance: u8) -> Self {
let mut states: Vec<StateId> = nfa_states.into_iter().collect();
states.sort();
Self {
nfa_states: states,
edit_distance,
}
}
#[inline]
pub fn edit_distance(&self) -> u8 {
self.edit_distance
}
}
impl ProductAutomaton {
pub fn new(nfa: NFA, max_distance: u8) -> Self {
Self {
nfa,
max_distance,
phonetic_weight: 0.0,
algorithm: Algorithm::Standard,
}
}
pub fn with_algorithm(nfa: NFA, max_distance: u8, algorithm: Algorithm) -> Self {
Self {
nfa,
max_distance,
phonetic_weight: 0.0,
algorithm,
}
}
pub fn with_phonetic_weight(nfa: NFA, max_distance: u8, phonetic_weight: f64) -> Self {
Self {
nfa,
max_distance,
phonetic_weight,
algorithm: Algorithm::Standard,
}
}
pub fn with_algorithm_and_weight(
nfa: NFA,
max_distance: u8,
algorithm: Algorithm,
phonetic_weight: f64,
) -> Self {
Self {
nfa,
max_distance,
phonetic_weight,
algorithm,
}
}
pub fn max_distance(&self) -> u8 {
self.max_distance
}
pub fn phonetic_weight(&self) -> f64 {
self.phonetic_weight
}
pub fn algorithm(&self) -> Algorithm {
self.algorithm
}
pub fn initial_state(&self) -> ProductState {
let initial_closure: FxHashSet<StateId> =
self.nfa.epsilon_closure_single(self.nfa.start()).into();
ProductState::new(initial_closure, 0)
}
pub fn is_accepting(&self, state: &ProductState) -> bool {
if state.edit_distance() > self.max_distance() {
return false;
}
state.nfa_states.iter().any(|&s| self.nfa.is_final(s))
}
fn nfa_step(&self, states: &FxHashSet<StateId>, b: u8) -> FxHashSet<StateId> {
let mut next_states = StateSet::new();
for &state in states {
for trans in self.nfa.transitions_from(state) {
if trans.label.matches(b) && trans.label.consumes_input() {
next_states.insert(trans.to);
}
}
}
self.nfa.epsilon_closure(&next_states).into()
}
fn nfa_advance(&self, states: &FxHashSet<StateId>) -> FxHashSet<StateId> {
let mut next_states = StateSet::new();
for &state in states {
for trans in self.nfa.transitions_from(state) {
if trans.label.consumes_input() {
next_states.insert(trans.to);
}
}
}
self.nfa.epsilon_closure(&next_states).into()
}
fn nfa_step_transposed(
&self,
states: &FxHashSet<StateId>,
b1: u8,
b2: u8,
) -> FxHashSet<StateId> {
let after_b2 = self.nfa_step(states, b2);
self.nfa_step(&after_b2, b1)
}
fn nfa_step_merged(&self, states: &FxHashSet<StateId>, _b1: u8, _b2: u8) -> FxHashSet<StateId> {
self.nfa_advance(states)
}
fn nfa_step_split(&self, states: &FxHashSet<StateId>, _b: u8) -> FxHashSet<StateId> {
let after_first = self.nfa_advance(states);
self.nfa_advance(&after_first)
}
pub fn accepts(&self, input: &[u8]) -> bool {
if self.nfa.is_empty() {
return input.is_empty() || input.len() <= self.max_distance() as usize;
}
let n = input.len();
let initial_closure: FxHashSet<StateId> =
self.nfa.epsilon_closure_single(self.nfa.start()).into();
let mut visited: FxHashSet<(usize, Vec<StateId>, u8)> = FxHashSet::default();
let mut queue: VecDeque<(usize, FxHashSet<StateId>, u8)> = VecDeque::new();
queue.push_back((0, initial_closure, 0));
while let Some((pos, nfa_states, errors)) = queue.pop_front() {
let mut states_vec: Vec<StateId> = nfa_states.iter().copied().collect();
states_vec.sort();
if !visited.insert((pos, states_vec.clone(), errors)) {
continue;
}
if errors > self.max_distance() {
continue;
}
if pos == n {
if self.can_reach_final(&nfa_states, errors) {
return true;
}
continue;
}
let b = input[pos];
let match_states = self.nfa_step(&nfa_states, b);
if !match_states.is_empty() {
queue.push_back((pos + 1, match_states, errors));
}
if errors < self.max_distance() {
let subst_states = self.nfa_advance(&nfa_states);
if !subst_states.is_empty() {
queue.push_back((pos + 1, subst_states, errors + 1));
}
queue.push_back((pos + 1, nfa_states.clone(), errors + 1));
let del_states = self.nfa_advance(&nfa_states);
if !del_states.is_empty() {
queue.push_back((pos, del_states, errors + 1));
}
if self.algorithm.supports_transposition() && pos + 1 < n {
let next_b = input[pos + 1];
let trans_states = self.nfa_step_transposed(&nfa_states, b, next_b);
if !trans_states.is_empty() {
queue.push_back((pos + 2, trans_states, errors + 1));
}
}
if self.algorithm.supports_merge_split() && pos + 1 < n {
let next_b = input[pos + 1];
let merge_states = self.nfa_step_merged(&nfa_states, b, next_b);
if !merge_states.is_empty() {
queue.push_back((pos + 2, merge_states, errors + 1));
}
}
if self.algorithm.supports_merge_split() {
let split_states = self.nfa_step_split(&nfa_states, b);
if !split_states.is_empty() {
queue.push_back((pos + 1, split_states, errors + 1));
}
}
}
}
false
}
fn can_reach_final(&self, states: &FxHashSet<StateId>, current_errors: u8) -> bool {
if states.iter().any(|&s| self.nfa.is_final(s)) {
return true;
}
let remaining = self.max_distance() - current_errors;
if remaining == 0 {
return false;
}
let mut visited: FxHashSet<Vec<StateId>> = FxHashSet::default();
let mut queue: VecDeque<(FxHashSet<StateId>, u8)> = VecDeque::new();
queue.push_back((states.clone(), 0));
while let Some((current, dist)) = queue.pop_front() {
let mut states_vec: Vec<StateId> = current.iter().copied().collect();
states_vec.sort();
if !visited.insert(states_vec) {
continue;
}
if dist > remaining {
continue;
}
if current.iter().any(|&s| self.nfa.is_final(s)) {
return true;
}
let next = self.nfa_advance(¤t);
if !next.is_empty() {
queue.push_back((next, dist + 1));
}
}
false
}
pub fn min_distance(&self, input: &[u8]) -> Option<u8> {
if self.nfa.is_empty() {
return if input.is_empty() {
Some(0)
} else if input.len() <= self.max_distance() as usize {
Some(input.len() as u8)
} else {
None
};
}
let n = input.len();
let initial_closure: FxHashSet<StateId> =
self.nfa.epsilon_closure_single(self.nfa.start()).into();
let mut min_dist: Option<u8> = None;
let mut visited: FxHashSet<(usize, Vec<StateId>, u8)> = FxHashSet::default();
let mut queue: VecDeque<(usize, FxHashSet<StateId>, u8)> = VecDeque::new();
queue.push_back((0, initial_closure, 0));
while let Some((pos, nfa_states, errors)) = queue.pop_front() {
let mut states_vec: Vec<StateId> = nfa_states.iter().copied().collect();
states_vec.sort();
if !visited.insert((pos, states_vec.clone(), errors)) {
continue;
}
if errors > self.max_distance() {
continue;
}
if let Some(min) = min_dist {
if errors >= min {
continue;
}
}
if pos == n {
if let Some(final_dist) = self.distance_to_final(&nfa_states, errors) {
match min_dist {
None => min_dist = Some(final_dist),
Some(current) if final_dist < current => min_dist = Some(final_dist),
_ => {}
}
}
continue;
}
let b = input[pos];
let match_states = self.nfa_step(&nfa_states, b);
if !match_states.is_empty() {
queue.push_back((pos + 1, match_states, errors));
}
if errors < self.max_distance() {
let subst_states = self.nfa_advance(&nfa_states);
if !subst_states.is_empty() {
queue.push_back((pos + 1, subst_states, errors + 1));
}
queue.push_back((pos + 1, nfa_states.clone(), errors + 1));
let del_states = self.nfa_advance(&nfa_states);
if !del_states.is_empty() {
queue.push_back((pos, del_states, errors + 1));
}
if self.algorithm.supports_transposition() && pos + 1 < n {
let next_b = input[pos + 1];
let trans_states = self.nfa_step_transposed(&nfa_states, b, next_b);
if !trans_states.is_empty() {
queue.push_back((pos + 2, trans_states, errors + 1));
}
}
if self.algorithm.supports_merge_split() && pos + 1 < n {
let next_b = input[pos + 1];
let merge_states = self.nfa_step_merged(&nfa_states, b, next_b);
if !merge_states.is_empty() {
queue.push_back((pos + 2, merge_states, errors + 1));
}
}
if self.algorithm.supports_merge_split() {
let split_states = self.nfa_step_split(&nfa_states, b);
if !split_states.is_empty() {
queue.push_back((pos + 1, split_states, errors + 1));
}
}
}
}
min_dist
}
fn distance_to_final(&self, states: &FxHashSet<StateId>, base_dist: u8) -> Option<u8> {
if states.iter().any(|&s| self.nfa.is_final(s)) {
return Some(base_dist);
}
let remaining = self.max_distance().saturating_sub(base_dist);
if remaining == 0 {
return None;
}
let mut visited: FxHashSet<Vec<StateId>> = FxHashSet::default();
let mut queue: VecDeque<(FxHashSet<StateId>, u8)> = VecDeque::new();
queue.push_back((states.clone(), 0));
while let Some((current, dist)) = queue.pop_front() {
let mut states_vec: Vec<StateId> = current.iter().copied().collect();
states_vec.sort();
if !visited.insert(states_vec) {
continue;
}
if dist > remaining {
continue;
}
if current.iter().any(|&s| self.nfa.is_final(s)) {
return Some(base_dist + dist);
}
let next = self.nfa_advance(¤t);
if !next.is_empty() {
queue.push_back((next, dist + 1));
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::phonetic::nfa::compiler::{compile, compile_bytes};
use crate::phonetic::regex::{parse, parse_bytes};
#[test]
fn test_product_exact_match() {
let nfa = compile(&parse("phone").expect("test: parse phone")).expect("test: compile nfa");
let product = ProductAutomatonChar::new(nfa, 2);
assert!(product.accepts("phone"));
assert!(!product.accepts("xyz"));
}
#[test]
fn test_product_alternation() {
let nfa = compile(&parse("ph|f").expect("test: parse ph|f")).expect("test: compile nfa");
let product = ProductAutomatonChar::new(nfa, 0);
assert!(product.accepts("ph"));
assert!(product.accepts("f"));
assert!(!product.accepts("g"));
}
#[test]
fn test_product_with_edit_distance() {
let nfa = compile(&parse("phone").expect("test: parse phone")).expect("test: compile nfa");
let product = ProductAutomatonChar::new(nfa, 2);
assert!(product.accepts("phone"));
assert!(product.accepts("phones"));
assert!(product.accepts("phon"));
assert!(product.accepts("phome"));
assert!(product.accepts("phon")); assert!(product.accepts("fone")); }
#[test]
fn test_product_phonetic_pattern() {
let nfa = compile(&parse("(ph|f)one").expect("test: parse (ph|f)one"))
.expect("test: compile nfa");
let product = ProductAutomatonChar::new(nfa, 1);
assert!(product.accepts("phone"));
assert!(product.accepts("fone"));
assert!(product.accepts("phones")); assert!(product.accepts("fones")); assert!(product.accepts("bone"));
let product_exact = ProductAutomatonChar::new(
compile(&parse("(ph|f)one").expect("test: parse (ph|f)one"))
.expect("test: compile nfa"),
0,
);
assert!(product_exact.accepts("phone"));
assert!(product_exact.accepts("fone"));
assert!(!product_exact.accepts("bone")); }
#[test]
fn test_product_star() {
let nfa = compile(&parse("a*").expect("test: parse a*")).expect("test: compile nfa");
let product = ProductAutomatonChar::new(nfa, 1);
assert!(product.accepts("")); assert!(product.accepts("a")); assert!(product.accepts("aa")); assert!(product.accepts("b")); assert!(product.accepts("ab")); }
#[test]
fn test_product_min_distance() {
let nfa = compile(&parse("phone").expect("test: parse phone")).expect("test: compile nfa");
let product = ProductAutomatonChar::new(nfa, 3);
assert_eq!(product.min_distance("phone"), Some(0));
assert_eq!(product.min_distance("phon"), Some(1));
assert_eq!(product.min_distance("phones"), Some(1));
assert_eq!(product.min_distance("phome"), Some(1));
}
#[test]
fn test_product_char_class() {
let nfa =
compile(&parse("[aeiou]+").expect("test: parse [aeiou]+")).expect("test: compile nfa");
let product = ProductAutomatonChar::new(nfa, 1);
assert!(product.accepts("a"));
assert!(product.accepts("aeiou"));
assert!(product.accepts("b")); assert!(product.accepts("ab")); assert!(product.accepts(""));
let product_exact = ProductAutomatonChar::new(
compile(&parse("[aeiou]+").expect("test: parse [aeiou]+")).expect("test: compile nfa"),
0,
);
assert!(product_exact.accepts("a"));
assert!(product_exact.accepts("aeiou"));
assert!(!product_exact.accepts("")); assert!(!product_exact.accepts("b")); }
#[test]
fn test_product_over_budget() {
let nfa = compile(&parse("abc").expect("test: parse abc")).expect("test: compile nfa");
let product = ProductAutomatonChar::new(nfa, 1);
assert!(product.accepts("abc")); assert!(product.accepts("ab")); assert!(product.accepts("abcd")); assert!(!product.accepts("xyz")); }
#[test]
fn test_product_bytes_exact() {
let nfa = compile_bytes(&parse_bytes(b"phone").expect("test: parse phone bytes"))
.expect("test: compile nfa bytes");
let product = ProductAutomaton::new(nfa, 2);
assert!(product.accepts(b"phone"));
assert!(!product.accepts(b"xyz"));
}
#[test]
fn test_product_bytes_with_edits() {
let nfa = compile_bytes(&parse_bytes(b"abc").expect("test: parse abc bytes"))
.expect("test: compile nfa bytes");
let product = ProductAutomaton::new(nfa, 1);
assert!(product.accepts(b"abc"));
assert!(product.accepts(b"ab"));
assert!(product.accepts(b"abcd"));
assert!(!product.accepts(b"xyz"));
}
#[test]
fn test_product_bytes_min_distance() {
let nfa = compile_bytes(&parse_bytes(b"phone").expect("test: parse phone bytes"))
.expect("test: compile nfa bytes");
let product = ProductAutomaton::new(nfa, 3);
assert_eq!(product.min_distance(b"phone"), Some(0));
assert_eq!(product.min_distance(b"phon"), Some(1));
}
#[test]
fn test_transposition_accepts() {
let nfa = compile(&parse("ab").expect("test: parse ab")).expect("test: compile nfa");
let standard = ProductAutomatonChar::new(nfa.clone(), 1);
assert!(!standard.accepts("ba"));
let transposition = ProductAutomatonChar::with_algorithm(nfa, 1, Algorithm::Transposition);
assert!(transposition.accepts("ba")); }
#[test]
fn test_transposition_min_distance() {
let nfa = compile(&parse("ab").expect("test: parse ab")).expect("test: compile nfa");
let standard = ProductAutomatonChar::new(nfa.clone(), 2);
assert_eq!(standard.min_distance("ba"), Some(2));
let transposition = ProductAutomatonChar::with_algorithm(nfa, 2, Algorithm::Transposition);
assert_eq!(transposition.min_distance("ba"), Some(1));
}
#[test]
fn test_transposition_longer_string() {
let nfa = compile(&parse("the").expect("test: parse the")).expect("test: compile nfa");
let standard = ProductAutomatonChar::new(nfa.clone(), 1);
assert!(!standard.accepts("hte"));
let transposition = ProductAutomatonChar::with_algorithm(nfa, 1, Algorithm::Transposition);
assert!(transposition.accepts("hte"));
}
#[test]
fn test_merge_split_accepts() {
let nfa = compile(&parse("abc").expect("test: parse abc")).expect("test: compile nfa");
let standard = ProductAutomatonChar::new(nfa.clone(), 1);
assert!(standard.accepts("abcd"));
assert!(standard.accepts("ab"));
let merge_split =
ProductAutomatonChar::with_algorithm(nfa.clone(), 1, Algorithm::MergeAndSplit);
assert!(merge_split.accepts("abcd")); assert!(merge_split.accepts("ab")); }
#[test]
fn test_merge_split_min_distance() {
let nfa = compile(&parse("abc").expect("test: parse abc")).expect("test: compile nfa");
let standard = ProductAutomatonChar::new(nfa.clone(), 3);
let merge_split = ProductAutomatonChar::with_algorithm(nfa, 3, Algorithm::MergeAndSplit);
assert_eq!(standard.min_distance("abc"), Some(0));
assert_eq!(merge_split.min_distance("abc"), Some(0));
assert_eq!(standard.min_distance("ab"), Some(1));
assert_eq!(merge_split.min_distance("ab"), Some(1));
}
#[test]
fn test_algorithm_getter() {
let nfa = compile(&parse("test").expect("test: parse test")).expect("test: compile nfa");
let standard = ProductAutomatonChar::new(nfa.clone(), 1);
assert_eq!(standard.algorithm(), Algorithm::Standard);
let transposition =
ProductAutomatonChar::with_algorithm(nfa.clone(), 1, Algorithm::Transposition);
assert_eq!(transposition.algorithm(), Algorithm::Transposition);
let merge_split = ProductAutomatonChar::with_algorithm(nfa, 1, Algorithm::MergeAndSplit);
assert_eq!(merge_split.algorithm(), Algorithm::MergeAndSplit);
}
#[test]
fn test_byte_level_transposition() {
let nfa = compile_bytes(&parse_bytes(b"ab").expect("test: parse ab bytes"))
.expect("test: compile nfa bytes");
let standard = ProductAutomaton::new(nfa.clone(), 1);
assert!(!standard.accepts(b"ba"));
let transposition = ProductAutomaton::with_algorithm(nfa, 1, Algorithm::Transposition);
assert!(transposition.accepts(b"ba")); }
#[cfg(feature = "phonetic-rules")]
mod articulatory_tests {
use super::*;
use crate::transducer::ArticulatoryCosts;
#[test]
fn test_articulatory_costs_constructor() {
let nfa =
compile(&parse("test").expect("test: parse test")).expect("test: compile nfa");
let costs = ArticulatoryCosts::default();
let product = ProductAutomatonChar::with_articulatory_costs(
nfa,
2.0,
Algorithm::Standard,
costs.clone(),
);
assert!(product.articulatory_costs().is_some());
assert!((product.max_cost() - 2.0).abs() < 1e-9);
assert_eq!(product.algorithm(), Algorithm::Standard);
}
#[test]
fn test_substitution_cost_varies_by_phonetic_similarity() {
let nfa = compile(&parse("p").expect("test: parse p")).expect("test: compile nfa");
let costs = ArticulatoryCosts::default();
let product =
ProductAutomatonChar::with_articulatory_costs(nfa, 2.0, Algorithm::Standard, costs);
let pb_cost = product.substitution_cost('b', Some('p'));
let pk_cost = product.substitution_cost('k', Some('p'));
assert!(
pb_cost < pk_cost,
"p→b ({}) should be cheaper than p→k ({})",
pb_cost,
pk_cost
);
let pp_cost = product.substitution_cost('p', Some('p'));
assert!(pp_cost < 0.01, "p→p should be nearly free, got {}", pp_cost);
}
#[test]
fn test_transition_uses_articulatory_costs() {
let nfa = compile(&parse("p").expect("test: parse p")).expect("test: compile nfa");
let costs = ArticulatoryCosts::default();
let product =
ProductAutomatonChar::with_articulatory_costs(nfa, 2.0, Algorithm::Standard, costs);
let initial = product.initial_state();
let match_successors = product.transition(&initial, 'p');
let match_state = match_successors
.iter()
.find(|s| s.accumulated_cost < 0.01)
.expect("should find match state with zero cost");
assert!(
match_state.accumulated_cost < 0.01,
"Exact match should have near-zero cost, got {}",
match_state.accumulated_cost
);
let b_successors = product.transition(&initial, 'b');
let b_subst_state = b_successors
.iter()
.find(|s| s.accumulated_cost > 0.01 && s.accumulated_cost < 0.5)
.expect("should find substitution state with low cost for 'b'");
let k_successors = product.transition(&initial, 'k');
let k_subst_state = k_successors
.iter()
.find(|s| s.accumulated_cost > 0.3)
.expect("should find substitution state with higher cost for 'k'");
assert!(
b_subst_state.accumulated_cost < k_subst_state.accumulated_cost,
"p→b ({}) should be cheaper than p→k ({})",
b_subst_state.accumulated_cost,
k_subst_state.accumulated_cost
);
}
#[test]
fn test_accumulated_cost_tracking() {
let nfa = compile(&parse("ab").expect("test: parse ab")).expect("test: compile nfa");
let costs = ArticulatoryCosts::default();
let product = ProductAutomatonChar::with_articulatory_costs(
nfa,
3.0, Algorithm::Standard,
costs,
);
let initial = product.initial_state();
assert!(
initial.accumulated_cost.abs() < 1e-9,
"Initial cost should be 0"
);
let after_a = product.transition(&initial, 'a');
let match_a = after_a
.iter()
.find(|s| s.accumulated_cost < 0.01)
.expect("should find exact match for 'a'");
let after_d = product.transition(match_a, 'd');
let subst_state = after_d
.iter()
.find(|s| s.accumulated_cost > 0.1)
.expect("should find state with accumulated substitution cost");
assert!(
subst_state.accumulated_cost > 0.1,
"Accumulated cost should reflect substitution, got {}",
subst_state.accumulated_cost
);
}
#[test]
fn test_fixed_cost_without_articulatory() {
let nfa = compile(&parse("p").expect("test: parse p")).expect("test: compile nfa");
let product = ProductAutomatonChar::new(nfa, 2);
let initial = product.initial_state();
let b_successors = product.transition(&initial, 'b');
let k_successors = product.transition(&initial, 'k');
let b_subst = b_successors
.iter()
.find(|s| (s.accumulated_cost - 1.0).abs() < 0.01);
let k_subst = k_successors
.iter()
.find(|s| (s.accumulated_cost - 1.0).abs() < 0.01);
assert!(
b_subst.is_some(),
"Should find substitution with cost 1.0 for 'b'"
);
assert!(
k_subst.is_some(),
"Should find substitution with cost 1.0 for 'k'"
);
}
#[test]
fn test_edit_distance_rounds_up() {
use rustc_hash::FxHashSet;
let states: FxHashSet<StateId> = vec![0].into_iter().collect();
let state1 = ProductStateChar::new(states.clone(), 0.3);
assert_eq!(state1.edit_distance(), 1);
let state2 = ProductStateChar::new(states.clone(), 1.7);
assert_eq!(state2.edit_distance(), 2);
let state3 = ProductStateChar::new(states, 0.0);
assert_eq!(state3.edit_distance(), 0);
}
#[test]
fn test_ipa_articulatory_costs() {
let nfa = compile(&parse("p").expect("test: parse p")).expect("test: compile nfa");
let costs = ArticulatoryCosts::default();
let product =
ProductAutomatonChar::with_articulatory_costs(nfa, 2.0, Algorithm::Standard, costs);
let sh_cost = product.substitution_cost('ʃ', Some('s'));
let sh_p_cost = product.substitution_cost('ʃ', Some('p'));
assert!(
sh_cost < sh_p_cost,
"ʃ→s ({}) should be cheaper than ʃ→p ({})",
sh_cost,
sh_p_cost
);
}
#[test]
fn test_max_cost_threshold() {
let nfa = compile(&parse("abc").expect("test: parse abc")).expect("test: compile nfa");
let costs = ArticulatoryCosts::default();
let product = ProductAutomatonChar::with_articulatory_costs(
nfa,
0.5, Algorithm::Standard,
costs,
);
let initial = product.initial_state();
let z_successors = product.transition(&initial, 'z');
for state in &z_successors {
assert!(
state.accumulated_cost <= 0.5 + 1e-9,
"State cost {} exceeds max_cost 0.5",
state.accumulated_cost
);
}
}
#[test]
fn test_is_accepting_with_articulatory_costs() {
let nfa = compile(&parse("p").expect("test: parse p")).expect("test: compile nfa");
let costs = ArticulatoryCosts::default();
let product = ProductAutomatonChar::with_articulatory_costs(
nfa,
1.0, Algorithm::Standard,
costs,
);
let initial = product.initial_state();
let after_p = product.transition(&initial, 'p');
let match_state = after_p
.iter()
.find(|s| s.accumulated_cost < 0.01)
.expect("should find match state");
assert!(
product.is_accepting(match_state),
"Should accept after matching 'p'"
);
let after_b = product.transition(&initial, 'b');
let subst_state = after_b
.iter()
.find(|s| s.accumulated_cost > 0.01 && s.accumulated_cost <= 1.0);
if let Some(state) = subst_state {
assert!(
product.is_accepting(state),
"Should accept similar substitution within cost budget"
);
}
}
}
}