verbora_distance/lib.rs
1//! String distance and similarity metrics for Rust.
2//!
3//! String distance and similarity — seven public metrics across four algorithms.
4//!
5//! ```
6//! use verbora_distance::{levenshtein, jaro_winkler, dice_coefficient, hamming};
7//!
8//! assert_eq!(levenshtein("kitten", "sitting", &Default::default()), 3.0);
9//! assert_eq!(dice_coefficient("abc", "abc"), 1.0);
10//! assert_eq!(hamming("karolin", "kathrin", false), 3);
11//! assert_eq!(jaro_winkler("abc", "abc", &Default::default()), 1.0);
12//! ```
13//!
14//! # Conventions differ between metrics
15//!
16//! The metrics deliberately do not share a single direction convention, and this crate does
17//! not "fix" that, since doing so would change every caller's results:
18//!
19//! | Metric | Range | Direction |
20//! |--------|-------|-----------|
21//! | [`fn@levenshtein`], [`fn@damerau_levenshtein`] | `0..` | distance — lower is closer |
22//! | [`fn@hamming`] | `-1`, `0..` | distance — lower is closer; `-1` means incomparable |
23//! | [`fn@jaro`], [`fn@jaro_winkler`] | `0..=1` | similarity — higher is closer |
24//! | [`dice_coefficient`] | `0..=1`, or `NaN` | similarity — higher is closer |
25//!
26//! The [`verbora_core::StringMetric`] implementations below record which
27//! direction each one uses, so generic code can adapt without any metric
28//! changing its output.
29//!
30//! # Unicode
31//!
32//! Every metric here indexes text by UTF-16 code unit —
33//! because that is observable in the results. See [`units`] for the mechanism
34//! and for the ASCII fast path that keeps it free on ordinary input.
35//!
36//! # Batch computation (feature = `parallel`)
37//!
38//! Every metric above is a pure, stateless free function, so scoring many
39//! independent pairs is embarrassingly parallel with zero coordination cost.
40//! With the `parallel` feature enabled, [`par_levenshtein_batch`],
41//! [`par_damerau_levenshtein_batch`], [`par_jaro_winkler_batch`],
42//! [`par_dice_coefficient_batch`] and [`par_hamming_batch`] fan a batch of
43//! pairs out across a `rayon` thread pool. Each is exactly
44//! `pairs.par_iter().map(<the sequential function>).collect()` — see the
45//! individual function docs for cost trade-offs and when a plain sequential
46//! loop is the better choice (usually: for small batches or short strings).
47
48pub mod dice;
49pub mod hamming;
50pub mod jaro_winkler;
51pub mod levenshtein;
52pub mod units;
53
54pub use dice::dice_coefficient;
55#[cfg(feature = "parallel")]
56pub use dice::par_dice_coefficient_batch;
57#[cfg(feature = "parallel")]
58pub use hamming::par_hamming_batch;
59pub use hamming::{INCOMPARABLE, hamming, hamming_checked};
60#[cfg(feature = "parallel")]
61pub use jaro_winkler::par_jaro_winkler_batch;
62pub use jaro_winkler::{jaro, jaro_winkler};
63pub use levenshtein::{
64 SearchResult, damerau_levenshtein, damerau_levenshtein_search, levenshtein, levenshtein_search,
65};
66#[cfg(feature = "parallel")]
67pub use levenshtein::{par_damerau_levenshtein_batch, par_levenshtein_batch};
68
69use verbora_core::StringMetric;
70
71/// Levenshtein distance as a [`StringMetric`].
72#[derive(Debug, Clone, Copy, Default)]
73pub struct Levenshtein(pub levenshtein::Options);
74
75impl StringMetric for Levenshtein {
76 const IS_SIMILARITY: bool = false;
77 fn measure(&self, a: &str, b: &str) -> f64 {
78 levenshtein(a, b, &self.0)
79 }
80}
81
82/// Damerau–Levenshtein distance as a [`StringMetric`].
83#[derive(Debug, Clone, Copy, Default)]
84pub struct DamerauLevenshtein(pub levenshtein::Options);
85
86impl StringMetric for DamerauLevenshtein {
87 const IS_SIMILARITY: bool = false;
88 fn measure(&self, a: &str, b: &str) -> f64 {
89 damerau_levenshtein(a, b, &self.0)
90 }
91}
92
93/// Jaro–Winkler similarity as a [`StringMetric`].
94#[derive(Debug, Clone, Copy, Default)]
95pub struct JaroWinkler(pub jaro_winkler::Options);
96
97impl StringMetric for JaroWinkler {
98 const IS_SIMILARITY: bool = true;
99 fn measure(&self, a: &str, b: &str) -> f64 {
100 jaro_winkler(a, b, &self.0)
101 }
102}
103
104/// Sørensen–Dice coefficient as a [`StringMetric`].
105#[derive(Debug, Clone, Copy, Default)]
106pub struct Dice;
107
108impl StringMetric for Dice {
109 const IS_SIMILARITY: bool = true;
110 fn measure(&self, a: &str, b: &str) -> f64 {
111 dice_coefficient(a, b)
112 }
113}
114
115/// Hamming distance as a [`StringMetric`].
116///
117/// Incomparable inputs measure as `-1.0`, matching the scalar API.
118#[derive(Debug, Clone, Copy, Default)]
119pub struct Hamming {
120 /// Fold case before comparing.
121 pub ignore_case: bool,
122}
123
124impl StringMetric for Hamming {
125 const IS_SIMILARITY: bool = false;
126 fn measure(&self, a: &str, b: &str) -> f64 {
127 hamming(a, b, self.ignore_case) as f64
128 }
129}