use super::state::GeneralizedState;
use crate::transducer::universal::bit_vector::CharacteristicVector;
use crate::transducer::OperationSet;
#[derive(Debug, Clone)]
pub struct GeneralizedAutomaton {
max_distance: u8,
operations: OperationSet,
}
impl GeneralizedAutomaton {
#[must_use]
pub fn new(max_distance: u8) -> Self {
Self {
max_distance,
operations: OperationSet::standard(),
}
}
#[must_use]
pub fn with_operations(max_distance: u8, operations: OperationSet) -> Self {
Self {
max_distance,
operations,
}
}
#[must_use]
pub fn max_distance(&self) -> u8 {
self.max_distance
}
fn initial_state(&self) -> GeneralizedState {
GeneralizedState::initial(self.max_distance)
}
#[must_use]
fn is_accepting(&self, state: &GeneralizedState, word_len: usize, input_len: usize) -> bool {
use crate::transducer::generalized::GeneralizedPosition;
let n = self.max_distance as i32;
state.positions().any(|pos| {
match pos {
GeneralizedPosition::MFinal { offset, errors } => {
*offset <= 0 && *errors <= self.max_distance
}
GeneralizedPosition::INonFinal { .. } => {
let current_word_pos = input_len as i32 + pos.offset();
if current_word_pos < 0 {
return false; }
let remaining_chars = word_len as i32 - current_word_pos;
let remaining_errors = n - (pos.errors() as i32);
remaining_chars <= remaining_errors
}
GeneralizedPosition::ITransposing { .. }
| GeneralizedPosition::MTransposing { .. }
| GeneralizedPosition::ISplitting { .. }
| GeneralizedPosition::MSplitting { .. } => false,
}
})
}
pub fn accepts(&self, word: &str, input: &str) -> bool {
if input.is_empty() {
return word.len() <= self.max_distance as usize;
}
let max_expansion = self
.operations
.operations()
.iter()
.map(|op| op.consume_y().saturating_sub(op.consume_x()))
.max()
.unwrap_or(0);
if max_expansion > 0 {
let max_len = word.len() * (1 + max_expansion as usize) + self.max_distance as usize;
if input.len() > max_len {
return false;
}
} else {
if input.len() > word.len() + self.max_distance as usize {
return false;
}
}
let mut state = self.initial_state();
let word_chars: Option<Vec<char>> = if self.max_distance > 1 {
Some(word.chars().collect())
} else {
None
};
for (i, input_char) in input.chars().enumerate() {
let subword = self.relevant_subword(word, i + 1);
#[cfg(debug_assertions)]
eprintln!(
"\n[DEBUG] === Input position i={}, char='{}' ===",
i, input_char
);
#[cfg(debug_assertions)]
eprintln!(" Subword: {:?}", subword);
#[cfg(debug_assertions)]
eprintln!(
" State before transition: {} positions",
state.positions().count()
);
let bit_vector = CharacteristicVector::new(input_char, &subword);
if let Some(next_state) = state.transition(
&self.operations,
&bit_vector,
word,
word_chars.as_deref(),
&subword,
input_char,
i + 1,
) {
#[cfg(debug_assertions)]
eprintln!(
" State after transition: {} positions",
next_state.positions().count()
);
state = next_state;
} else {
#[cfg(debug_assertions)]
eprintln!(" ✗ Transition failed, rejecting");
return false;
}
}
#[cfg(debug_assertions)]
{
eprintln!("\n[DEBUG] Final state positions:");
for pos in state.positions() {
eprintln!("[DEBUG] {}", pos);
}
}
let accepted = self.is_accepting(&state, word.len(), input.len());
#[cfg(debug_assertions)]
eprintln!("[DEBUG] Accepted: {}", accepted);
accepted
}
fn relevant_subword(&self, word: &str, position: usize) -> String {
let n = self.max_distance as i32;
let i = position as i32;
let start = i - n;
let v = std::cmp::min(word.len() as i32, i + n + 1);
let mut result = String::new();
for pos in start..=v {
if pos < 1 {
result.push('$');
} else if pos <= word.len() as i32 {
let idx = (pos - 1) as usize;
if let Some(ch) = word.chars().nth(idx) {
result.push(ch);
}
}
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new() {
let automaton = GeneralizedAutomaton::new(2);
assert_eq!(automaton.max_distance(), 2);
}
#[test]
fn test_debug_identical() {
let automaton = GeneralizedAutomaton::new(2);
let word = "test";
let input = "test";
let mut state = automaton.initial_state();
eprintln!("\nDEBUG: Initial state = {}", state);
let word_chars: Vec<char> = word.chars().collect();
for (i, ch) in input.chars().enumerate() {
eprintln!(
"\nDEBUG: Processing char {} ('{}') at input position {}",
i + 1,
ch,
i
);
let subword = automaton.relevant_subword(word, i + 1);
eprintln!("DEBUG: Relevant subword = '{}'", subword);
let bit_vector = CharacteristicVector::new(ch, &subword);
eprintln!("DEBUG: Bit vector length = {}", bit_vector.len());
match state.transition(
&automaton.operations,
&bit_vector,
word,
Some(&word_chars),
&subword,
ch,
i + 1,
) {
Some(next) => {
eprintln!("DEBUG: Next state = {}", next);
state = next;
}
None => {
panic!("Transition failed at position {}", i);
}
}
}
eprintln!("\nDEBUG: Final state = {}", state);
eprintln!(
"DEBUG: Word length = {}, Input length = {}",
word.len(),
input.len()
);
let is_accepting = automaton.is_accepting(&state, word.len(), input.len());
eprintln!("DEBUG: is_accepting = {}", is_accepting);
if !is_accepting {
eprintln!("\nDEBUG: Checking each position:");
for pos in state.positions() {
let current_word_pos = input.len() as i32 + pos.offset();
let remaining_chars = word.len() as i32 - current_word_pos;
let remaining_errors = automaton.max_distance() as i32 - pos.errors() as i32;
eprintln!(" Position {}: current_word_pos={}, remaining_chars={}, remaining_errors={}, accepting={}",
pos, current_word_pos, remaining_chars, remaining_errors,
remaining_chars >= 0 && remaining_chars <= remaining_errors);
}
}
assert!(is_accepting, "Should accept identical strings");
}
#[test]
fn test_accepts_identical() {
let automaton = GeneralizedAutomaton::new(2);
assert!(automaton.accepts("test", "test"));
assert!(automaton.accepts("", ""));
assert!(automaton.accepts("hello", "hello"));
}
#[test]
fn test_accepts_one_substitution() {
let automaton = GeneralizedAutomaton::new(2);
assert!(automaton.accepts("test", "text")); assert!(automaton.accepts("hello", "hallo")); }
#[test]
fn test_debug_one_insertion() {
let automaton = GeneralizedAutomaton::new(2);
let word = "test";
let input = "tests";
eprintln!(
"\nDEBUG: Testing insertion word='{}', input='{}', max_distance={}",
word,
input,
automaton.max_distance()
);
let mut state = automaton.initial_state();
eprintln!("Initial state: {}", state);
let word_chars: Vec<char> = word.chars().collect();
for (i, ch) in input.chars().enumerate() {
eprintln!(
"\n--- Processing char {} ('{}') at position {} ---",
i + 1,
ch,
i + 1
);
let subword = automaton.relevant_subword(word, i + 1);
eprintln!("Relevant subword: '{}'", subword);
let bit_vector = CharacteristicVector::new(ch, &subword);
eprintln!("Bit vector length: {}", bit_vector.len());
match state.transition(
&automaton.operations,
&bit_vector,
word,
Some(&word_chars),
&subword,
ch,
i + 1,
) {
Some(next) => {
eprintln!("Next state: {}", next);
state = next;
}
None => {
eprintln!("Transition failed!");
panic!("Should not fail at position {}", i + 1);
}
}
}
eprintln!("\n--- Final state check ---");
eprintln!("Final state: {}", state);
eprintln!("Word length: {}", word.len());
eprintln!("Input length: {}", input.len());
let n = automaton.max_distance() as i32;
for pos in state.positions() {
let current_word_pos = input.len() as i32 + pos.offset();
let remaining_chars = word.len() as i32 - current_word_pos;
let remaining_errors = n - (pos.errors() as i32);
eprintln!("\nPosition: {}", pos);
eprintln!(
" current_word_pos = {} + {} = {}",
input.len(),
pos.offset(),
current_word_pos
);
eprintln!(
" remaining_chars = {} - {} = {}",
word.len(),
current_word_pos,
remaining_chars
);
eprintln!(
" remaining_errors = {} - {} = {}",
n,
pos.errors(),
remaining_errors
);
eprintln!(
" Accept? {} >= 0 && {} <= {} = {}",
remaining_chars,
remaining_chars,
remaining_errors,
remaining_chars >= 0 && remaining_chars <= remaining_errors
);
}
let is_accepting = automaton.is_accepting(&state, word.len(), input.len());
eprintln!("\nFinal is_accepting result: {}", is_accepting);
assert!(is_accepting, "Should accept insertion");
}
#[test]
fn test_accepts_one_insertion() {
let automaton = GeneralizedAutomaton::new(2);
assert!(automaton.accepts("test", "tests")); assert!(automaton.accepts("test", "ttest")); }
#[test]
fn test_accepts_one_deletion() {
let automaton = GeneralizedAutomaton::new(2);
assert!(automaton.accepts("tests", "test")); assert!(automaton.accepts("ttest", "test")); }
#[test]
fn test_rejects_too_far() {
let automaton = GeneralizedAutomaton::new(2);
assert!(!automaton.accepts("test", "hello")); assert!(!automaton.accepts("abc", "xyz")); }
#[test]
fn test_empty_input() {
let automaton = GeneralizedAutomaton::new(2);
assert!(automaton.accepts("", "")); assert!(automaton.accepts("ab", "")); assert!(!automaton.accepts("abc", "")); }
#[test]
fn test_empty_word() {
let automaton = GeneralizedAutomaton::new(2);
assert!(automaton.accepts("", "ab")); assert!(!automaton.accepts("", "abc")); }
#[test]
fn test_max_distance_zero() {
let automaton = GeneralizedAutomaton::new(0);
assert!(automaton.accepts("test", "test")); assert!(!automaton.accepts("test", "text")); }
#[test]
fn test_debug_deletion_middle() {
let automaton = GeneralizedAutomaton::new(1);
let word = "test";
let input = "tst";
eprintln!(
"\nDEBUG: Testing deletion in middle: word='{}', input='{}', max_distance={}",
word,
input,
automaton.max_distance()
);
eprintln!("Expected: true (1 deletion)");
let mut state = automaton.initial_state();
eprintln!("\nInitial state: {}", state);
let word_chars: Vec<char> = word.chars().collect();
for (i, ch) in input.chars().enumerate() {
eprintln!(
"\n--- Processing char {} ('{}') at position {} ---",
i + 1,
ch,
i + 1
);
let subword = automaton.relevant_subword(word, i + 1);
eprintln!("Relevant subword: '{}'", subword);
let bit_vector = CharacteristicVector::new(ch, &subword);
eprintln!("Bit vector length: {}", bit_vector.len());
match state.transition(
&automaton.operations,
&bit_vector,
word,
Some(&word_chars),
&subword,
ch,
i + 1,
) {
Some(next) => {
eprintln!("Next state: {}", next);
state = next;
}
None => {
eprintln!("Transition failed - no successor state!");
panic!("Should not fail at position {}", i + 1);
}
}
}
eprintln!("\n--- Final state check ---");
eprintln!("Final state: {}", state);
eprintln!("Word length: {}", word.len());
eprintln!("Input length: {}", input.len());
let n = automaton.max_distance() as i32;
eprintln!("\nChecking acceptance for each position:");
use crate::transducer::generalized::GeneralizedPosition;
for pos in state.positions() {
match pos {
GeneralizedPosition::MFinal { offset, errors } => {
eprintln!("\nM-type position: offset={}, errors={}", offset, errors);
eprintln!(
" M-type accepting? offset <= 0 && errors <= {}: {} <= 0 && {} <= {} = {}",
n,
offset,
errors,
n,
*offset <= 0 && *errors <= n as u8
);
}
GeneralizedPosition::INonFinal { offset, errors } => {
let current_word_pos = input.len() as i32 + offset;
let remaining_chars = word.len() as i32 - current_word_pos;
let remaining_errors = n - (*errors as i32);
eprintln!("\nI-type position: offset={}, errors={}", offset, errors);
eprintln!(
" current_word_pos = {} + {} = {}",
input.len(),
offset,
current_word_pos
);
eprintln!(
" remaining_chars = {} - {} = {}",
word.len(),
current_word_pos,
remaining_chars
);
eprintln!(
" remaining_errors = {} - {} = {}",
n, errors, remaining_errors
);
eprintln!(
" Accept? {} >= 0 && {} <= {} = {}",
remaining_chars,
remaining_chars,
remaining_errors,
remaining_chars >= 0 && remaining_chars <= remaining_errors
);
}
GeneralizedPosition::ITransposing { offset, errors } => {
eprintln!("\nI-type transposing: offset={}, errors={} (not accepting - intermediate state)", offset, errors);
}
GeneralizedPosition::MTransposing { offset, errors } => {
eprintln!("\nM-type transposing: offset={}, errors={} (not accepting - intermediate state)", offset, errors);
}
GeneralizedPosition::ISplitting { offset, errors, .. } => {
eprintln!("\nI-type splitting: offset={}, errors={} (not accepting - intermediate state)", offset, errors);
}
GeneralizedPosition::MSplitting { offset, errors, .. } => {
eprintln!("\nM-type splitting: offset={}, errors={} (not accepting - intermediate state)", offset, errors);
}
}
}
let result = automaton.accepts(word, input);
eprintln!("\n=== Final result: {} ===", result);
eprintln!("Expected: true");
assert!(result, "Should accept deletion in middle");
}
#[test]
fn test_max_distance_one() {
let automaton = GeneralizedAutomaton::new(1);
assert!(automaton.accepts("test", "text")); assert!(automaton.accepts("test", "tst")); assert!(!automaton.accepts("test", "tx")); }
#[test]
fn test_transposition_distance_zero() {
let ops = crate::transducer::OperationSet::with_transposition();
let automaton = GeneralizedAutomaton::with_operations(0, ops);
assert!(automaton.accepts("test", "test")); assert!(!automaton.accepts("test", "tset")); assert!(!automaton.accepts("test", "etst")); }
#[test]
fn test_transposition_adjacent_swap_middle() {
let ops = crate::transducer::OperationSet::with_transposition();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(automaton.accepts("test", "tset"));
}
#[test]
fn test_transposition_adjacent_swap_start() {
let ops = crate::transducer::OperationSet::with_transposition();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(automaton.accepts("test", "etst"));
}
#[test]
fn test_transposition_adjacent_swap_end() {
let ops = crate::transducer::OperationSet::with_transposition();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(automaton.accepts("test", "tets"));
}
#[test]
fn test_transposition_longer_words() {
let ops = crate::transducer::OperationSet::with_transposition();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(automaton.accepts("algorithm", "lagorithm"));
assert!(automaton.accepts("programming", "porgramming"));
}
#[test]
fn test_transposition_rejects_non_adjacent() {
let ops = crate::transducer::OperationSet::with_transposition();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(!automaton.accepts("test", "tsta"));
assert!(!automaton.accepts("abc", "cba"));
}
#[test]
fn test_transposition_multiple_swaps() {
let ops = crate::transducer::OperationSet::with_transposition();
let automaton = GeneralizedAutomaton::with_operations(2, ops);
assert!(automaton.accepts("abcd", "badc"));
assert!(automaton.accepts("test", "etts"));
}
#[test]
fn test_transposition_with_standard_operations() {
let ops = crate::transducer::OperationSet::with_transposition();
let automaton = GeneralizedAutomaton::with_operations(2, ops);
assert!(automaton.accepts("test", "tsxt"));
assert!(automaton.accepts("test", "set"));
}
#[test]
fn test_transposition_empty_and_single() {
let ops = crate::transducer::OperationSet::with_transposition();
let automaton = GeneralizedAutomaton::with_operations(1, ops.clone());
assert!(automaton.accepts("", ""));
assert!(automaton.accepts("a", "a"));
assert!(automaton.accepts("a", "b"));
let strict_automaton = GeneralizedAutomaton::with_operations(0, ops);
assert!(!strict_automaton.accepts("a", "b")); }
#[test]
fn test_transposition_two_char_words() {
let ops = crate::transducer::OperationSet::with_transposition();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(automaton.accepts("ab", "ba"));
assert!(automaton.accepts("xy", "yx"));
assert!(automaton.accepts("aa", "aa"));
}
#[test]
fn test_merge_simple() {
let ops = crate::transducer::OperationSet::with_merge_split();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(automaton.accepts("a", "ab"));
}
#[test]
fn test_merge_at_start() {
let ops = crate::transducer::OperationSet::with_merge_split();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(automaton.accepts("ac", "abc"));
assert!(automaton.accepts("test", "teest"));
}
#[test]
fn test_merge_at_end() {
let ops = crate::transducer::OperationSet::with_merge_split();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(automaton.accepts("xa", "xab"));
assert!(automaton.accepts("testa", "testab"));
}
#[test]
fn test_merge_middle() {
let ops = crate::transducer::OperationSet::with_merge_split();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(automaton.accepts("cat", "cabt"));
}
#[test]
fn test_merge_distance_zero() {
let ops = crate::transducer::OperationSet::with_merge_split();
let automaton = GeneralizedAutomaton::with_operations(0, ops);
assert!(automaton.accepts("test", "test"));
assert!(!automaton.accepts("test", "teest"));
assert!(!automaton.accepts("a", "ab"));
}
#[test]
fn test_merge_with_standard_operations() {
let ops = crate::transducer::OperationSet::with_merge_split();
let automaton = GeneralizedAutomaton::with_operations(2, ops);
assert!(automaton.accepts("test", "texst"));
assert!(automaton.accepts("test", "eest"));
}
#[test]
fn test_merge_empty_and_edge_cases() {
let ops = crate::transducer::OperationSet::with_merge_split();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(automaton.accepts("", ""));
assert!(automaton.accepts("a", "a"));
assert!(automaton.accepts("a", "ab"));
assert!(automaton.accepts("ab", "ab"));
assert!(automaton.accepts("ab", "abb")); }
#[test]
fn test_split_simple() {
let ops = crate::transducer::OperationSet::with_merge_split();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(automaton.accepts("ab", "a"));
}
#[test]
fn test_split_at_start() {
let ops = crate::transducer::OperationSet::with_merge_split();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(automaton.accepts("abc", "ac"));
assert!(automaton.accepts("abcd", "acd"));
}
#[test]
fn test_split_at_end() {
let ops = crate::transducer::OperationSet::with_merge_split();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(automaton.accepts("xab", "xa"));
assert!(automaton.accepts("testab", "testa"));
}
#[test]
fn test_split_middle() {
let ops = crate::transducer::OperationSet::with_merge_split();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(automaton.accepts("cat", "caat"));
}
#[test]
fn test_split_distance_zero() {
let ops = crate::transducer::OperationSet::with_merge_split();
let automaton = GeneralizedAutomaton::with_operations(0, ops);
assert!(automaton.accepts("test", "test"));
assert!(!automaton.accepts("ttest", "test"));
assert!(!automaton.accepts("ab", "a"));
}
#[test]
fn test_split_with_standard_operations() {
let ops = crate::transducer::OperationSet::with_merge_split();
let automaton = GeneralizedAutomaton::with_operations(2, ops);
assert!(automaton.accepts("test", "txst"));
assert!(automaton.accepts("test", "test"));
}
#[test]
fn test_split_empty_and_edge_cases() {
let ops = crate::transducer::OperationSet::with_merge_split();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(automaton.accepts("", ""));
assert!(automaton.accepts("a", "a"));
assert!(automaton.accepts("ab", "a"));
assert!(automaton.accepts("ab", "ab"));
assert!(automaton.accepts("abb", "ab")); }
#[test]
fn test_split_and_merge_combined() {
let ops = crate::transducer::OperationSet::with_merge_split();
let automaton = GeneralizedAutomaton::with_operations(2, ops);
assert!(automaton.accepts("abc", "ac"));
assert!(automaton.accepts("test", "test"));
}
#[test]
fn test_all_multichar_operations_combined() {
let ops = crate::transducer::OperationSetBuilder::new()
.with_standard_ops()
.with_transposition()
.with_merge()
.with_split()
.build();
let automaton = GeneralizedAutomaton::with_operations(3, ops);
assert!(automaton.accepts("abc", "acbc"));
assert!(automaton.accepts("hello", "hello"));
}
#[test]
fn test_multichar_with_distance_constraints() {
let ops = crate::transducer::OperationSetBuilder::new()
.with_standard_ops()
.with_transposition()
.with_merge()
.with_split()
.build();
let automaton1 = GeneralizedAutomaton::with_operations(1, ops.clone());
assert!(automaton1.accepts("ab", "ba")); assert!(automaton1.accepts("a", "aa")); assert!(!automaton1.accepts("ab", "bba"));
let automaton2 = GeneralizedAutomaton::with_operations(2, ops.clone());
assert!(automaton2.accepts("abc", "baca"));
let automaton3 = GeneralizedAutomaton::with_operations(3, ops);
assert!(automaton3.accepts("abc", "bbcaa")); }
#[test]
fn test_multichar_operations_at_string_boundaries() {
let ops = crate::transducer::OperationSetBuilder::new()
.with_standard_ops()
.with_transposition()
.with_merge()
.with_split()
.build();
let automaton = GeneralizedAutomaton::with_operations(2, ops);
assert!(automaton.accepts("ab", "ba"));
assert!(automaton.accepts("cab", "cba"));
assert!(automaton.accepts("abc", "aabc"));
assert!(automaton.accepts("abc", "abcc"));
assert!(automaton.accepts("abc", "bc"));
assert!(automaton.accepts("abc", "ab")); }
#[test]
fn test_repeated_multichar_operations() {
let ops = crate::transducer::OperationSetBuilder::new()
.with_standard_ops()
.with_transposition()
.with_merge()
.with_split()
.build();
let automaton2 = GeneralizedAutomaton::with_operations(2, ops.clone());
assert!(automaton2.accepts("abcd", "badc"));
assert!(automaton2.accepts("ab", "aabb"));
let automaton3 = GeneralizedAutomaton::with_operations(3, ops);
assert!(automaton3.accepts("abc", "aabbcc")); }
#[test]
fn test_multichar_with_standard_operations_complex() {
let ops = crate::transducer::OperationSetBuilder::new()
.with_standard_ops()
.with_transposition()
.with_merge()
.with_split()
.build();
let automaton = GeneralizedAutomaton::with_operations(3, ops);
assert!(automaton.accepts("abc", "bac")); assert!(automaton.accepts("abc", "bacd")); assert!(automaton.accepts("abcd", "bac"));
assert!(automaton.accepts("abc", "aabc")); assert!(automaton.accepts("abc", "aabx"));
assert!(automaton.accepts("abc", "bc")); assert!(automaton.accepts("abc", "bcd")); }
#[test]
fn test_multichar_edge_cases() {
let ops = crate::transducer::OperationSetBuilder::new()
.with_standard_ops()
.with_transposition()
.with_merge()
.with_split()
.build();
let automaton = GeneralizedAutomaton::with_operations(2, ops);
assert!(automaton.accepts("", ""));
assert!(automaton.accepts("a", "")); assert!(automaton.accepts("", "a"));
assert!(automaton.accepts("a", "a"));
assert!(automaton.accepts("a", "aa"));
assert!(automaton.accepts("ab", "ba")); assert!(automaton.accepts("ab", "aab")); assert!(automaton.accepts("ab", "abb"));
assert!(automaton.accepts("test", "test"));
assert!(automaton.accepts("hello", "hello"));
}
#[test]
fn test_multichar_pathological_cases() {
let ops = crate::transducer::OperationSetBuilder::new()
.with_standard_ops()
.with_transposition()
.with_merge()
.with_split()
.build();
let automaton = GeneralizedAutomaton::with_operations(5, ops);
assert!(automaton.accepts("aaaa", "aaaa"));
assert!(automaton.accepts("aaaa", "aaaaaaaa")); assert!(automaton.accepts("aaaa", "aa"));
assert!(automaton.accepts("abab", "baba")); assert!(automaton.accepts("abab", "aabbab"));
assert!(automaton.accepts("abc", "cba")); }
#[test]
fn test_multichar_operations_respect_invariants() {
let ops = crate::transducer::OperationSetBuilder::new()
.with_standard_ops()
.with_transposition()
.with_merge()
.with_split()
.build();
let automaton0 = GeneralizedAutomaton::with_operations(0, ops.clone());
assert!(automaton0.accepts("test", "test"));
assert!(!automaton0.accepts("test", "tset")); assert!(!automaton0.accepts("test", "teest")); assert!(!automaton0.accepts("test", "tes"));
let automaton1 = GeneralizedAutomaton::with_operations(1, ops.clone());
assert!(automaton1.accepts("test", "tset")); assert!(automaton1.accepts("test", "teest")); assert!(!automaton1.accepts("test", "tseest"));
assert!(!automaton1.accepts("abc", "aabbcc")); }
#[test]
fn test_multichar_subsumption_correctness() {
let ops = crate::transducer::OperationSetBuilder::new()
.with_standard_ops()
.with_transposition()
.with_merge()
.with_split()
.build();
let automaton = GeneralizedAutomaton::with_operations(2, ops);
assert!(automaton.accepts("ab", "ba")); assert!(automaton.accepts("abc", "bac")); assert!(automaton.accepts("test", "tset"));
assert!(automaton.accepts("a", "aa")); assert!(automaton.accepts("ab", "aab")); assert!(automaton.accepts("ab", "aabb"));
assert!(automaton.accepts("abcd", "bacd")); assert!(automaton.accepts("abcd", "abdc")); }
#[test]
fn test_multichar_operation_ordering() {
let ops = crate::transducer::OperationSetBuilder::new()
.with_standard_ops()
.with_transposition()
.with_merge()
.with_split()
.build();
let automaton = GeneralizedAutomaton::with_operations(3, ops);
assert!(automaton.accepts("abc", "baac"));
assert!(automaton.accepts("abc", "abba"));
assert!(automaton.accepts("ab", "baa"));
assert!(automaton.accepts("abc", "aabc")); assert!(automaton.accepts("abc", "bac")); assert!(automaton.accepts("abc", "ab")); }
#[test]
fn test_phonetic_debug_simple() {
let ops = crate::transducer::phonetic::consonant_digraphs();
eprintln!("Operation set has {} operations", ops.operations().len());
for op in ops.operations() {
eprintln!(
" Operation: consume_x={}, consume_y={}, weight={}",
op.consume_x(),
op.consume_y(),
op.weight()
);
}
let automaton = GeneralizedAutomaton::with_operations(1, ops);
eprintln!("\n=== Testing 'ph' → 'f' ===");
let subword = automaton.relevant_subword("ph", 1);
eprintln!("Relevant subword at position 1: '{}'", subword);
eprintln!("Subword chars: {:?}", subword.chars().collect::<Vec<_>>());
let result = automaton.accepts("ph", "f");
eprintln!("Result: {}", result);
assert!(result, "Expected 'ph' → 'f' to be accepted");
}
#[test]
fn test_phonetic_digraph_2to1_ch_to_k() {
let phonetic_ops = crate::transducer::phonetic::consonant_digraphs();
let mut builder = crate::transducer::OperationSetBuilder::new().with_standard_ops();
for op in phonetic_ops.operations() {
builder = builder.with_operation(op.clone());
}
let ops = builder.build();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(automaton.accepts("church", "kurk"));
assert!(automaton.accepts("chair", "kair"));
let ops0 = crate::transducer::OperationSet::default();
let automaton0 = GeneralizedAutomaton::with_operations(0, ops0);
assert!(automaton0.accepts("church", "church")); assert!(!automaton0.accepts("church", "kurk")); }
#[test]
fn test_phonetic_digraph_2to1_ph_to_f() {
let phonetic_ops = crate::transducer::phonetic::consonant_digraphs();
let mut builder = crate::transducer::OperationSetBuilder::new().with_standard_ops();
for op in phonetic_ops.operations() {
builder = builder.with_operation(op.clone());
}
let ops = builder.build();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(automaton.accepts("phone", "fone"));
assert!(automaton.accepts("graph", "graf"));
}
#[test]
fn test_phonetic_digraph_2to1_sh_to_s() {
let phonetic_ops = crate::transducer::phonetic::consonant_digraphs();
let mut builder = crate::transducer::OperationSetBuilder::new().with_standard_ops();
for op in phonetic_ops.operations() {
builder = builder.with_operation(op.clone());
}
let ops = builder.build();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(automaton.accepts("ship", "sip"));
assert!(automaton.accepts("wash", "was"));
}
#[test]
fn test_phonetic_digraph_2to1_th_to_t() {
let phonetic_ops = crate::transducer::phonetic::consonant_digraphs();
let mut builder = crate::transducer::OperationSetBuilder::new().with_standard_ops();
for op in phonetic_ops.operations() {
builder = builder.with_operation(op.clone());
}
let ops = builder.build();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(automaton.accepts("think", "tink"));
assert!(automaton.accepts("bath", "bat"));
}
#[test]
fn test_phonetic_digraph_multiple_in_word() {
let phonetic_ops = crate::transducer::phonetic::consonant_digraphs();
let mut builder = crate::transducer::OperationSetBuilder::new().with_standard_ops();
for op in phonetic_ops.operations() {
builder = builder.with_operation(op.clone());
}
let ops = builder.build();
let automaton = GeneralizedAutomaton::with_operations(2, ops);
assert!(automaton.accepts("church", "kurc")); assert!(automaton.accepts("church", "churk")); }
#[test]
fn test_phonetic_with_standard_ops() {
let phonetic_ops = crate::transducer::phonetic::consonant_digraphs();
let mut builder = crate::transducer::OperationSetBuilder::new().with_standard_ops();
for op in phonetic_ops.operations() {
builder = builder.with_operation(op.clone());
}
let ops = builder.build();
let automaton = GeneralizedAutomaton::with_operations(2, ops);
assert!(automaton.accepts("phone", "fones"));
assert!(automaton.accepts("chair", "kair")); assert!(automaton.accepts("chair", "kair")); }
#[test]
fn test_phonetic_distance_constraints() {
let phonetic_ops = crate::transducer::phonetic::consonant_digraphs();
let mut builder = crate::transducer::OperationSetBuilder::new().with_standard_ops();
for op in phonetic_ops.operations() {
builder = builder.with_operation(op.clone());
}
let ops = builder.build();
let automaton1 = GeneralizedAutomaton::with_operations(1, ops.clone());
assert!(automaton1.accepts("phone", "fone")); assert!(!automaton1.accepts("phone", "fo"));
let automaton2 = GeneralizedAutomaton::with_operations(2, ops);
assert!(automaton2.accepts("phone", "fo")); }
#[test]
fn test_cross_validate_standard_operations() {
use crate::transducer::universal::Standard;
use crate::transducer::universal::UniversalAutomaton;
let test_cases = vec![
("kitten", "sitting", 3, true),
("kitten", "sitting", 2, false),
("saturday", "sunday", 3, true),
("saturday", "sunday", 2, false),
("test", "test", 0, true),
("test", "tast", 1, true),
("", "", 0, true),
("a", "b", 1, true),
("abc", "def", 3, true),
];
for (word, input, distance, expected) in test_cases {
let gen_auto = GeneralizedAutomaton::new(distance);
let univ_auto = UniversalAutomaton::<Standard>::new(distance);
let gen_result = gen_auto.accepts(word, input);
let univ_result = univ_auto.accepts(word, input);
assert_eq!(
gen_result, univ_result,
"Mismatch for ('{}', '{}', {}): gen={}, univ={}",
word, input, distance, gen_result, univ_result
);
assert_eq!(
gen_result, expected,
"Expected {} for ('{}', '{}', {}), got {}",
expected, word, input, distance, gen_result
);
}
}
#[test]
fn test_cross_validate_phonetic_merge_simple() {
let phonetic_ops = crate::transducer::phonetic::consonant_digraphs();
let mut builder = crate::transducer::OperationSetBuilder::new().with_standard_ops();
for op in phonetic_ops.operations() {
builder = builder.with_operation(op.clone());
}
let ops = builder.build();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
let accept_cases = vec![
("phone", "fone"), ("graph", "graf"), ("ship", "sip"), ("think", "tink"), ("church", "kurc"), ("chair", "kair"), ];
for (word, input) in accept_cases {
assert!(
automaton.accepts(word, input),
"Should accept ('{}', '{}')",
word,
input
);
}
let reject_cases = vec![
("phone", "fo"), ("church", "urk"), ];
for (word, input) in reject_cases {
assert!(
!automaton.accepts(word, input),
"Should reject ('{}', '{}') at distance 1",
word,
input
);
}
}
#[test]
fn test_cross_validate_fractional_weights() {
let phonetic_ops = crate::transducer::phonetic::consonant_digraphs();
let mut builder = crate::transducer::OperationSetBuilder::new().with_standard_ops();
for op in phonetic_ops.operations() {
builder = builder.with_operation(op.clone());
}
let ops = builder.build();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(
automaton.accepts("church", "kurk"),
"Two phonetic operations (2×0.15=0 errors) should work at distance 1"
);
assert!(
automaton.accepts("church", "kurks"),
"Two phonetic + one standard operation (total 1 error) should work at distance 1"
);
assert!(
!automaton.accepts("church", "korks"),
"Two phonetic + two standard operations should fail at distance 1"
);
}
#[test]
fn test_phonetic_split_k_to_ch() {
let phonetic_ops = crate::transducer::phonetic::consonant_digraphs();
let mut builder = crate::transducer::OperationSetBuilder::new().with_standard_ops();
for op in phonetic_ops.operations() {
builder = builder.with_operation(op.clone());
}
let ops = builder.build();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(
automaton.accepts("ark", "arch"),
"Split k→ch should work at distance 1"
);
assert!(
automaton.accepts("back", "bach"),
"Split k→ch at word end should work"
);
assert!(
automaton.accepts("kan", "chan"),
"Split k→ch at word start should work"
);
}
#[test]
fn test_phonetic_split_f_to_ph() {
let phonetic_ops = crate::transducer::phonetic::consonant_digraphs();
let mut builder = crate::transducer::OperationSetBuilder::new().with_standard_ops();
for op in phonetic_ops.operations() {
builder = builder.with_operation(op.clone());
}
let ops = builder.build();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(
automaton.accepts("graf", "graph"),
"Split f→ph should work at distance 1"
);
assert!(
automaton.accepts("foto", "photo"),
"Split f→ph at word start should work"
);
}
#[test]
fn test_phonetic_split_s_to_sh() {
let phonetic_ops = crate::transducer::phonetic::consonant_digraphs();
let mut builder = crate::transducer::OperationSetBuilder::new().with_standard_ops();
for op in phonetic_ops.operations() {
builder = builder.with_operation(op.clone());
}
let ops = builder.build();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(
automaton.accepts("sip", "ship"),
"Split s→sh should work at distance 1"
);
assert!(
automaton.accepts("sell", "shell"),
"Split s→sh at word start should work"
);
}
#[test]
fn test_phonetic_split_t_to_th() {
let phonetic_ops = crate::transducer::phonetic::consonant_digraphs();
let mut builder = crate::transducer::OperationSetBuilder::new().with_standard_ops();
for op in phonetic_ops.operations() {
builder = builder.with_operation(op.clone());
}
let ops = builder.build();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(
automaton.accepts("bat", "bath"),
"Split t→th should work at distance 1"
);
assert!(
automaton.accepts("tin", "thin"),
"Split t→th at word start should work"
);
}
#[test]
fn test_phonetic_split_multiple() {
let phonetic_ops = crate::transducer::phonetic::consonant_digraphs();
let mut builder = crate::transducer::OperationSetBuilder::new().with_standard_ops();
for op in phonetic_ops.operations() {
builder = builder.with_operation(op.clone());
}
let ops = builder.build();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(
automaton.accepts("kair", "chair"),
"Single k→ch split should work at distance 1"
);
assert!(
automaton.accepts("kat", "chath"),
"Two splits (k→ch, t→th) with fractional weights should work at distance 1"
);
}
#[test]
fn test_phonetic_split_with_standard_ops() {
let phonetic_ops = crate::transducer::phonetic::consonant_digraphs();
let mut builder = crate::transducer::OperationSetBuilder::new().with_standard_ops();
for op in phonetic_ops.operations() {
builder = builder.with_operation(op.clone());
}
let ops = builder.build();
let automaton = GeneralizedAutomaton::with_operations(2, ops);
assert!(
automaton.accepts("graf", "graphe"),
"Split f→ph + insert 'e' should work at distance 1"
);
assert!(
automaton.accepts("bak", "batch"),
"Split k→ch + insert should work at distance 1"
);
}
#[test]
fn test_phonetic_split_distance_constraints() {
let phonetic_ops = crate::transducer::phonetic::consonant_digraphs();
let mut builder = crate::transducer::OperationSetBuilder::new().with_standard_ops();
for op in phonetic_ops.operations() {
builder = builder.with_operation(op.clone());
}
let ops = builder.build();
let automaton = GeneralizedAutomaton::with_operations(0, ops.clone());
assert!(
!automaton.accepts("ark", "arch"),
"Split should not work at distance 0"
);
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(
automaton.accepts("ark", "arch"),
"Split should work at distance 1"
);
}
#[test]
fn test_phonetic_transpose_qu_to_kw() {
let phonetic_ops = crate::transducer::phonetic::consonant_digraphs();
let mut builder = crate::transducer::OperationSetBuilder::new().with_standard_ops();
for op in phonetic_ops.operations() {
builder = builder.with_operation(op.clone());
}
let ops = builder.build();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(
automaton.accepts("queen", "kween"),
"Transpose qu→kw should work at distance 1"
);
assert!(
automaton.accepts("quick", "kwick"),
"Transpose qu→kw at word start should work"
);
assert!(
automaton.accepts("quit", "kwit"),
"Transpose qu→kw should work"
);
}
#[test]
fn test_phonetic_transpose_kw_to_qu() {
let phonetic_ops = crate::transducer::phonetic::consonant_digraphs();
let mut builder = crate::transducer::OperationSetBuilder::new().with_standard_ops();
for op in phonetic_ops.operations() {
builder = builder.with_operation(op.clone());
}
let ops = builder.build();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(
automaton.accepts("kween", "queen"),
"Transpose kw→qu should work at distance 1"
);
assert!(
automaton.accepts("kwik", "quik"),
"Transpose kw→qu should work"
);
}
#[test]
fn test_phonetic_transpose_multiple() {
let phonetic_ops = crate::transducer::phonetic::consonant_digraphs();
let mut builder = crate::transducer::OperationSetBuilder::new().with_standard_ops();
for op in phonetic_ops.operations() {
builder = builder.with_operation(op.clone());
}
let ops = builder.build();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(
automaton.accepts("queen", "kween"),
"Single transpose should work at distance 1"
);
assert!(
automaton.accepts("ququ", "kwkw"),
"Two transposes with fractional weights should work at distance 1"
);
}
#[test]
fn test_phonetic_transpose_with_standard_ops() {
let phonetic_ops = crate::transducer::phonetic::consonant_digraphs();
let mut builder = crate::transducer::OperationSetBuilder::new().with_standard_ops();
for op in phonetic_ops.operations() {
builder = builder.with_operation(op.clone());
}
let ops = builder.build();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(
automaton.accepts("queen", "kweens"),
"Transpose + insert should work at distance 1"
);
}
#[test]
fn test_phonetic_transpose_distance_constraints() {
let phonetic_ops = crate::transducer::phonetic::consonant_digraphs();
let mut builder = crate::transducer::OperationSetBuilder::new().with_standard_ops();
for op in phonetic_ops.operations() {
builder = builder.with_operation(op.clone());
}
let ops = builder.build();
let automaton = GeneralizedAutomaton::with_operations(0, ops.clone());
assert!(
!automaton.accepts("queen", "kween"),
"Transpose should not work at distance 0"
);
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(
automaton.accepts("queen", "kween"),
"Transpose should work at distance 1"
);
}
#[test]
fn test_phonetic_mixed_merge_split_transpose() {
let phonetic_ops = crate::transducer::phonetic::consonant_digraphs();
let mut builder = crate::transducer::OperationSetBuilder::new().with_standard_ops();
for op in phonetic_ops.operations() {
builder = builder.with_operation(op.clone());
}
let ops = builder.build();
let automaton = GeneralizedAutomaton::with_operations(1, ops);
assert!(
automaton.accepts("phone", "fone"),
"Merge operation should work"
);
assert!(
automaton.accepts("graf", "graph"),
"Split operation should work"
);
assert!(
automaton.accepts("queen", "kween"),
"Transpose operation should work"
);
}
}