Date: 2025-11-12 Status: 📋 READY FOR IMPLEMENTATION Prerequisite Reading: English Phonetic Feasibility Analysis Related Documents:
This guide provides step-by-step instructions for implementing English phonetic correction support in liblevenshtein-rust using the generalized operation framework.
Target Audience: Rust developers implementing the phonetic matching feature
Estimated Effort:
Prerequisites:
use liblevenshtein::operation::{OperationSet, OperationSetBuilder, OperationType};
use liblevenshtein::substitution::SubstitutionSet;
use liblevenshtein::transducer::UniversalAutomaton;
// Create basic phonetic operation set
let ops = OperationSetBuilder::new()
.with_match()
// Consonant digraphs: ch→ç, sh→$, ph→f, th→+
.with_operation(OperationType::with_restriction(
2, 1, 0.15,
SubstitutionSet::from_pairs(&[
("ch", "ç"), ("sh", "$"), ("ph", "f"), ("th", "+"),
]),
"consonant_digraphs",
))
// Vowel digraphs: ea→ë, ee→ë, oa→ö
.with_operation(OperationType::with_restriction(
2, 1, 0.15,
SubstitutionSet::from_pairs(&[
("ea", "ë"), ("ee", "ë"), ("oa", "ö"),
]),
"vowel_digraphs",
))
.with_standard_ops()
.build();
// Create automaton for query
let automaton = UniversalAutomaton::new("telephone", 2, &ops);
// Check matches
assert!(automaton.accepts("tel@fön")); // Phonetic spelling
assert!(automaton.accepts("telefone")); // Common misspelling
Create the following files:
src/
transducer/
operation/
mod.rs # Operation module
phonetic.rs # NEW: Phonetic operation builders
context.rs # NEW (Phase 3): Context patterns
tests/
phonetic/
mod.rs # Test module
core_operations.rs # Phase 1 tests
extended_operations.rs # Phase 2 tests
context_operations.rs # Phase 3 tests
benches/
phonetic_matcher.rs # Performance benchmarks
docs/
research/
phonetic-corrections/
ENGLISH_PHONETIC_FEASIBILITY.md # Already created
IMPLEMENTATION_GUIDE.md # This file
Goal: Implement fully modelable rules (60-70% coverage)
Effort: 3-5 days
Files: src/transducer/operation/phonetic.rs
File: src/transducer/operation/phonetic.rs
//! Phonetic operation sets for English spelling corrections
//!
//! Based on analysis from https://zompist.com/spell.html
//! See docs/research/phonetic-corrections/ENGLISH_PHONETIC_FEASIBILITY.md
use crate::operation::{OperationSet, OperationSetBuilder, OperationType};
use crate::substitution::SubstitutionSet;
/// Creates a basic phonetic operation set for English
///
/// Coverage: ~60-70% of common phonetic transformations
/// Memory: ~1-8 MB
/// Use cases: Spell checking, fuzzy search, OCR correction
///
/// # Examples
///
/// ```
/// use liblevenshtein::operation::phonetic::phonetic_english_basic;
/// use liblevenshtein::transducer::UniversalAutomaton;
///
/// let ops = phonetic_english_basic();
/// let automaton = UniversalAutomaton::new("telephone", 2, &ops);
///
/// assert!(automaton.accepts("tel@fön"));
/// ```
pub fn phonetic_english_basic() -> OperationSet {
OperationSetBuilder::new()
.with_match()
.with_operation(consonant_digraphs())
.with_operation(vowel_digraphs())
.with_operation(vowel_trigraphs())
.with_operation(silent_e_deletion())
.with_operation(double_consonant_simplification())
.with_operation(initial_cluster_reduction())
.with_operation(y_digraphs())
.with_standard_ops()
.build()
}
Implementation:
/// Consonant digraphs: ch→ç, sh→$, ph→f, th→+, qu→kw, wr→r, wh→w
///
/// Coverage: ~25% of English words
/// Examples:
/// - "phone" → "fön" (ph→f)
/// - "church" → "çurç" (ch→ç)
/// - "write" → "rït" (wr→r)
fn consonant_digraphs() -> OperationType {
OperationType::with_restriction(
2, 1, 0.15, // 2 chars → 1 char, low cost (phonetically equivalent)
SubstitutionSet::from_pairs(&[
("ch", "ç"), // church → çurç
("sh", "$"), // ship → $ip
("ph", "f"), // phone → fön
("th", "+"), // think → +ink
("qu", "kw"), // queen → kwën
("wr", "r"), // write → rït
("wh", "w"), // white → wït
("rh", "r"), // rhyme → rïm
]),
"consonant_digraphs",
)
}
Test:
File: tests/phonetic/core_operations.rs
#[test]
fn test_consonant_digraphs() {
let ops = phonetic_english_basic();
// Test ch→ç
let automaton = UniversalAutomaton::new("church", 2, &ops);
assert!(automaton.accepts("çurç"));
// Test ph→f
let automaton = UniversalAutomaton::new("phone", 2, &ops);
assert!(automaton.accepts("fön"));
// Test multiple digraphs
let automaton = UniversalAutomaton::new("photograph", 2, &ops);
assert!(automaton.accepts("fötögräf"));
}
Implementation:
/// Vowel digraphs (2→1): ea→ë, ee→ë, ai→ä, oa→ö, etc.
///
/// Coverage: ~40% of multi-syllable words
/// Examples:
/// - "read" → "rëd" (ea→ë)
/// - "boat" → "böt" (oa→ö)
fn vowel_digraphs() -> OperationType {
OperationType::with_restriction(
2, 1, 0.15,
SubstitutionSet::from_pairs(&[
("ea", "ë"), ("ee", "ë"), // eat→ët, bee→bë
("ai", "ä"), ("ay", "ä"), // wait→wät, day→dä
("oa", "ö"), // boat→böt
("au", "ò"), ("aw", "ò"), // caught→kòt, law→lò
("ou", "ôw"), ("ow", "ôw"), // loud→lôwd, cow→kôw
("oi", "öy"), ("oy", "öy"), // oil→öyl, boy→böy
("eu", "ü"), ("ew", "ü"), // feud→füd, new→nü
("ie", "ë"), // believe→bëlëv (common case)
("oo", "u"), // food→fud
]),
"vowel_digraphs",
)
}
/// Vowel trigraphs (3→1): eau→ö
///
/// Less common but high-value patterns
fn vowel_trigraphs() -> OperationType {
OperationType::with_restriction(
3, 1, 0.2,
SubstitutionSet::from_pairs(&[
("eau", "ö"), // beauty→büty
("eou", "ü"), // feud variants
]),
"vowel_trigraphs",
)
}
Test:
#[test]
fn test_vowel_digraphs() {
let ops = phonetic_english_basic();
// Test ea→ë
let automaton = UniversalAutomaton::new("read", 2, &ops);
assert!(automaton.accepts("rëd"));
// Test multiple digraphs
let automaton = UniversalAutomaton::new("beautiful", 2, &ops);
assert!(automaton.accepts("büt@f@l"));
}
Implementation:
/// Silent final 'e' deletion
///
/// Note: Without position context, applies to all 'e'
/// Edit distance threshold filters incorrect applications
///
/// Coverage: ~30% of words
/// Examples:
/// - "rate" → "rät" (final e deleted)
/// - "take" → "täk"
fn silent_e_deletion() -> OperationType {
OperationType::with_restriction(
1, 0, 0.1, // Deletion with low cost
SubstitutionSet::from_chars(&['e']),
"silent_e",
)
}
Test:
#[test]
fn test_silent_e() {
let ops = phonetic_english_basic();
let automaton = UniversalAutomaton::new("rate", 2, &ops);
assert!(automaton.accepts("rät"));
let automaton = UniversalAutomaton::new("take", 2, &ops);
assert!(automaton.accepts("täk"));
}
Implementation:
/// Double consonant simplification: bb→b, cc→c, etc.
///
/// Coverage: ~20% of words
/// Examples:
/// - "running" → "runing" (nn→n)
/// - "committee" → "comitë" (mm→m, tt→t, ee→ë)
fn double_consonant_simplification() -> OperationType {
OperationType::with_restriction(
2, 1, 0.1,
SubstitutionSet::double_consonants(),
"geminate_simplification",
)
}
Helper for SubstitutionSet:
File: src/substitution.rs (extend existing)
impl SubstitutionSet {
/// Creates a set for all double consonants (bb→b, cc→c, etc.)
pub fn double_consonants() -> Self {
const CONSONANTS: &str = "bcdfghjklmnpqrstvwxyz";
let pairs: Vec<(&str, &str)> = CONSONANTS.chars()
.map(|c| {
let double = format!("{}{}", c, c);
let single = c.to_string();
// Leak strings to get 'static lifetime (acceptable for const data)
let double_static: &'static str = Box::leak(double.into_boxed_str());
let single_static: &'static str = Box::leak(single.into_boxed_str());
(double_static, single_static)
})
.collect();
SubstitutionSet::from_pairs(&pairs)
}
}
Test:
#[test]
fn test_double_consonants() {
let ops = phonetic_english_basic();
let automaton = UniversalAutomaton::new("running", 2, &ops);
assert!(automaton.accepts("runing"));
let automaton = UniversalAutomaton::new("committee", 2, &ops);
assert!(automaton.accepts("comitë"));
}
Implementation:
/// Initial consonant cluster reduction: kn→n, gn→n, ps→s, etc.
///
/// Note: Without position context, applies everywhere
/// Acceptable with edit distance threshold
///
/// Coverage: ~5% of words
/// Examples:
/// - "knight" → "nït" (kn→n)
/// - "psychology" → "sïkölöjë" (ps→s)
fn initial_cluster_reduction() -> OperationType {
OperationType::with_restriction(
2, 1, 0.15,
SubstitutionSet::from_pairs(&[
("kn", "n"), // knight→nït
("gn", "n"), // gnat→nât
("pn", "n"), // pneumonia→nümönië
("mn", "n"), // mnemonic→nëmönik
("pt", "t"), // pterodactyl→têrödäktil
("ps", "s"), // psychology→sïkölöjë
]),
"initial_cluster_reduction",
)
}
Implementation:
/// Y digraphs: ey→ë, ay→ä, oy→öy
///
/// Coverage: ~10% of words
/// Examples:
/// - "key" → "kë" (ey→ë)
/// - "boy" → "böy" (oy→öy)
fn y_digraphs() -> OperationType {
OperationType::with_restriction(
2, 1, 0.15,
SubstitutionSet::from_pairs(&[
("ey", "ë"), // key→kë
("ay", "ä"), // say→sä
("oy", "öy"), // boy→böy
]),
"y_digraphs",
)
}
File: src/transducer/operation/mod.rs
pub mod phonetic;
// Re-export for convenience
pub use phonetic::{
phonetic_english_basic,
// Add more as implemented
};
File: tests/phonetic/core_operations.rs
use liblevenshtein::operation::phonetic::phonetic_english_basic;
use liblevenshtein::transducer::UniversalAutomaton;
#[test]
fn test_telephone_example() {
let ops = phonetic_english_basic();
let automaton = UniversalAutomaton::new("telephone", 2, &ops);
// Phonetic spelling
assert!(automaton.accepts("tel@fön"));
// Common misspelling
assert!(automaton.accepts("telefone"));
}
#[test]
fn test_beautiful_example() {
let ops = phonetic_english_basic();
let automaton = UniversalAutomaton::new("beautiful", 3, &ops);
assert!(automaton.accepts("büt@f@l"));
}
#[test]
fn test_psychology_example() {
let ops = phonetic_english_basic();
let automaton = UniversalAutomaton::new("psychology", 3, &ops);
assert!(automaton.accepts("sïkölöjë"));
}
File: benches/phonetic_matcher.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion, BenchmarkId};
use liblevenshtein::operation::phonetic::phonetic_english_basic;
use liblevenshtein::transducer::UniversalAutomaton;
fn bench_phonetic_construction(c: &mut Criterion) {
let ops = phonetic_english_basic();
let words = vec![
("short", 5),
("telephone", 10),
("beautiful", 10),
("psychology", 15),
("extraordinary", 20),
];
for (word, len) in words {
c.bench_with_input(
BenchmarkId::new("construct", len),
&word,
|b, &word| {
b.iter(|| {
UniversalAutomaton::new(black_box(word), 2, &ops)
})
},
);
}
}
fn bench_phonetic_matching(c: &mut Criterion) {
let ops = phonetic_english_basic();
let automaton = UniversalAutomaton::new("telephone", 2, &ops);
c.bench_function("match/telephone", |b| {
b.iter(|| automaton.accepts(black_box("tel@fön")))
});
}
criterion_group!(benches, bench_phonetic_construction, bench_phonetic_matching);
criterion_main!(benches);
Run benchmarks:
RUSTFLAGS="-C target-cpu=native" taskset -c 0 cargo bench --bench phonetic_matcher
src/transducer/operation/phonetic.rsSubstitutionSet::double_consonants() helpercargo test --test phoneticcargo bench --bench phonetic_matcherGoal: Implement partially modelable rules with approximations (75-85% coverage) Effort: 2-3 weeks Prerequisite: Phase 1 complete
File: src/transducer/operation/phonetic.rs
/// Creates an extended phonetic operation set for English
///
/// Coverage: ~75-85% of phonetic transformations
/// Memory: ~8-50 MB
/// Includes: Context-encoded patterns, larger operations (d=3,4)
///
/// # Examples
///
/// ```
/// use liblevenshtein::operation::phonetic::phonetic_english_extended;
/// use liblevenshtein::transducer::UniversalAutomaton;
///
/// let ops = phonetic_english_extended();
/// let automaton = UniversalAutomaton::new("daughter", 3, &ops);
///
/// assert!(automaton.accepts("dòt@r"));
/// ```
pub fn phonetic_english_extended() -> OperationSet {
OperationSetBuilder::new()
.with_match()
// Include Phase 1 operations
.with_operation(consonant_digraphs())
.with_operation(vowel_digraphs())
.with_operation(vowel_trigraphs())
.with_operation(silent_e_deletion())
.with_operation(double_consonant_simplification())
.with_operation(initial_cluster_reduction())
.with_operation(y_digraphs())
// Phase 2 additions
.with_operation(velar_softening_contextual())
.with_operation(vowel_r_coloring())
.with_operation(vowel_double_r())
.with_operation(gh_vowel_lengthening())
.with_operation(gh_before_vowel())
.with_operation(aught_ought_patterns())
.with_operation(ough_variants())
.with_operation(silent_gh())
.with_operation(tion_sion_endings())
.with_standard_ops()
.build()
}
Implementation:
/// Context-encoded c/g softening
///
/// c→s before e/i/y (cell→sêl)
/// c→k elsewhere (cow→kôw)
/// g→j before e/i/y (gel→jêl)
///
/// Method 2 from feasibility analysis: pre-encode context
fn velar_softening_contextual() -> OperationType {
OperationType::with_restriction(
2, 2, 0.25,
SubstitutionSet::from_pairs(&[
// Soft c before front vowels
("ce", "se"), ("ci", "si"), ("cy", "sy"),
// Hard c elsewhere
("ca", "ka"), ("co", "ko"), ("cu", "ku"),
// Soft g before front vowels
("ge", "je"), ("gi", "ji"), ("gy", "jy"),
// Note: Hard g is just 'g', handled by match operation
]),
"velar_softening",
)
}
Test:
#[test]
fn test_velar_softening() {
let ops = phonetic_english_extended();
// Soft c
let automaton = UniversalAutomaton::new("ceiling", 3, &ops);
assert!(automaton.accepts("sëling"));
// Hard c
let automaton = UniversalAutomaton::new("cat", 3, &ops);
assert!(automaton.accepts("kât"));
// Soft g
let automaton = UniversalAutomaton::new("gel", 3, &ops);
assert!(automaton.accepts("jêl"));
}
Implementation:
/// Vowel + single r coloring
///
/// ar→ôr, er→@r, ir→@r, or→ör, ur→@r
fn vowel_r_coloring() -> OperationType {
OperationType::with_restriction(
2, 2, 0.3,
SubstitutionSet::from_pairs(&[
("ar", "ôr"), // car→kôr
("er", "@r"), // her→h@r
("ir", "@r"), // sir→s@r
("or", "ör"), // for→för
("ur", "@r"), // fur→f@r
]),
"vowel_r_coloring",
)
}
/// Vowel + double r simplification + coloring
///
/// arr→är, err→är, irr→är, orr→är, urr→är
fn vowel_double_r() -> OperationType {
OperationType::with_restriction(
3, 2, 0.3,
SubstitutionSet::from_pairs(&[
("arr", "är"), // carry→kärë
("err", "är"), // error→är@r
("irr", "är"), // mirror→mir@r
("orr", "är"), // sorry→särë
("urr", "är"), // hurry→härë
]),
"vowel_double_r",
)
}
Implementation:
/// GH vowel lengthening patterns
///
/// igh→ï (right→rït)
/// eigh→ä (eight→ät)
fn gh_vowel_lengthening() -> OperationType {
OperationType::with_restriction(
3, 1, 0.2,
SubstitutionSet::from_pairs(&[
("igh", "ï"), // right→rït
("eigh", "ä"), // eight→ät
]),
"gh_lengthening",
)
}
/// GH before vowels: gh→g
///
/// gha→ga, ghe→ge, etc.
fn gh_before_vowel() -> OperationType {
OperationType::with_restriction(
3, 2, 0.25,
SubstitutionSet::from_pairs(&[
("gha", "ga"), ("ghe", "ge"), ("ghi", "gi"),
("gho", "go"), ("ghu", "gu"),
]),
"gh_before_vowel",
)
}
/// aught/ought patterns
///
/// aught→òt, ought→òt
fn aught_ought_patterns() -> OperationType {
OperationType::with_restriction(
4, 2, 0.25,
SubstitutionSet::from_pairs(&[
("aught", "òt"), // daughter→dòt@r
("ought", "òt"), // bought→bòt
]),
"aught_ought",
)
}
/// ough variants
///
/// Multiple pronunciations for "ough"
fn ough_variants() -> OperationType {
OperationType::with_restriction(
4, 1, 0.25,
SubstitutionSet::from_pairs(&[
("ough", "ö"), // dough→dö
("ough", "òf"), // cough→kòf
("ough", "ô"), // through→+rô
("ough", "ùf"), // enough→enùf
]),
"ough_variants",
)
}
/// Silent gh (often final position)
fn silent_gh() -> OperationType {
OperationType::with_restriction(
2, 0, 0.15,
SubstitutionSet::from_pairs(&[("gh", "")]),
"silent_gh",
)
}
Test:
#[test]
fn test_gh_patterns() {
let ops = phonetic_english_extended();
// igh→ï
let automaton = UniversalAutomaton::new("right", 3, &ops);
assert!(automaton.accepts("rït"));
// aught→òt
let automaton = UniversalAutomaton::new("daughter", 3, &ops);
assert!(automaton.accepts("dòt@r"));
// ough variants (dough)
let automaton = UniversalAutomaton::new("dough", 3, &ops);
assert!(automaton.accepts("dö"));
}
Implementation:
/// -tion/-sion endings
///
/// tion→$@n, sion→$@n
fn tion_sion_endings() -> OperationType {
OperationType::with_restriction(
4, 2, 0.2,
SubstitutionSet::from_pairs(&[
("tion", "$@n"), // nation→nä$@n
("sion", "$@n"), // fusion→fü$@n
]),
"tion_sion",
)
}
Goal: Add bi-directional context support for lazy automaton (80-85% coverage) Effort: 2-3 weeks Prerequisite: Phase 2 complete
File: src/transducer/operation/context.rs
//! Context patterns for conditional operations
use std::fmt;
/// Pattern for matching characters in context
#[derive(Clone)]
pub enum ContextPattern {
/// Match if character satisfies predicate
Predicate(fn(char) -> bool),
/// Match if character in set
CharSet(Vec<char>),
/// Match any character
Any,
}
impl ContextPattern {
/// Create pattern that matches if predicate returns true
pub fn matches<F>(predicate: fn(char) -> bool) -> Self {
ContextPattern::Predicate(predicate)
}
/// Create pattern that matches characters in string
pub fn chars(chars: &str) -> Self {
ContextPattern::CharSet(chars.chars().collect())
}
/// Check if pattern matches character
pub fn matches_char(&self, ch: char) -> bool {
match self {
ContextPattern::Predicate(f) => f(ch),
ContextPattern::CharSet(set) => set.contains(&ch),
ContextPattern::Any => true,
}
}
}
impl fmt::Debug for ContextPattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ContextPattern::Predicate(_) => write!(f, "Predicate(<fn>)"),
ContextPattern::CharSet(chars) => write!(f, "CharSet({:?})", chars),
ContextPattern::Any => write!(f, "Any"),
}
}
}
File: src/transducer/operation.rs (modify existing)
use crate::operation::context::ContextPattern;
pub struct OperationType {
pub x_consumed: u8,
pub y_consumed: u8,
pub weight: f32,
pub restriction: Option<SubstitutionSet>,
pub name: &'static str,
// NEW: Context patterns
pub left_context: Option<ContextPattern>,
pub right_context: Option<ContextPattern>,
}
impl OperationType {
// ... existing methods
/// Add left context (character before operation)
pub fn with_left_context(mut self, pattern: ContextPattern) -> Self {
self.left_context = Some(pattern);
self
}
/// Add right context (character after operation)
pub fn with_right_context(mut self, pattern: ContextPattern) -> Self {
self.right_context = Some(pattern);
self
}
/// Check if operation applies in given context
pub fn applies_in_context(&self, word: &str, pos: usize) -> bool {
// Check left context
if let Some(ref left) = self.left_context {
if pos == 0 {
return false; // No left context available
}
let prev_char = word.chars().nth(pos - 1).unwrap();
if !left.matches_char(prev_char) {
return false;
}
}
// Check right context
if let Some(ref right) = self.right_context {
let next_pos = pos + self.x_consumed as usize;
if next_pos >= word.len() {
return false; // No right context available
}
let next_char = word.chars().nth(next_pos).unwrap();
if !right.matches_char(next_char) {
return false;
}
}
true
}
}
File: src/transducer/lazy.rs (modify transition logic)
impl State {
pub fn transition(&self, word: &str, pos: usize, ops: &OperationSet) -> State {
let mut next_state = State::new();
for position in &self.positions {
// Get applicable operations at this position
let applicable_ops = ops.operations().iter()
.filter(|op| {
// Check if operation applies in context
op.applies_in_context(word, pos)
&& op.applies_to(/* characters */)
});
// Apply operations...
}
next_state
}
}
File: src/transducer/operation/phonetic.rs
use crate::operation::context::ContextPattern;
/// Creates contextual phonetic operation set (lazy only)
///
/// Coverage: ~80-85% of phonetic transformations
/// Note: Only works with lazy automaton (context-dependent)
pub fn phonetic_english_contextual() -> OperationSet {
OperationSetBuilder::new()
.with_match()
// Include Phase 1 & 2 operations
// ... (omitted for brevity)
// Phase 3: Contextual operations
.with_operation(soft_c_contextual())
.with_operation(hard_c_default())
.with_operation(soft_g_contextual())
.with_operation(x_voicing_contextual())
.with_standard_ops()
.build()
}
/// Soft c before front vowels (e, i, y)
fn soft_c_contextual() -> OperationType {
OperationType::with_restriction(
1, 1, 0.25,
SubstitutionSet::from_pairs(&[("c", "s")]),
"soft_c",
)
.with_right_context(ContextPattern::chars("eiy"))
}
/// Hard c (elsewhere, higher cost)
fn hard_c_default() -> OperationType {
OperationType::with_restriction(
1, 1, 0.35,
SubstitutionSet::from_pairs(&[("c", "k")]),
"hard_c",
)
// No context = applies everywhere
}
/// Soft g before front vowels
fn soft_g_contextual() -> OperationType {
OperationType::with_restriction(
1, 1, 0.25,
SubstitutionSet::from_pairs(&[("g", "j")]),
"soft_g",
)
.with_right_context(ContextPattern::chars("eiy"))
}
/// x voicing: x→gz after 'e' before vowel
fn x_voicing_contextual() -> OperationType {
OperationType::with_restriction(
1, 2, 0.3,
SubstitutionSet::from_pairs(&[("x", "gz")]),
"x_voicing",
)
.with_left_context(ContextPattern::chars("e"))
.with_right_context(ContextPattern::chars("aeiou"))
}
#[test]
fn test_soft_c_context() {
let ops = phonetic_english_contextual();
// Soft c before 'e'
let automaton = LazyAutomaton::new("cell", 3, &ops);
assert!(automaton.accepts("sêl"));
// Hard c before 'a'
let automaton = LazyAutomaton::new("cat", 3, &ops);
assert!(automaton.accepts("kât"));
}
#[test]
fn test_x_voicing() {
let ops = phonetic_english_contextual();
// x→gz after 'e' before vowel
let automaton = LazyAutomaton::new("exit", 3, &ops);
assert!(automaton.accepts("egzit"));
// x→ks elsewhere
let automaton = LazyAutomaton::new("fox", 3, &ops);
assert!(automaton.accepts("fòks"));
}
Test each operation in isolation:
#[test]
fn test_consonant_digraph_ch() {
let op = consonant_digraphs();
assert_eq!(op.x_consumed, 2);
assert_eq!(op.y_consumed, 1);
assert_eq!(op.weight, 0.15);
assert!(op.restriction.as_ref().unwrap().allows("ch", "ç"));
}
Test complete words:
#[test]
fn test_common_words() {
let ops = phonetic_english_extended();
let test_cases = vec![
("telephone", "tel@fön", 2),
("beautiful", "büt@f@l", 3),
("daughter", "dòt@r", 3),
("psychology", "sïkölöjë", 3),
("knight", "nït", 2),
];
for (spelling, phonetic, threshold) in test_cases {
let automaton = UniversalAutomaton::new(spelling, threshold, &ops);
assert!(
automaton.accepts(phonetic),
"Failed: {} → {} (threshold {})",
spelling, phonetic, threshold
);
}
}
Create test corpus from CMU Pronouncing Dictionary:
File: tests/phonetic/coverage_test.rs
use std::fs::File;
use std::io::{BufRead, BufReader};
#[test]
#[ignore] // Expensive test
fn measure_coverage() {
let ops = phonetic_english_extended();
// Load CMU Pronouncing Dictionary
let file = File::open("tests/data/cmudict.txt").unwrap();
let reader = BufReader::new(file);
let mut total = 0;
let mut exact_matches = 0;
let mut close_matches = 0;
for line in reader.lines() {
let line = line.unwrap();
if line.starts_with(";;;") {
continue; // Skip comments
}
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() < 2 {
continue;
}
let spelling = parts[0].to_lowercase();
let phonetic = parts[1..].join(""); // Simplified
let automaton = UniversalAutomaton::new(&spelling, 3, &ops);
if automaton.accepts(&phonetic) {
exact_matches += 1;
} else if automaton.distance(&phonetic) <= 1.5 {
close_matches += 1;
}
total += 1;
}
let exact_pct = (exact_matches as f32 / total as f32) * 100.0;
let close_pct = ((exact_matches + close_matches) as f32 / total as f32) * 100.0;
println!("Total words: {}", total);
println!("Exact matches: {} ({:.1}%)", exact_matches, exact_pct);
println!("Close matches: {} ({:.1}%)", exact_matches + close_matches, close_pct);
// Assert minimum coverage
assert!(exact_pct >= 60.0, "Coverage too low: {:.1}%", exact_pct);
}
Run:
cargo test --test coverage_test -- --ignored --nocapture
Use proptest for fuzzing:
use proptest::prelude::*;
proptest! {
#[test]
fn test_distance_properties(
word1 in "[a-z]{3,10}",
word2 in "[a-z]{3,10}",
) {
let ops = phonetic_english_basic();
// Distance should be symmetric
let d1 = compute_distance(&word1, &word2, &ops);
let d2 = compute_distance(&word2, &word1, &ops);
assert_eq!(d1, d2);
// Distance to self should be 0
let d_self = compute_distance(&word1, &word1, &ops);
assert_eq!(d_self, 0.0);
}
}
Generate flamegraph:
RUSTFLAGS="-C target-cpu=native" taskset -c 0 cargo flamegraph \
--bench phonetic_matcher -- --bench
Monitor state space size:
#[test]
fn measure_state_space() {
let ops = phonetic_english_extended();
let automaton = UniversalAutomaton::new("telephone", 3, &ops);
println!("Number of states: {}", automaton.state_count());
println!("Memory usage: {} bytes", automaton.memory_usage());
}
use liblevenshtein::operation::phonetic::phonetic_english_extended;
use liblevenshtein::transducer::UniversalAutomaton;
pub struct PhoneticSpellChecker {
dictionary: Vec<String>,
operations: OperationSet,
}
impl PhoneticSpellChecker {
pub fn new(dictionary: Vec<String>) -> Self {
Self {
dictionary,
operations: phonetic_english_extended(),
}
}
pub fn suggest(&self, misspelling: &str, max_distance: u8) -> Vec<String> {
let automaton = UniversalAutomaton::new(misspelling, max_distance, &self.operations);
self.dictionary.iter()
.filter(|word| automaton.accepts(word))
.cloned()
.collect()
}
}
// Usage
let checker = PhoneticSpellChecker::new(load_dictionary());
let suggestions = checker.suggest("telefone", 2);
assert!(suggestions.contains(&"telephone".to_string()));
pub struct PhoneticSearch {
documents: Vec<Document>,
operations: OperationSet,
}
impl PhoneticSearch {
pub fn search(&self, query: &str, threshold: u8) -> Vec<&Document> {
let automaton = UniversalAutomaton::new(query, threshold, &self.operations);
self.documents.iter()
.filter(|doc| {
doc.words.iter().any(|word| automaton.accepts(word))
})
.collect()
}
}
Issue: Memory usage too high
Solution:
Issue: Coverage lower than expected
Solution:
Issue: Too many false positives
Solution:
Issue: Performance too slow
Solution:
This guide provides a complete implementation roadmap for English phonetic corrections in liblevenshtein-rust. Follow the three phases incrementally, testing and benchmarking at each step.
Expected Results:
Total Effort: 5-7 weeks for full implementation
Next Steps:
Document Version: 1.0 Last Updated: 2025-11-12 Author: Claude Code (Anthropic AI Assistant) Status: 📋 READY FOR IMPLEMENTATION
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |