This document describes the implementation of UTF-8 character-level support for the liblevenshtein-rust library. The implementation adds character-level variants alongside existing byte-level implementations without breaking backward compatibility.
The original implementation operated at the byte level:
Vec<u8> via into_bytes()u8 valueschar (mismatch!)This caused issues with multi-byte UTF-8 sequences:
File: src/dictionary/char_unit.rs
pub trait CharUnit:
Copy + Clone + Eq + PartialEq + std::hash::Hash + std::fmt::Debug + Send + Sync + 'static
{
fn from_str(s: &str) -> Vec<Self>;
fn to_string(units: &[Self]) -> String;
fn iter_str(s: &str) -> Box<dyn Iterator<Item = Self> + '_>;
}
Implementations:
impl CharUnit for u8 - Byte-level (existing behavior, fastest)impl CharUnit for char - Character-level (proper Unicode semantics)File: src/dictionary/mod.rs
pub trait DictionaryNode: Clone + Send + Sync {
type Unit: CharUnit; // ← NEW: Associated type
fn is_final(&self) -> bool;
fn transition(&self, label: Self::Unit) -> Option<Self>; // ← Changed from u8
fn edges(&self) -> Box<dyn Iterator<Item = (Self::Unit, Self)> + '_>; // ← Changed
fn has_edge(&self, label: Self::Unit) -> bool; // ← Changed
fn edge_count(&self) -> Option<usize>;
}
pub trait Dictionary {
type Node: DictionaryNode;
fn root(&self) -> Self::Node;
fn contains(&self, term: &str) -> bool {
let mut node = self.root();
for unit in <Self::Node as DictionaryNode>::Unit::iter_str(term) { // ← Generic
match node.transition(unit) {
Some(next) => node = next,
None => return false,
}
}
node.is_final()
}
// ... rest unchanged
}
All existing implementations now specify type Unit = u8:
Modified Files:
src/dictionary/double_array_trie.rs - impl DictionaryNode for DoubleArrayTrieNodesrc/dictionary/dawg.rs - impl DictionaryNode for DawgDictionaryNodesrc/dictionary/dawg_optimized.rs - impl DictionaryNode for OptimizedDawgNodeRefsrc/dictionary/dynamic_dawg.rs - impl DictionaryNode for DynamicDawgNodesrc/dictionary/suffix_automaton.rs - impl DictionaryNode for SuffixNodeHandlesrc/dictionary/compressed_suffix_automaton.rs - impl DictionaryNode for CompressedSuffixNodesrc/dictionary/pathmap.rs - impl<V> DictionaryNode for PathMapNode<V>Example:
impl DictionaryNode for DoubleArrayTrieNode {
type Unit = u8; // ← Explicit byte-level
fn is_final(&self) -> bool { /* ... */ }
fn transition(&self, label: u8) -> Option<Self> { /* ... */ }
// ... rest unchanged
}
File: src/transducer/intersection.rs
// PathNode now generic over CharUnit
pub struct PathNode<U: CharUnit> {
label: U,
depth: u16,
parent: Option<Box<PathNode<U>>>,
}
// Intersection uses node's associated Unit type
pub struct Intersection<N: DictionaryNode> {
pub label: Option<N::Unit>, // ← Generic
pub node: N,
pub state: State,
pub parent: Option<Box<PathNode<N::Unit>>>, // ← Generic
}
impl<N: DictionaryNode> Intersection<N> {
pub fn term(&self) -> String {
let mut units = Vec::new();
// Collect labels...
N::Unit::to_string(&units) // ← Use trait method
}
}
File: src/transducer/transition.rs
// Characteristic vector now generic
fn characteristic_vector<'a, U: CharUnit>(
dict_unit: U, // ← Generic
query: &[U], // ← Generic
window_size: usize,
offset: usize,
buffer: &'a mut [bool; 8],
) -> &'a [bool] {
let len = window_size.min(8);
for (i, item) in buffer.iter_mut().enumerate().take(len) {
let query_idx = offset + i;
*item = query_idx < query.len() && query[query_idx] == dict_unit;
}
&buffer[..len]
}
// State transition now generic
pub fn transition_state_pooled<U: CharUnit>(
state: &State,
pool: &mut StatePool,
dict_unit: U, // ← Generic
query: &[U], // ← Generic
max_distance: usize,
algorithm: Algorithm,
prefix_mode: bool,
) -> Option<State> {
// ... implementation uses generic U throughout
}
File: src/transducer/query.rs
pub struct QueryIterator<N: DictionaryNode, R: QueryResult = String> {
pending: VecDeque<Box<Intersection<N>>>,
query: Vec<N::Unit>, // ← Uses node's Unit type
max_distance: usize,
algorithm: Algorithm,
finished: bool,
state_pool: StatePool,
substring_mode: bool,
_result_type: PhantomData<R>,
}
impl<N: DictionaryNode, R: QueryResult> QueryIterator<N, R> {
pub fn with_substring_mode(
root: N,
query: String,
max_distance: usize,
algorithm: Algorithm,
substring_mode: bool,
) -> Self {
let query_units = N::Unit::from_str(&query); // ← Generic conversion
let initial = initial_state(query_units.len(), max_distance, algorithm);
// ...
}
}
Similar updates in:
src/transducer/ordered_query.rs - OrderedQueryIteratorsrc/transducer/value_filtered_query.rs - ValueFilteredQueryIterator, ValueSetFilteredQueryIteratorFile: src/transducer/mod.rs
The Transducer API remains unchanged - it works with any Dictionary:
impl<D: Dictionary> Transducer<D> {
pub fn query(&self, term: &str, max_distance: usize) -> QueryIterator<D::Node, String> {
QueryIterator::with_substring_mode(
self.dictionary.root(),
term.to_string(),
max_distance,
self.algorithm,
self.dictionary.is_suffix_based(),
)
}
// ... all other methods work generically
}
CharUnit Trait (src/dictionary/char_unit.rs)
from_str(), to_string(), iter_str()u8 (byte-level)char (character-level)Dictionary Traits (src/dictionary/mod.rs)
type Unit: CharUnit to DictionaryNodeSelf::UnitDictionary::contains() default implementationExisting Implementations (7 files)
type Unit = u8 to all DictionaryNode implementationsGeneric Intersection (src/transducer/intersection.rs)
PathNode<U: CharUnit>N::UnitN::Unit::to_string()Generic Transitions (src/transducer/transition.rs)
characteristic_vector<U: CharUnit>() generictransition_state<U: CharUnit>() generictransition_state_pooled<U: CharUnit>() genericGeneric Iterators (3 files)
Vec<N::Unit>Vec<N::Unit>Vec<N::Unit>N::Unit::from_str() for conversionDAWG Query Helper (src/dictionary/dawg_query.rs)
PathNode<u8>Test Fixes
Compilation
cargo checkCharacter-Level Dictionary Implementations
DoubleArrayTrieChar (char-based variant)DawgDictionaryChar (char-based variant)UTF-8 Integration Tests
Performance Benchmarks
Documentation
src/dictionary/char_unit.rs (169 lines)src/dictionary/mod.rs - Added CharUnit import, made traits genericsrc/dictionary/double_array_trie.rs - Added type Unit = u8src/dictionary/dawg.rs - Added type Unit = u8src/dictionary/dawg_optimized.rs - Added type Unit = u8src/dictionary/dynamic_dawg.rs - Added type Unit = u8src/dictionary/suffix_automaton.rs - Added type Unit = u8src/dictionary/compressed_suffix_automaton.rs - Added type Unit = u8src/dictionary/pathmap.rs - Added type Unit = u8src/dictionary/dawg_query.rs - Updated PathNode to PathNode<u8>src/transducer/intersection.rs - Made PathNode and Intersection genericsrc/transducer/transition.rs - Made all transition functions genericsrc/transducer/query.rs - Made QueryIterator generic over N::Unitsrc/transducer/ordered_query.rs - Made OrderedQueryIterator generic over N::Unitsrc/transducer/value_filtered_query.rs - Made value iterators generic over N::Unituse liblevenshtein::prelude::*;
// Byte-level dictionary (default, fastest)
let dict = DoubleArrayTrie::from_terms(vec!["test", "café"]);
let transducer = Transducer::new(dict, Algorithm::Standard);
// Works as before, but treats multi-byte UTF-8 as multiple units
let results: Vec<_> = transducer.query("test", 2).collect();
use liblevenshtein::prelude::*;
// Character-level dictionary (proper Unicode)
let dict = DoubleArrayTrieChar::from_terms(vec!["test", "café", "中文", "🎉"]);
let transducer = Transducer::new(dict, Algorithm::Standard);
// Distance measured in characters, not bytes
// "" → "¡" is distance 1 (one char), not 2 (two bytes)
let results: Vec<_> = transducer.query("café", 2).collect();
Option A (Chosen): Associated Type
trait DictionaryNode {
type Unit: CharUnit;
fn transition(&self, label: Self::Unit) -> Option<Self>;
}
Option B (Rejected): Generic Parameter
trait DictionaryNode<U: CharUnit> {
fn transition(&self, label: U) -> Option<Self>;
}
Rationale:
N::Unit vs N::UQueryIterator<N> vs QueryIterator<N, U>Chosen: Additive approach (new types alongside old)
Alternative: Modify existing types to be generic
Rationale:
TRANSPOSITION_FIX_SUMMARY.md lines 205-208tests/test_empty_query.rs, tests/debug_unicode_empty_query.rsCan 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 |