use libdictenstein::{Dictionary, DictionaryNode};
use crate::phonetic_rewrite_wfst::{RewriteRule, RewriteWfst};
#[derive(Clone)]
pub struct PhoneticPipelineConfig {
pub pattern: Option<String>,
pub max_distance: u8,
pub phonetic_weight: f64,
pub edit_weight: f64,
pub rewrite_rules: Vec<RewriteRule>,
pub allow_identity: bool,
}
impl Default for PhoneticPipelineConfig {
fn default() -> Self {
Self {
pattern: None,
max_distance: 2,
phonetic_weight: 0.0,
edit_weight: 1.0,
rewrite_rules: Vec::new(),
allow_identity: true,
}
}
}
pub struct PhoneticPipelineBuilder<D = ()> {
config: PhoneticPipelineConfig,
dictionary: Option<D>,
}
impl PhoneticPipelineBuilder<()> {
pub fn new() -> PhoneticPipelineBuilder<()> {
PhoneticPipelineBuilder {
config: PhoneticPipelineConfig::default(),
dictionary: None,
}
}
}
impl Default for PhoneticPipelineBuilder<()> {
fn default() -> Self {
PhoneticPipelineBuilder::new()
}
}
impl<D> PhoneticPipelineBuilder<D> {
pub fn phonetic_pattern(mut self, pattern: &str) -> Self {
self.config.pattern = Some(pattern.to_string());
self
}
pub fn max_edit_distance(mut self, distance: u8) -> Self {
self.config.max_distance = distance;
self
}
pub fn phonetic_weight(mut self, weight: f64) -> Self {
self.config.phonetic_weight = weight;
self
}
pub fn edit_weight(mut self, weight: f64) -> Self {
self.config.edit_weight = weight;
self
}
pub fn add_rewrite_rule(mut self, input: &str, output: &str, cost: f64) -> Self {
self.config
.rewrite_rules
.push(RewriteRule::with_cost(input, output, cost));
self
}
pub fn add_rewrite_rules(mut self, rules: Vec<RewriteRule>) -> Self {
self.config.rewrite_rules.extend(rules);
self
}
pub fn allow_identity(mut self, allow: bool) -> Self {
self.config.allow_identity = allow;
self
}
pub fn dictionary<D2>(self, dictionary: &D2) -> PhoneticPipelineBuilder<D2>
where
D2: Dictionary + Clone + Send + Sync + 'static,
D2::Node: Send + Sync,
<D2::Node as DictionaryNode>::Unit: Into<char> + TryFrom<char> + Copy + Send + Sync,
{
PhoneticPipelineBuilder {
config: self.config,
dictionary: Some(dictionary.clone()),
}
}
pub fn build_rewrite_wfst(&self) -> RewriteWfst {
let mut wfst = RewriteWfst::with_rules(self.config.rewrite_rules.clone());
wfst.set_allow_identity(self.config.allow_identity);
wfst
}
}
#[cfg(feature = "phonetic-rules")]
impl<D> PhoneticPipelineBuilder<D> {
pub fn build_phonetic_nfa(&self) -> Result<crate::phonetic_nfa_wfst::PhoneticNfaWfst, String> {
let pattern = self
.config
.pattern
.as_ref()
.ok_or_else(|| "No phonetic pattern specified".to_string())?;
use liblevenshtein::phonetic::nfa::compiler::compile;
use liblevenshtein::phonetic::regex::parse;
let ast = parse(pattern).map_err(|e| format!("Parse error: {:?}", e))?;
let nfa = compile(&ast).map_err(|e| format!("Compile error: {:?}", e))?;
Ok(
crate::phonetic_nfa_wfst::PhoneticNfaWfst::with_phonetic_weight(
nfa,
self.config.phonetic_weight,
),
)
}
}
#[cfg(feature = "phonetic-rules")]
impl<D> PhoneticPipelineBuilder<D>
where
D: Dictionary + Clone + Send + Sync + 'static,
D::Node: Send + Sync,
<D::Node as DictionaryNode>::Unit: Into<char> + TryFrom<char> + Copy + Send + Sync,
{
pub fn build(&self) -> Result<crate::phonetic_wfst::PhoneticWfst<D>, String> {
let dictionary = self
.dictionary
.as_ref()
.ok_or_else(|| "No dictionary specified".to_string())?;
let pattern = self
.config
.pattern
.as_ref()
.ok_or_else(|| "No phonetic pattern specified".to_string())?;
use liblevenshtein::phonetic::nfa::compiler::compile;
use liblevenshtein::phonetic::regex::parse;
let ast = parse(pattern).map_err(|e| format!("Parse error: {:?}", e))?;
let nfa = compile(&ast).map_err(|e| format!("Compile error: {:?}", e))?;
Ok(crate::phonetic_wfst::PhoneticWfst::with_phonetic_weight(
dictionary,
nfa,
self.config.max_distance,
self.config.phonetic_weight,
))
}
}
#[derive(Debug, Clone)]
pub struct PhoneticMatch {
pub term: String,
pub total_cost: f64,
pub phonetic_cost: f64,
pub edit_cost: f64,
}
impl PhoneticMatch {
pub fn new(term: String, phonetic_cost: f64, edit_cost: f64) -> Self {
Self {
term,
total_cost: phonetic_cost + edit_cost,
phonetic_cost,
edit_cost,
}
}
}
impl Eq for PhoneticMatch {}
impl PartialEq for PhoneticMatch {
fn eq(&self, other: &Self) -> bool {
self.term == other.term && self.total_cost == other.total_cost
}
}
impl PartialOrd for PhoneticMatch {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PhoneticMatch {
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(test)]
mod tests {
use super::*;
#[test]
fn test_pipeline_config_default() {
let config = PhoneticPipelineConfig::default();
assert_eq!(config.max_distance, 2);
assert_eq!(config.phonetic_weight, 0.0);
assert!(config.allow_identity);
}
#[test]
fn test_pipeline_builder_creation() {
let builder = PhoneticPipelineBuilder::new();
assert!(builder.config.pattern.is_none());
}
#[test]
fn test_pipeline_builder_pattern() {
let builder = PhoneticPipelineBuilder::new()
.phonetic_pattern("(ph|f)one")
.max_edit_distance(3)
.phonetic_weight(0.5);
assert_eq!(builder.config.pattern, Some("(ph|f)one".to_string()));
assert_eq!(builder.config.max_distance, 3);
assert_eq!(builder.config.phonetic_weight, 0.5);
}
#[test]
fn test_pipeline_builder_rules() {
let builder = PhoneticPipelineBuilder::new()
.add_rewrite_rule("ph", "f", 0.1)
.add_rewrite_rule("c", "k", 0.2);
assert_eq!(builder.config.rewrite_rules.len(), 2);
}
#[test]
fn test_pipeline_builder_rewrite_wfst() {
let builder = PhoneticPipelineBuilder::new()
.add_rewrite_rule("ph", "f", 0.1)
.allow_identity(false);
let wfst = builder.build_rewrite_wfst();
assert_eq!(wfst.num_rules(), 1);
}
#[test]
fn test_phonetic_match_ordering() {
let m1 = PhoneticMatch::new("phone".to_string(), 0.0, 0.0);
let m2 = PhoneticMatch::new("fone".to_string(), 0.1, 0.0);
let m3 = PhoneticMatch::new("tone".to_string(), 0.0, 1.0);
assert!(m1 < m2); assert!(m1 < m3); assert!(m2 < m3); }
#[test]
#[cfg(feature = "phonetic-rules")]
fn test_pipeline_builder_build_nfa() {
let builder = PhoneticPipelineBuilder::new()
.phonetic_pattern("(a|b)c")
.phonetic_weight(0.1);
let result = builder.build_phonetic_nfa();
assert!(result.is_ok());
}
#[test]
#[cfg(feature = "phonetic-rules")]
fn test_pipeline_builder_build_full() {
use libdictenstein::dynamic_dawg::char::DynamicDawgChar;
let dict = DynamicDawgChar::<()>::from_terms(vec!["phone", "fone", "help"]);
let builder = PhoneticPipelineBuilder::new()
.phonetic_pattern("(ph|f)one")
.max_edit_distance(2)
.dictionary(&dict);
let result = builder.build();
assert!(result.is_ok());
let wfst = result.expect("test fixture: build must be Ok");
assert_eq!(wfst.max_distance(), 2);
}
}