#[cfg(feature = "phonetic-rules")]
use crate::phonetic::nfa::product::{ProductAutomaton, ProductAutomatonChar};
#[cfg(feature = "phonetic-rules")]
use crate::phonetic::nfa::{NFAChar, NFA};
use libdictenstein::{Dictionary, DictionaryNode};
use std::collections::VecDeque;
#[derive(Debug, Clone, PartialEq)]
pub struct PhoneticCandidate {
pub term: String,
pub edit_distance: u8,
pub phonetic_cost: f64,
pub total_cost: f64,
}
impl PhoneticCandidate {
pub fn new(term: String, edit_distance: u8, phonetic_cost: f64) -> Self {
let total_cost = edit_distance as f64 + phonetic_cost;
Self {
term,
edit_distance,
phonetic_cost,
total_cost,
}
}
}
impl Eq for PhoneticCandidate {}
impl PartialOrd for PhoneticCandidate {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PhoneticCandidate {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match self.total_cost.partial_cmp(&other.total_cost) {
Some(std::cmp::Ordering::Equal) | None => self.term.cmp(&other.term),
Some(ord) => ord,
}
}
}
#[cfg(feature = "phonetic-rules")]
#[derive(Debug, Clone)]
pub struct PhoneticTransducerChar<D: Dictionary> {
dictionary: D,
nfa: NFAChar,
max_distance: u8,
phonetic_weight: f64,
}
#[cfg(feature = "phonetic-rules")]
impl<D: Dictionary> PhoneticTransducerChar<D>
where
D::Node: DictionaryNode<Unit = char>,
{
pub fn new(dictionary: D, nfa: NFAChar, max_distance: u8) -> Self {
Self {
dictionary,
nfa,
max_distance,
phonetic_weight: 0.0,
}
}
pub fn with_phonetic_weight(
dictionary: D,
nfa: NFAChar,
max_distance: u8,
phonetic_weight: f64,
) -> Self {
Self {
dictionary,
nfa,
max_distance,
phonetic_weight,
}
}
pub fn query(&self, input: &str) -> PhoneticQueryIteratorChar<'_, D> {
PhoneticQueryIteratorChar::new(
&self.dictionary,
&self.nfa,
input,
self.max_distance,
self.phonetic_weight,
)
}
pub fn query_sorted(&self, input: &str) -> Vec<PhoneticCandidate> {
let mut results: Vec<_> = self.query(input).collect();
results.sort();
results
}
#[inline]
pub fn dictionary(&self) -> &D {
&self.dictionary
}
#[inline]
pub fn nfa(&self) -> &NFAChar {
&self.nfa
}
#[inline]
pub fn max_distance(&self) -> u8 {
self.max_distance
}
pub fn into_dictionary(self) -> D {
self.dictionary
}
}
#[cfg(feature = "phonetic-rules")]
pub struct PhoneticQueryIteratorChar<'a, D: Dictionary> {
product: ProductAutomatonChar,
queue: VecDeque<(D::Node, String, usize)>,
#[allow(dead_code)]
dictionary: &'a D,
max_depth: usize,
_phonetic_weight: f64,
}
#[cfg(feature = "phonetic-rules")]
impl<'a, D: Dictionary> PhoneticQueryIteratorChar<'a, D>
where
D::Node: DictionaryNode<Unit = char>,
{
fn new(
dictionary: &'a D,
nfa: &NFAChar,
_input: &str,
max_distance: u8,
phonetic_weight: f64,
) -> Self {
let product = ProductAutomatonChar::new(nfa.clone(), max_distance);
let mut queue = VecDeque::new();
queue.push_back((dictionary.root(), String::new(), 0));
let max_depth = 100;
Self {
product,
queue,
dictionary,
max_depth,
_phonetic_weight: phonetic_weight,
}
}
}
#[cfg(feature = "phonetic-rules")]
impl<D: Dictionary> Iterator for PhoneticQueryIteratorChar<'_, D>
where
D::Node: DictionaryNode<Unit = char>,
{
type Item = PhoneticCandidate;
fn next(&mut self) -> Option<Self::Item> {
while let Some((node, path, depth)) = self.queue.pop_front() {
if depth > self.max_depth {
continue;
}
if node.is_final() {
if let Some(distance) = self.product.min_distance(&path) {
return Some(PhoneticCandidate::new(path.clone(), distance, 0.0));
}
}
for (c, child) in node.edges() {
let mut child_path = path.clone();
child_path.push(c);
self.queue.push_back((child, child_path, depth + 1));
}
}
None
}
}
#[cfg(feature = "phonetic-rules")]
#[derive(Debug, Clone)]
pub struct PhoneticTransducer<D: Dictionary> {
dictionary: D,
nfa: NFA,
max_distance: u8,
phonetic_weight: f64,
}
#[cfg(feature = "phonetic-rules")]
impl<D: Dictionary> PhoneticTransducer<D>
where
D::Node: DictionaryNode<Unit = u8>,
{
pub fn new(dictionary: D, nfa: NFA, max_distance: u8) -> Self {
Self {
dictionary,
nfa,
max_distance,
phonetic_weight: 0.0,
}
}
pub fn with_phonetic_weight(
dictionary: D,
nfa: NFA,
max_distance: u8,
phonetic_weight: f64,
) -> Self {
Self {
dictionary,
nfa,
max_distance,
phonetic_weight,
}
}
pub fn query(&self, input: &[u8]) -> PhoneticQueryIterator<'_, D> {
PhoneticQueryIterator::new(
&self.dictionary,
&self.nfa,
input,
self.max_distance,
self.phonetic_weight,
)
}
pub fn query_sorted(&self, input: &[u8]) -> Vec<PhoneticCandidateByte> {
let mut results: Vec<_> = self.query(input).collect();
results.sort();
results
}
#[inline]
pub fn dictionary(&self) -> &D {
&self.dictionary
}
#[inline]
pub fn nfa(&self) -> &NFA {
&self.nfa
}
#[inline]
pub fn max_distance(&self) -> u8 {
self.max_distance
}
pub fn into_dictionary(self) -> D {
self.dictionary
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PhoneticCandidateByte {
pub term: Vec<u8>,
pub edit_distance: u8,
pub phonetic_cost: f64,
pub total_cost: f64,
}
impl Eq for PhoneticCandidateByte {}
impl PhoneticCandidateByte {
pub fn new(term: Vec<u8>, edit_distance: u8, phonetic_cost: f64) -> Self {
let total_cost = edit_distance as f64 + phonetic_cost;
Self {
term,
edit_distance,
phonetic_cost,
total_cost,
}
}
}
impl PartialOrd for PhoneticCandidateByte {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PhoneticCandidateByte {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match self.total_cost.partial_cmp(&other.total_cost) {
Some(std::cmp::Ordering::Equal) | None => self.term.cmp(&other.term),
Some(ord) => ord,
}
}
}
#[cfg(feature = "phonetic-rules")]
pub struct PhoneticQueryIterator<'a, D: Dictionary> {
product: ProductAutomaton,
queue: VecDeque<(D::Node, Vec<u8>, usize)>,
#[allow(dead_code)]
dictionary: &'a D,
max_depth: usize,
_phonetic_weight: f64,
}
#[cfg(feature = "phonetic-rules")]
impl<'a, D: Dictionary> PhoneticQueryIterator<'a, D>
where
D::Node: DictionaryNode<Unit = u8>,
{
fn new(
dictionary: &'a D,
nfa: &NFA,
_input: &[u8],
max_distance: u8,
phonetic_weight: f64,
) -> Self {
let product = ProductAutomaton::new(nfa.clone(), max_distance);
let mut queue = VecDeque::new();
queue.push_back((dictionary.root(), Vec::new(), 0));
let max_depth = 100;
Self {
product,
queue,
dictionary,
max_depth,
_phonetic_weight: phonetic_weight,
}
}
}
#[cfg(feature = "phonetic-rules")]
impl<D: Dictionary> Iterator for PhoneticQueryIterator<'_, D>
where
D::Node: DictionaryNode<Unit = u8>,
{
type Item = PhoneticCandidateByte;
fn next(&mut self) -> Option<Self::Item> {
while let Some((node, path, depth)) = self.queue.pop_front() {
if depth > self.max_depth {
continue;
}
if node.is_final() {
if let Some(distance) = self.product.min_distance(&path) {
return Some(PhoneticCandidateByte::new(path.clone(), distance, 0.0));
}
}
for (b, child) in node.edges() {
let mut child_path = path.clone();
child_path.push(b);
self.queue.push_back((child, child_path, depth + 1));
}
}
None
}
}
#[cfg(test)]
#[cfg(feature = "phonetic-rules")]
mod tests {
use super::*;
use crate::phonetic::nfa::compiler::compile;
use crate::phonetic::regex::parse;
use libdictenstein::double_array_trie::char::DoubleArrayTrieChar;
#[test]
fn test_phonetic_candidate_ordering() {
let c1 = PhoneticCandidate::new("apple".to_string(), 0, 0.0);
let c2 = PhoneticCandidate::new("apply".to_string(), 1, 0.0);
let c3 = PhoneticCandidate::new("banana".to_string(), 0, 0.0);
assert!(c1 < c2); assert!(c1 < c3); }
#[test]
fn test_phonetic_transducer_basic() {
let dict = DoubleArrayTrieChar::from_terms(["phone", "fone", "bone", "tone"]);
let nfa = compile(&parse("(ph|f)one").expect("parse")).expect("compile");
let transducer = PhoneticTransducerChar::new(dict, nfa, 1);
let results: Vec<_> = transducer.query("phone").collect();
let terms: Vec<_> = results.iter().map(|c| c.term.as_str()).collect();
assert!(terms.contains(&"phone") || terms.contains(&"fone"));
}
#[test]
fn test_phonetic_transducer_sorted() {
let dict = DoubleArrayTrieChar::from_terms(["test", "best", "rest", "nest"]);
let nfa = compile(&parse("test").expect("parse")).expect("compile");
let transducer = PhoneticTransducerChar::new(dict, nfa, 1);
let results = transducer.query_sorted("test");
if !results.is_empty() {
assert_eq!(results[0].term, "test");
assert_eq!(results[0].edit_distance, 0);
}
}
#[test]
fn test_phonetic_transducer_no_match() {
let dict = DoubleArrayTrieChar::from_terms(["xyz", "abc", "def"]);
let nfa = compile(&parse("phone").expect("parse")).expect("compile");
let transducer = PhoneticTransducerChar::new(dict, nfa, 1);
let results: Vec<_> = transducer.query("phone").collect();
assert!(results.is_empty());
}
#[test]
fn test_phonetic_transducer_alternation() {
let dict = DoubleArrayTrieChar::from_terms(["cat", "kat", "bat", "hat"]);
let nfa = compile(&parse("(c|k)at").expect("parse")).expect("compile");
let transducer = PhoneticTransducerChar::new(dict, nfa, 0);
let results: Vec<_> = transducer.query("cat").collect();
let terms: Vec<_> = results.iter().map(|c| c.term.as_str()).collect();
assert!(terms.contains(&"cat"));
assert!(terms.contains(&"kat"));
}
#[test]
fn test_phonetic_transducer_with_distance() {
let dict = DoubleArrayTrieChar::from_terms(["phone", "phones", "phoned"]);
let nfa = compile(&parse("phone").expect("parse")).expect("compile");
let transducer = PhoneticTransducerChar::new(dict, nfa, 1);
let results = transducer.query_sorted("phone");
let terms: Vec<_> = results.iter().map(|c| c.term.as_str()).collect();
assert!(terms.contains(&"phone"));
}
#[test]
fn test_phonetic_transducer_accessors() {
let dict = DoubleArrayTrieChar::from_terms(["test"]);
let nfa = compile(&parse("test").expect("parse")).expect("compile");
let transducer = PhoneticTransducerChar::new(dict, nfa.clone(), 2);
assert_eq!(transducer.max_distance(), 2);
assert!(!transducer.dictionary().is_empty());
let _recovered_dict = transducer.into_dictionary();
}
}