use crate::cache::multimap::FuzzyMultiMap;
use crate::phonetic::expansion::expand_phonetic_alternatives_char;
use crate::phonetic::nfa::{compile as compile_nfa, ProductAutomatonChar};
use crate::phonetic::regex::{parse as parse_regex, ParseError as RegexParseError};
use crate::phonetic::types::{PhoneChar, RewriteRuleChar};
use crate::phonetic::{apply_rules_seq_char, zompist_rules_char};
use crate::transducer::Algorithm;
use libdictenstein::dynamic_dawg::char::DynamicDawgChar;
use libdictenstein::dynamic_dawg::char_zipper::DynamicDawgCharZipper;
use libdictenstein::{
DictZipper, Dictionary, DictionaryNode, DictionaryValue, MappedDictionary,
MappedDictionaryNode, MutableMappedDictionary, SyncStrategy, ValuedDictZipper,
};
use std::collections::HashSet;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PhoneticNormalizedCandidate {
pub term: String,
pub distance: usize,
pub normalized_form: String,
}
#[derive(Debug)]
pub enum RegexQueryError {
ParseError(RegexParseError),
CompileError(RegexParseError),
}
impl fmt::Display for RegexQueryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RegexQueryError::ParseError(e) => write!(f, "Regex parse error: {}", e),
RegexQueryError::CompileError(e) => write!(f, "NFA compile error: {}", e),
}
}
}
impl std::error::Error for RegexQueryError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
RegexQueryError::ParseError(e) => Some(e),
RegexQueryError::CompileError(e) => Some(e),
}
}
}
impl From<RegexParseError> for RegexQueryError {
fn from(err: RegexParseError) -> Self {
RegexQueryError::ParseError(err)
}
}
pub struct PhoneticNormalizedDictionary<V: DictionaryValue = (), D: Dictionary = DynamicDawgChar<V>>
{
originals: D,
normalized_multimap: FuzzyMultiMap<HashSet<String>, DynamicDawgChar<HashSet<String>>>,
rules: Vec<RewriteRuleChar>,
fuel: usize,
_value: std::marker::PhantomData<V>,
}
pub type PhoneticNormalizedDictionaryChar<V = ()> =
PhoneticNormalizedDictionary<V, DynamicDawgChar<V>>;
#[derive(Clone)]
pub struct PhoneticNormalizedNode<N: DictionaryNode> {
inner: N,
}
impl<N: DictionaryNode> DictionaryNode for PhoneticNormalizedNode<N> {
type Unit = N::Unit;
fn is_final(&self) -> bool {
self.inner.is_final()
}
fn transition(&self, label: Self::Unit) -> Option<Self> {
self.inner
.transition(label)
.map(|n| PhoneticNormalizedNode { inner: n })
}
fn edges(&self) -> Box<dyn Iterator<Item = (Self::Unit, Self)> + '_> {
Box::new(
self.inner
.edges()
.map(|(label, node)| (label, PhoneticNormalizedNode { inner: node })),
)
}
fn has_edge(&self, label: Self::Unit) -> bool {
self.inner.has_edge(label)
}
fn edge_count(&self) -> Option<usize> {
self.inner.edge_count()
}
}
impl<N: MappedDictionaryNode> MappedDictionaryNode for PhoneticNormalizedNode<N> {
type Value = N::Value;
fn value(&self) -> Option<Self::Value> {
self.inner.value()
}
}
#[derive(Clone)]
pub struct PhoneticNormalizedZipper<Z: DictZipper> {
inner: Z,
}
impl<Z: DictZipper> DictZipper for PhoneticNormalizedZipper<Z> {
type Unit = Z::Unit;
fn is_final(&self) -> bool {
self.inner.is_final()
}
fn descend(&self, label: Self::Unit) -> Option<Self> {
self.inner
.descend(label)
.map(|z| PhoneticNormalizedZipper { inner: z })
}
fn children(&self) -> impl Iterator<Item = (Self::Unit, Self)> {
self.inner
.children()
.map(|(label, z)| (label, PhoneticNormalizedZipper { inner: z }))
}
fn path(&self) -> Vec<Self::Unit> {
self.inner.path()
}
}
impl<Z: ValuedDictZipper> ValuedDictZipper for PhoneticNormalizedZipper<Z> {
type Value = Z::Value;
fn value(&self) -> Option<Self::Value> {
self.inner.value()
}
}
impl<V, D> Dictionary for PhoneticNormalizedDictionary<V, D>
where
V: DictionaryValue,
D: Dictionary,
{
type Node = PhoneticNormalizedNode<D::Node>;
fn root(&self) -> Self::Node {
PhoneticNormalizedNode {
inner: self.originals.root(),
}
}
fn contains(&self, term: &str) -> bool {
self.originals.contains(term)
}
fn len(&self) -> Option<usize> {
self.originals.len()
}
fn is_empty(&self) -> bool {
self.originals.is_empty()
}
fn sync_strategy(&self) -> SyncStrategy {
SyncStrategy::InternalSync
}
}
impl<V, D> MappedDictionary for PhoneticNormalizedDictionary<V, D>
where
V: DictionaryValue,
D: MappedDictionary<Value = V>,
{
type Value = V;
fn get_value(&self, term: &str) -> Option<Self::Value> {
self.originals.get_value(term)
}
}
impl<V, D> MutableMappedDictionary for PhoneticNormalizedDictionary<V, D>
where
V: DictionaryValue,
D: MutableMappedDictionary<Value = V>,
{
fn insert_with_value(&self, term: &str, value: Self::Value) -> bool {
let is_new = self.originals.insert_with_value(term, value);
if is_new {
let normalized = self.normalize(term);
let term_string = term.to_string();
self.normalized_multimap.update_or_insert(
&normalized,
HashSet::from([term_string.clone()]),
|set| {
set.insert(term_string.clone());
},
);
}
is_new
}
fn update_or_insert<F>(&self, term: &str, default_value: Self::Value, update_fn: F) -> bool
where
F: Fn(&mut Self::Value),
{
let existed = self.originals.get_value(term).is_some();
let is_new = self
.originals
.update_or_insert(term, default_value, update_fn);
if is_new && !existed {
let normalized = self.normalize(term);
let term_string = term.to_string();
self.normalized_multimap.update_or_insert(
&normalized,
HashSet::from([term_string.clone()]),
|set| {
set.insert(term_string.clone());
},
);
}
is_new
}
fn union_with<F>(&self, other: &Self, merge_fn: F) -> usize
where
F: Fn(&Self::Value, &Self::Value) -> Self::Value,
Self::Value: Clone,
{
let count = self.originals.union_with(&other.originals, merge_fn);
for (term, _) in other.iter_terms() {
let normalized = self.normalize(&term);
self.normalized_multimap.update_or_insert(
&normalized,
HashSet::from([term.clone()]),
|set| {
set.insert(term.clone());
},
);
}
count
}
}
impl<V> PhoneticNormalizedDictionary<V, DynamicDawgChar<V>>
where
V: DictionaryValue + Default,
{
pub fn new() -> Self {
Self::with_rules(zompist_rules_char())
}
pub fn with_rules(rules: Vec<RewriteRuleChar>) -> Self {
Self::with_rules_and_algorithm(rules, Algorithm::Standard)
}
pub fn with_rules_and_algorithm(rules: Vec<RewriteRuleChar>, algorithm: Algorithm) -> Self {
let fuel = Self::compute_fuel(&rules);
let normalized_dict = DynamicDawgChar::<HashSet<String>>::new();
Self {
originals: DynamicDawgChar::new(),
normalized_multimap: FuzzyMultiMap::new(normalized_dict, algorithm),
rules,
fuel,
_value: std::marker::PhantomData,
}
}
pub fn from_terms<I, S>(terms: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
Self::from_terms_with_rules(terms, zompist_rules_char())
}
pub fn from_terms_with_rules<I, S>(terms: I, rules: Vec<RewriteRuleChar>) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
Self::from_terms_with_rules_and_algorithm(terms, rules, Algorithm::Standard)
}
pub fn from_terms_with_algorithm<I, S>(terms: I, algorithm: Algorithm) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
Self::from_terms_with_rules_and_algorithm(terms, zompist_rules_char(), algorithm)
}
pub fn from_terms_with_rules_and_algorithm<I, S>(
terms: I,
rules: Vec<RewriteRuleChar>,
algorithm: Algorithm,
) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let fuel = Self::compute_fuel(&rules);
let originals = DynamicDawgChar::new();
let normalized_dict = DynamicDawgChar::<HashSet<String>>::new();
for term in terms {
let term = term.as_ref();
originals.insert_with_value(term, V::default());
let normalized = normalize_string_char(term, &rules, fuel);
let term_string = term.to_string();
normalized_dict.update_or_insert(
&normalized,
HashSet::from([term_string.clone()]),
|set| {
set.insert(term_string.clone());
},
);
}
Self {
originals,
normalized_multimap: FuzzyMultiMap::new(normalized_dict, algorithm),
rules,
fuel,
_value: std::marker::PhantomData,
}
}
pub fn from_terms_with_values<I, S>(terms: I, rules: Vec<RewriteRuleChar>) -> Self
where
I: IntoIterator<Item = (S, V)>,
S: AsRef<str>,
{
let fuel = Self::compute_fuel(&rules);
let originals = DynamicDawgChar::new();
let normalized_dict = DynamicDawgChar::<HashSet<String>>::new();
for (term, value) in terms {
let term = term.as_ref();
originals.insert_with_value(term, value);
let normalized = normalize_string_char(term, &rules, fuel);
let term_string = term.to_string();
normalized_dict.update_or_insert(
&normalized,
HashSet::from([term_string.clone()]),
|set| {
set.insert(term_string.clone());
},
);
}
Self {
originals,
normalized_multimap: FuzzyMultiMap::new(normalized_dict, Algorithm::Standard),
rules,
fuel,
_value: std::marker::PhantomData,
}
}
}
impl<V> Default for PhoneticNormalizedDictionary<V, DynamicDawgChar<V>>
where
V: DictionaryValue + Default,
{
fn default() -> Self {
Self::new()
}
}
impl<V, D> PhoneticNormalizedDictionary<V, D>
where
V: DictionaryValue,
D: Dictionary,
{
fn compute_fuel(_rules: &[RewriteRuleChar]) -> usize {
100 }
pub fn normalize(&self, term: &str) -> String {
normalize_string_char(term, &self.rules, self.fuel)
}
pub fn rules(&self) -> &[RewriteRuleChar] {
&self.rules
}
pub fn fuel(&self) -> usize {
self.fuel
}
pub fn algorithm(&self) -> Algorithm {
self.normalized_multimap.algorithm()
}
pub fn normalized_count(&self) -> usize {
self.normalized_multimap.dictionary().len().unwrap_or(0)
}
pub fn originals(&self) -> &D {
&self.originals
}
pub fn normalized_multimap(
&self,
) -> &FuzzyMultiMap<HashSet<String>, DynamicDawgChar<HashSet<String>>> {
&self.normalized_multimap
}
}
impl<V, D> PhoneticNormalizedDictionary<V, D>
where
V: DictionaryValue + Default,
D: MutableMappedDictionary<Value = V>,
{
pub fn insert(&self, term: &str) -> bool {
self.insert_with_value(term, V::default())
}
}
impl<V, D> PhoneticNormalizedDictionary<V, D>
where
V: DictionaryValue,
D: Dictionary,
{
pub fn remove(&self, term: &str) -> bool {
let normalized = self.normalize(term);
if let Some(originals) = self.normalized_multimap.dictionary().get_value(&normalized) {
if originals.contains(term) {
let mut new_originals = originals.clone();
new_originals.remove(term);
if new_originals.is_empty() {
self.normalized_multimap.insert(&normalized, HashSet::new());
} else {
self.normalized_multimap.insert(&normalized, new_originals);
}
return true;
}
}
false
}
}
impl<V, D> PhoneticNormalizedDictionary<V, D>
where
V: DictionaryValue,
D: Dictionary,
{
pub fn iter_terms(&self) -> impl Iterator<Item = (String, String)> + '_ {
let pairs: Vec<_> = self
.normalized_multimap
.dictionary()
.iter()
.flat_map(|(normalized, originals)| {
originals
.iter()
.filter(|_| !originals.is_empty()) .map(move |term| (term.clone(), normalized.clone()))
.collect::<Vec<_>>()
})
.collect();
pairs.into_iter()
}
pub fn iter_normalized(&self) -> impl Iterator<Item = (String, HashSet<String>)> {
let pairs: Vec<_> = self
.normalized_multimap
.dictionary()
.iter()
.filter(|(_, originals)| !originals.is_empty()) .map(|(k, v)| (k.clone(), v.clone()))
.collect();
pairs.into_iter()
}
}
impl<V> PhoneticNormalizedDictionary<V, DynamicDawgChar<V>>
where
V: DictionaryValue,
{
pub fn zipper(&self) -> PhoneticNormalizedZipper<DynamicDawgCharZipper<V>> {
PhoneticNormalizedZipper {
inner: DynamicDawgCharZipper::new_from_dict(&self.originals),
}
}
pub fn query_original_regex(
&self,
pattern: &str,
max_distance: u8,
) -> Result<Vec<PhoneticNormalizedCandidate>, RegexQueryError> {
let ast = parse_regex(pattern)?;
let nfa = compile_nfa(&ast).map_err(RegexQueryError::CompileError)?;
let product = ProductAutomatonChar::with_algorithm(nfa, max_distance, self.algorithm());
let mut results = Vec::new();
for (original, _) in self.originals.iter() {
if let Some(distance) = product.min_distance(&original) {
let normalized_form = self.normalize(&original);
results.push(PhoneticNormalizedCandidate {
term: original,
distance: distance as usize,
normalized_form,
});
}
}
results.sort_by_key(|c| c.distance);
Ok(results)
}
}
impl<V, D> PhoneticNormalizedDictionary<V, D>
where
V: DictionaryValue,
D: Dictionary,
{
pub fn query(&self, query: &str, max_distance: usize) -> Vec<PhoneticNormalizedCandidate> {
let normalized_query = self.normalize(query);
if max_distance == 0 {
if let Some(originals) = self
.normalized_multimap
.dictionary()
.get_value(&normalized_query)
{
return originals
.iter()
.filter(|term| !term.is_empty()) .map(|term| PhoneticNormalizedCandidate {
term: term.clone(),
distance: 0,
normalized_form: normalized_query.clone(),
})
.collect();
}
return Vec::new();
}
let fuzzy_results = self
.normalized_multimap
.query_with_distance(&normalized_query, max_distance);
let mut results: Vec<PhoneticNormalizedCandidate> = fuzzy_results
.into_iter()
.flat_map(|(normalized_form, distance, originals)| {
originals
.into_iter()
.filter(|term| !term.is_empty()) .map(move |term| PhoneticNormalizedCandidate {
term,
distance,
normalized_form: normalized_form.clone(),
})
})
.collect();
results.sort_by_key(|c| c.distance);
results
}
pub fn query_regex(
&self,
pattern: &str,
max_distance: u8,
) -> Result<Vec<PhoneticNormalizedCandidate>, RegexQueryError> {
let ast = parse_regex(pattern)?;
let nfa = compile_nfa(&ast).map_err(RegexQueryError::CompileError)?;
let product = ProductAutomatonChar::with_algorithm(nfa, max_distance, self.algorithm());
let mut results = Vec::new();
for (normalized, originals) in self.normalized_multimap.dictionary().iter() {
if originals.is_empty() {
continue; }
if let Some(distance) = product.min_distance(&normalized) {
for term in originals.iter() {
results.push(PhoneticNormalizedCandidate {
term: term.clone(),
distance: distance as usize,
normalized_form: normalized.clone(),
});
}
}
}
results.sort_by_key(|c| c.distance);
Ok(results)
}
pub fn query_with_product(
&self,
product: &ProductAutomatonChar,
) -> Vec<PhoneticNormalizedCandidate> {
let mut results = Vec::new();
for (normalized, originals) in self.normalized_multimap.dictionary().iter() {
if originals.is_empty() {
continue; }
if let Some(distance) = product.min_distance(&normalized) {
for term in originals.iter() {
results.push(PhoneticNormalizedCandidate {
term: term.clone(),
distance: distance as usize,
normalized_form: normalized.clone(),
});
}
}
}
results.sort_by_key(|c| c.distance);
results
}
pub fn query_phonetic_pattern(
&self,
query: &str,
max_distance: u8,
) -> Result<Vec<PhoneticNormalizedCandidate>, RegexQueryError> {
let pattern = expand_phonetic_alternatives_char(query, &self.rules);
self.query_regex(&pattern, max_distance)
}
pub fn expand_to_phonetic_pattern(&self, query: &str) -> String {
let normalized = self.normalize(query);
expand_phonetic_alternatives_char(&normalized, &self.rules)
}
}
const VOWEL_MASK: u64 = (1 << (b'a' - b'a'))
| (1 << (b'e' - b'a'))
| (1 << (b'i' - b'a'))
| (1 << (b'o' - b'a'))
| (1 << (b'u' - b'a'));
#[inline(always)]
fn is_vowel(c: char) -> bool {
let lower = (c as u32) | 0x20; if lower < b'a' as u32 || lower > b'z' as u32 {
return false;
}
let bit = 1u64 << (lower - b'a' as u32);
(VOWEL_MASK & bit) != 0
}
thread_local! {
static NORMALIZE_BUFFER: std::cell::RefCell<NormalizeBuffers> =
std::cell::RefCell::new(NormalizeBuffers::new());
}
struct NormalizeBuffers {
input_phones: Vec<PhoneChar>,
output_string: String,
}
impl NormalizeBuffers {
fn new() -> Self {
Self {
input_phones: Vec::with_capacity(64),
output_string: String::with_capacity(64),
}
}
fn normalize(&mut self, input: &str, rules: &[RewriteRuleChar], fuel: usize) -> String {
if rules.is_empty() {
return input.to_string();
}
self.input_phones.clear();
self.input_phones.extend(input.chars().map(|c| {
if is_vowel(c) {
PhoneChar::Vowel(c)
} else {
PhoneChar::Consonant(c)
}
}));
let result = apply_rules_seq_char(rules, &self.input_phones, fuel);
match result {
Some(phones) => {
self.output_string.clear();
self.output_string.reserve(phones.len());
for p in phones.iter() {
match p {
PhoneChar::Vowel(c) | PhoneChar::Consonant(c) => {
self.output_string.push(*c)
}
PhoneChar::Digraph(c1, c2) => {
self.output_string.push(*c1);
self.output_string.push(*c2);
}
PhoneChar::Trigraph(c1, c2, c3) => {
self.output_string.push(*c1);
self.output_string.push(*c2);
self.output_string.push(*c3);
}
PhoneChar::Tetragraph(c1, c2, c3, c4) => {
self.output_string.push(*c1);
self.output_string.push(*c2);
self.output_string.push(*c3);
self.output_string.push(*c4);
}
PhoneChar::Pentagraph(c1, c2, c3, c4, c5) => {
self.output_string.push(*c1);
self.output_string.push(*c2);
self.output_string.push(*c3);
self.output_string.push(*c4);
self.output_string.push(*c5);
}
PhoneChar::Hexagraph(c1, c2, c3, c4, c5, c6) => {
self.output_string.push(*c1);
self.output_string.push(*c2);
self.output_string.push(*c3);
self.output_string.push(*c4);
self.output_string.push(*c5);
self.output_string.push(*c6);
}
PhoneChar::Heptagraph(c1, c2, c3, c4, c5, c6, c7) => {
self.output_string.push(*c1);
self.output_string.push(*c2);
self.output_string.push(*c3);
self.output_string.push(*c4);
self.output_string.push(*c5);
self.output_string.push(*c6);
self.output_string.push(*c7);
}
PhoneChar::Sequence(chars) => {
for c in chars {
self.output_string.push(*c);
}
}
PhoneChar::Silent => {}
}
}
self.output_string.clone()
}
None => input.to_string(),
}
}
}
#[inline]
fn normalize_string_char(input: &str, rules: &[RewriteRuleChar], fuel: usize) -> String {
NORMALIZE_BUFFER.with(|buffers| buffers.borrow_mut().normalize(input, rules, fuel))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_phonetic_normalized_basic() {
let dict = PhoneticNormalizedDictionary::<()>::from_terms([
"phone", "fone", "elephant", "elegance",
]);
let phone_normalized = dict.normalize("phone");
let fone_normalized = dict.normalize("fone");
println!("phone -> {}", phone_normalized);
println!("fone -> {}", fone_normalized);
let results = dict.query("fone", 0);
println!("Results for 'fone' with distance 0: {:?}", results);
assert!(!results.is_empty());
}
#[test]
fn test_phonetic_normalized_with_distance() {
let dict =
PhoneticNormalizedDictionary::<()>::from_terms(["phone", "bone", "cone", "tone"]);
let results = dict.query("fone", 1);
println!("Results for 'fone' with distance 1: {:?}", results);
assert!(!results.is_empty());
}
#[test]
fn test_phonetic_normalized_elephant() {
let dict = PhoneticNormalizedDictionary::<()>::from_terms(["elephant"]);
let elephant_normalized = dict.normalize("elephant");
let elefant_normalized = dict.normalize("elefant");
println!("elephant -> {}", elephant_normalized);
println!("elefant -> {}", elefant_normalized);
let results = dict.query("elefant", 1);
println!("Results for 'elefant' with distance 1: {:?}", results);
assert!(
results.iter().any(|c| c.term == "elephant"),
"Should find 'elephant' when searching for 'elefant'"
);
}
#[test]
fn test_phonetic_normalized_normalize() {
let dict = PhoneticNormalizedDictionary::<()>::from_terms(["test"]);
let normalized = dict.normalize("knight");
println!("knight -> {}", normalized);
let normalized = dict.normalize("night");
println!("night -> {}", normalized);
}
#[test]
fn test_phonetic_normalized_empty_rules() {
let dict =
PhoneticNormalizedDictionary::<()>::from_terms_with_rules(["phone", "fone"], vec![]);
assert_eq!(dict.normalize("phone"), "phone");
assert_eq!(dict.normalize("fone"), "fone");
let results = dict.query("phone", 0);
assert_eq!(results.len(), 1);
assert_eq!(results[0].term, "phone");
}
#[test]
fn test_phonetic_normalized_multiple_originals() {
let dict = PhoneticNormalizedDictionary::<()>::from_terms(["phone", "phon"]);
let phone_norm = dict.normalize("phone");
let phon_norm = dict.normalize("phon");
println!("phone -> {}", phone_norm);
println!("phon -> {}", phon_norm);
if phone_norm == phon_norm {
let results = dict.query("phone", 0);
assert!(results.len() >= 2, "Should find both 'phone' and 'phon'");
}
}
#[test]
fn test_phonetic_normalized_candidate_structure() {
let dict = PhoneticNormalizedDictionary::<()>::from_terms(["test"]);
let results = dict.query("test", 0);
assert!(!results.is_empty());
let candidate = &results[0];
assert_eq!(candidate.term, "test");
assert_eq!(candidate.distance, 0);
assert!(!candidate.normalized_form.is_empty());
}
#[test]
fn test_phonetic_normalized_algorithm() {
let dict = PhoneticNormalizedDictionary::<()>::from_terms_with_algorithm(
["phone", "fone"],
Algorithm::Transposition,
);
assert_eq!(dict.algorithm(), Algorithm::Transposition);
let results = dict.query("phone", 0);
assert!(!results.is_empty());
}
#[test]
fn test_phonetic_normalized_count() {
let dict = PhoneticNormalizedDictionary::<()>::from_terms(["phone", "fone", "bone"]);
let count = dict.normalized_count();
println!("Normalized count: {}", count);
assert!(count >= 1 && count <= 3);
}
#[test]
fn test_dictionary_trait() {
let dict = PhoneticNormalizedDictionary::<()>::from_terms(["phone", "fone", "bone"]);
assert!(dict.contains("phone"));
assert!(dict.contains("fone"));
assert!(!dict.contains("unknown"));
assert_eq!(dict.len(), Some(3));
let root = dict.root();
assert!(!root.is_final()); }
#[test]
fn test_insert() {
let dict = PhoneticNormalizedDictionary::<()>::new();
assert!(dict.insert("phone"));
assert!(dict.insert("fone"));
assert!(!dict.insert("phone"));
assert!(dict.contains("phone"));
assert!(dict.contains("fone"));
assert_eq!(dict.len(), Some(2));
}
#[test]
fn test_remove() {
let dict = PhoneticNormalizedDictionary::<()>::from_terms(["phone", "fone", "bone"]);
assert!(dict.remove("phone"));
assert!(!dict.remove("phone"));
let results = dict.query("phone", 0);
assert!(!results.iter().any(|c| c.term == "phone"));
}
#[test]
fn test_zipper_navigation() {
let dict = PhoneticNormalizedDictionary::<()>::from_terms(["cat", "car", "card"]);
let zipper = dict.zipper();
assert!(!zipper.is_final());
let c = zipper.descend('c').expect("Should have 'c' edge");
assert!(!c.is_final());
let ca = c.descend('a').expect("Should have 'a' edge");
assert!(!ca.is_final());
let cat = ca.descend('t').expect("Should have 't' edge");
assert!(cat.is_final());
assert_eq!(cat.path(), vec!['c', 'a', 't']);
}
#[test]
fn test_iter_terms() {
let dict = PhoneticNormalizedDictionary::<()>::from_terms(["phone", "fone", "bone"]);
let terms: Vec<_> = dict.iter_terms().map(|(term, _)| term).collect();
assert_eq!(terms.len(), 3);
assert!(terms.contains(&"phone".to_string()));
assert!(terms.contains(&"fone".to_string()));
assert!(terms.contains(&"bone".to_string()));
}
#[test]
fn test_regex_query_basic() {
let dict = PhoneticNormalizedDictionary::<()>::from_terms_with_rules(
["cat", "car", "card", "care", "bat"],
vec![],
);
let results = dict.query_regex("ca.", 0).expect("regex should parse");
println!("Results for 'ca.': {:?}", results);
assert!(results.iter().any(|c| c.term == "cat"));
assert!(results.iter().any(|c| c.term == "car"));
assert!(!results.iter().any(|c| c.term == "card"));
}
#[test]
fn test_regex_query_with_distance() {
let dict = PhoneticNormalizedDictionary::<()>::from_terms_with_rules(
["cat", "car", "card", "care", "bat"],
vec![],
);
let results = dict.query_regex("ca.", 1).expect("regex should parse");
println!("Results for 'ca.' with distance 1: {:?}", results);
assert!(results.iter().any(|c| c.term == "card"));
assert!(results.iter().any(|c| c.term == "care"));
}
#[test]
fn test_regex_query_alternation() {
let dict = PhoneticNormalizedDictionary::<()>::from_terms_with_rules(
["phone", "fone", "bone", "cone", "tone"],
vec![],
);
let results = dict
.query_regex("(ph|f)one", 0)
.expect("regex should parse");
println!("Results for '(ph|f)one': {:?}", results);
assert!(results.iter().any(|c| c.term == "phone"));
assert!(results.iter().any(|c| c.term == "fone"));
assert!(!results.iter().any(|c| c.term == "bone"));
}
#[test]
fn test_regex_query_optional() {
let dict = PhoneticNormalizedDictionary::<()>::from_terms_with_rules(
["color", "colour", "cola"],
vec![],
);
let results = dict.query_regex("colou?r", 0).expect("regex should parse");
println!("Results for 'colou?r': {:?}", results);
assert!(results.iter().any(|c| c.term == "color"));
assert!(results.iter().any(|c| c.term == "colour"));
assert!(!results.iter().any(|c| c.term == "cola"));
}
#[test]
fn test_regex_query_with_phonetic_normalization() {
let dict = PhoneticNormalizedDictionary::<()>::from_terms(["phone", "fone", "elephant"]);
let phone_norm = dict.normalize("phone");
let fone_norm = dict.normalize("fone");
println!("phone -> {}, fone -> {}", phone_norm, fone_norm);
assert_eq!(
phone_norm, fone_norm,
"phone and fone should normalize to same form"
);
let results = dict.query(&phone_norm, 0);
println!("Results for normalized query: {:?}", results);
assert!(
results.iter().any(|c| c.term == "phone"),
"Should find 'phone'"
);
assert!(
results.iter().any(|c| c.term == "fone"),
"Should find 'fone'"
);
}
#[test]
fn test_regex_query_error_handling() {
let dict = PhoneticNormalizedDictionary::<()>::from_terms_with_rules(["test"], vec![]);
let result = dict.query_regex("[invalid", 0);
assert!(result.is_err());
if let Err(e) = result {
println!("Expected error: {}", e);
}
}
#[test]
fn test_expand_to_phonetic_pattern() {
use crate::phonetic::types::{ContextChar, PhoneChar, RewriteRuleChar};
let rules = vec![RewriteRuleChar {
rule_id: 1,
rule_name: "ph_to_f".to_string(),
pattern: vec![PhoneChar::Consonant('p'), PhoneChar::Consonant('h')],
replacement: vec![PhoneChar::Consonant('f')],
context: ContextChar::Anywhere,
weight: 0.1,
syllable_condition: None,
}];
let dict =
PhoneticNormalizedDictionary::<()>::from_terms_with_rules(["phone", "fone"], rules);
let pattern = dict.expand_to_phonetic_pattern("fone");
println!("fone -> pattern: {}", pattern);
assert!(
pattern.contains("(ph|f)") || pattern.contains("(f|ph)"),
"Pattern should contain alternation for f/ph"
);
}
#[test]
fn test_query_phonetic_pattern_basic() {
use crate::phonetic::types::{ContextChar, PhoneChar, RewriteRuleChar};
let rules = vec![RewriteRuleChar {
rule_id: 1,
rule_name: "ph_to_f".to_string(),
pattern: vec![PhoneChar::Consonant('p'), PhoneChar::Consonant('h')],
replacement: vec![PhoneChar::Consonant('f')],
context: ContextChar::Anywhere,
weight: 0.1,
syllable_condition: None,
}];
let dict = PhoneticNormalizedDictionary::<()>::from_terms_with_rules(
["phone", "fone", "bone", "cone"],
rules,
);
let results = dict
.query_phonetic_pattern("fone", 0)
.expect("pattern should parse");
println!("Results for phonetic pattern 'fone': {:?}", results);
assert!(
results.iter().any(|c| c.term == "fone"),
"Should find 'fone'"
);
assert!(
results.iter().any(|c| c.term == "phone"),
"Should find 'phone'"
);
}
#[test]
fn test_query_phonetic_pattern_no_expansion() {
let dict = PhoneticNormalizedDictionary::<()>::from_terms_with_rules(
["cat", "car", "bat"],
vec![],
);
let pattern = dict.expand_to_phonetic_pattern("cat");
assert_eq!(pattern, "cat", "With no rules, pattern should be literal");
let results = dict
.query_phonetic_pattern("cat", 0)
.expect("pattern should parse");
assert_eq!(results.len(), 1);
assert_eq!(results[0].term, "cat");
}
#[test]
fn test_query_phonetic_pattern_with_distance() {
use crate::phonetic::types::{ContextChar, PhoneChar, RewriteRuleChar};
let rules = vec![RewriteRuleChar {
rule_id: 1,
rule_name: "ph_to_f".to_string(),
pattern: vec![PhoneChar::Consonant('p'), PhoneChar::Consonant('h')],
replacement: vec![PhoneChar::Consonant('f')],
context: ContextChar::Anywhere,
weight: 0.1,
syllable_condition: None,
}];
let dict = PhoneticNormalizedDictionary::<()>::from_terms_with_rules(
["phone", "fone", "bone", "cone"],
rules,
);
let results = dict
.query_phonetic_pattern("fone", 1)
.expect("pattern should parse");
println!(
"Results for phonetic pattern 'fone' with distance 1: {:?}",
results
);
assert!(results.iter().any(|c| c.term == "bone"));
assert!(results.iter().any(|c| c.term == "cone"));
}
#[test]
fn test_sync_strategy() {
let dict = PhoneticNormalizedDictionary::<()>::from_terms(["test"]);
assert_eq!(dict.sync_strategy(), SyncStrategy::InternalSync);
}
#[test]
fn test_query_original_regex() {
use crate::phonetic::types::{ContextChar, PhoneChar, RewriteRuleChar};
let rules = vec![RewriteRuleChar {
rule_id: 1,
rule_name: "ph_to_f".to_string(),
pattern: vec![PhoneChar::Consonant('p'), PhoneChar::Consonant('h')],
replacement: vec![PhoneChar::Consonant('f')],
context: ContextChar::Anywhere,
weight: 0.1,
syllable_condition: None,
}];
let dict = PhoneticNormalizedDictionary::<()>::from_terms_with_rules(
["phone", "fone", "bone", "cone"],
rules,
);
let pattern = dict.expand_to_phonetic_pattern("fone");
let results = dict
.query_original_regex(&pattern, 0)
.expect("pattern should parse");
assert!(
results.iter().any(|c| c.term == "phone"),
"Should find 'phone' via original regex"
);
assert!(
results.iter().any(|c| c.term == "fone"),
"Should find 'fone' via original regex"
);
assert!(
!results.iter().any(|c| c.term == "bone"),
"Should NOT find 'bone'"
);
assert!(
!results.iter().any(|c| c.term == "cone"),
"Should NOT find 'cone'"
);
}
#[test]
fn test_expand_normalizes_input() {
use crate::phonetic::types::{ContextChar, PhoneChar, RewriteRuleChar};
let rules = vec![RewriteRuleChar {
rule_id: 1,
rule_name: "ph_to_f".to_string(),
pattern: vec![PhoneChar::Consonant('p'), PhoneChar::Consonant('h')],
replacement: vec![PhoneChar::Consonant('f')],
context: ContextChar::Anywhere,
weight: 0.1,
syllable_condition: None,
}];
let dict =
PhoneticNormalizedDictionary::<()>::from_terms_with_rules(["phone", "fone"], rules);
let phone_norm = dict.normalize("phone");
let fone_norm = dict.normalize("fone");
assert_eq!(phone_norm, fone_norm, "Both should normalize to same form");
let pattern_from_phone = dict.expand_to_phonetic_pattern("phone");
let pattern_from_fone = dict.expand_to_phonetic_pattern("fone");
assert_eq!(
pattern_from_phone, pattern_from_fone,
"Expanding either query should give same pattern"
);
println!("Pattern: {}", pattern_from_fone);
}
}