use crate::transducer::substitution_set::SubstitutionSet;
use std::fmt;
#[derive(Clone, Debug)]
pub struct OperationType {
consume_x: usize,
consume_y: usize,
weight: f64,
restriction: Option<SubstitutionSet>,
name: &'static str,
}
impl OperationType {
#[inline]
pub fn new(consume_x: usize, consume_y: usize, weight: f64, name: &'static str) -> Self {
assert!(weight >= 0.0, "Operation weight must be non-negative");
if weight == 0.0 {
assert_eq!(
consume_x, consume_y,
"Zero-weight operation must preserve length (consume_x == consume_y)"
);
}
Self {
consume_x,
consume_y,
weight,
restriction: None,
name,
}
}
#[inline]
pub fn with_restriction(
consume_x: usize,
consume_y: usize,
weight: f64,
restriction: SubstitutionSet,
name: &'static str,
) -> Self {
let mut op = Self::new(consume_x, consume_y, weight, name);
op.restriction = Some(restriction);
op
}
#[inline]
pub fn consume_x(&self) -> usize {
self.consume_x
}
#[inline]
pub fn consume_y(&self) -> usize {
self.consume_y
}
#[inline]
pub fn weight(&self) -> f64 {
self.weight
}
#[inline]
pub fn name(&self) -> &'static str {
self.name
}
#[inline]
pub fn is_restricted(&self) -> bool {
self.restriction.is_some()
}
#[inline]
pub fn restriction(&self) -> Option<&SubstitutionSet> {
self.restriction.as_ref()
}
#[inline]
pub fn is_match(&self) -> bool {
self.weight == 0.0
}
#[inline]
pub fn is_insertion(&self) -> bool {
self.consume_x == 0 && self.consume_y > 0
}
#[inline]
pub fn is_deletion(&self) -> bool {
self.consume_x > 0 && self.consume_y == 0
}
#[inline]
pub fn is_substitution(&self) -> bool {
self.consume_x == 1 && self.consume_y == 1 && self.weight > 0.0
}
#[inline]
pub fn can_apply(&self, dict_chars: &[u8], query_chars: &[u8]) -> bool {
if dict_chars.len() != self.consume_x || query_chars.len() != self.consume_y {
return false;
}
if self.is_match() {
return dict_chars == query_chars;
}
match &self.restriction {
None => true, Some(set) => set.contains_str(dict_chars, query_chars),
}
}
#[inline]
pub fn can_apply_to_source(&self, dict_chars: &[u8]) -> bool {
if dict_chars.len() != self.consume_x {
return false;
}
match &self.restriction {
None => true, Some(set) => set.has_source(dict_chars),
}
}
#[inline]
pub fn matches_first_target_char(&self, dict_chars: &[u8], first_target_char: char) -> bool {
if dict_chars.len() != self.consume_x {
return false;
}
match &self.restriction {
None => false, Some(set) => set.has_target_starting_with(dict_chars, first_target_char),
}
}
}
impl fmt::Display for OperationType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}⟨{}, {}, {}⟩",
self.name, self.consume_x, self.consume_y, self.weight
)?;
if self.is_restricted() {
write!(f, " [restricted]")?;
}
Ok(())
}
}
impl PartialEq for OperationType {
fn eq(&self, other: &Self) -> bool {
self.consume_x == other.consume_x
&& self.consume_y == other.consume_y
&& (self.weight - other.weight).abs() < f64::EPSILON
&& self.restriction == other.restriction
&& self.name == other.name
}
}
impl Eq for OperationType {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_standard_operations() {
let match_op = OperationType::new(1, 1, 0.0, "match");
assert!(match_op.is_match());
assert!(!match_op.is_restricted());
assert_eq!(match_op.consume_x(), 1);
assert_eq!(match_op.consume_y(), 1);
assert_eq!(match_op.weight(), 0.0);
let subst_op = OperationType::new(1, 1, 1.0, "substitute");
assert!(subst_op.is_substitution());
assert!(!subst_op.is_match());
let insert_op = OperationType::new(0, 1, 1.0, "insert");
assert!(insert_op.is_insertion());
let delete_op = OperationType::new(1, 0, 1.0, "delete");
assert!(delete_op.is_deletion());
}
#[test]
#[should_panic(expected = "Zero-weight operation must preserve length")]
fn test_zero_weight_must_preserve_length() {
OperationType::new(2, 1, 0.0, "invalid");
}
#[test]
#[should_panic(expected = "Operation weight must be non-negative")]
fn test_negative_weight_panics() {
OperationType::new(1, 1, -0.5, "invalid");
}
#[test]
fn test_match_requires_equality() {
let match_op = OperationType::new(1, 1, 0.0, "match");
assert!(match_op.can_apply(b"a", b"a"));
assert!(!match_op.can_apply(b"a", b"b"));
}
#[test]
fn test_unrestricted_operations() {
let subst = OperationType::new(1, 1, 1.0, "substitute");
assert!(subst.can_apply(b"a", b"b"));
assert!(subst.can_apply(b"x", b"y"));
}
#[test]
fn test_restricted_operation() {
let mut phonetic = SubstitutionSet::new();
phonetic.allow('f', 'p');
phonetic.allow('p', 'h');
let restricted = OperationType::with_restriction(1, 1, 0.3, phonetic, "phonetic");
assert!(restricted.is_restricted());
assert!(restricted.can_apply(b"f", b"p"));
assert!(restricted.can_apply(b"p", b"h"));
assert!(!restricted.can_apply(b"a", b"b"));
}
#[test]
fn test_display() {
let op = OperationType::new(2, 1, 0.15, "ph_to_f");
let display = format!("{}", op);
assert!(display.contains("ph_to_f"));
assert!(display.contains("2"));
assert!(display.contains("1"));
assert!(display.contains("0.15"));
}
#[test]
fn test_can_apply_to_source_unrestricted() {
let subst = OperationType::new(1, 1, 1.0, "substitution");
assert!(subst.can_apply_to_source(b"a"));
assert!(subst.can_apply_to_source(b"x"));
assert!(subst.can_apply_to_source(b"z"));
assert!(!subst.can_apply_to_source(b"ab"));
assert!(!subst.can_apply_to_source(b""));
}
#[test]
fn test_can_apply_to_source_restricted() {
let mut phonetic = SubstitutionSet::new();
phonetic.allow_str("k", "ch");
phonetic.allow_str("t", "th");
let split_op = OperationType::with_restriction(1, 2, 0.15, phonetic, "split");
assert!(split_op.can_apply_to_source(b"k")); assert!(split_op.can_apply_to_source(b"t"));
assert!(!split_op.can_apply_to_source(b"a"));
assert!(!split_op.can_apply_to_source(b"e"));
assert!(!split_op.can_apply_to_source(b"s"));
assert!(!split_op.can_apply_to_source(b"ch")); }
#[test]
fn test_can_apply_to_source_multi_char() {
let mut phonetic = SubstitutionSet::new();
phonetic.allow_str("ch", "k");
phonetic.allow_str("sh", "s");
let merge_op = OperationType::with_restriction(2, 1, 0.15, phonetic, "merge");
assert!(merge_op.can_apply_to_source(b"ch")); assert!(merge_op.can_apply_to_source(b"sh"));
assert!(!merge_op.can_apply_to_source(b"ph"));
assert!(!merge_op.can_apply_to_source(b"th"));
assert!(!merge_op.can_apply_to_source(b"c"));
assert!(!merge_op.can_apply_to_source(b"s"));
}
#[test]
fn test_can_apply_to_source_vs_can_apply() {
let mut phonetic = SubstitutionSet::new();
phonetic.allow_str("k", "ch");
let split_op = OperationType::with_restriction(1, 2, 0.15, phonetic, "split");
assert!(split_op.can_apply_to_source(b"k"));
assert!(split_op.can_apply(b"k", b"ch"));
assert!(!split_op.can_apply(b"k", b"sh"));
assert!(!split_op.can_apply_to_source(b"a"));
assert!(!split_op.can_apply(b"a", b"ch"));
}
}