1#![deny(missing_docs)]
28
29use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque};
30use std::error::Error;
31use std::fmt;
32
33pub mod dot;
34
35pub type Symbol = u32;
39
40#[derive(Clone, Copy, Debug, Eq, PartialEq)]
42pub struct Arc {
43 pub src: usize,
45 pub dst: usize,
47 pub symbol: Symbol,
49}
50
51impl Arc {
52 pub fn new(src: usize, dst: usize, symbol: Symbol) -> Self {
54 Self { src, dst, symbol }
55 }
56}
57
58#[derive(Clone, Debug, Eq, PartialEq)]
64pub struct Lattice {
65 node_count: usize,
66 start: usize,
67 end: usize,
68 arcs: Vec<Arc>,
69 incoming: Vec<Vec<usize>>,
70 outgoing: Vec<Vec<usize>>,
71}
72
73#[derive(Clone, Debug, Eq, PartialEq)]
75pub enum LatticeError {
76 Empty,
78 MixedEmptyAndNonEmptyPaths,
80 InvalidNode {
82 node: usize,
84 node_count: usize,
86 },
87 InvalidArcOrder {
89 src: usize,
91 dst: usize,
93 },
94 InvalidEndpoint {
96 start: usize,
98 end: usize,
100 },
101}
102
103impl fmt::Display for LatticeError {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 match self {
106 Self::Empty => write!(f, "lattice must contain at least one node"),
107 Self::MixedEmptyAndNonEmptyPaths => write!(
108 f,
109 "mixed empty and non-empty paths need epsilon arcs, which moine-core does not model yet"
110 ),
111 Self::InvalidNode { node, node_count } => {
112 write!(f, "node {node} is outside 0..{node_count}")
113 }
114 Self::InvalidArcOrder { src, dst } => {
115 write!(f, "arc {src}->{dst} violates topological node order")
116 }
117 Self::InvalidEndpoint { start, end } => {
118 write!(f, "invalid endpoints start={start}, end={end}")
119 }
120 }
121 }
122}
123
124impl Error for LatticeError {}
125
126impl Lattice {
127 pub fn from_edges(
132 node_count: usize,
133 start: usize,
134 end: usize,
135 arcs: Vec<Arc>,
136 ) -> Result<Self, LatticeError> {
137 if node_count == 0 {
138 return Err(LatticeError::Empty);
139 }
140 if start >= node_count || end >= node_count || start > end {
141 return Err(LatticeError::InvalidEndpoint { start, end });
142 }
143
144 let mut incoming = vec![Vec::new(); node_count];
145 let mut outgoing = vec![Vec::new(); node_count];
146 for (idx, arc) in arcs.iter().enumerate() {
147 if arc.src >= node_count {
148 return Err(LatticeError::InvalidNode {
149 node: arc.src,
150 node_count,
151 });
152 }
153 if arc.dst >= node_count {
154 return Err(LatticeError::InvalidNode {
155 node: arc.dst,
156 node_count,
157 });
158 }
159 if arc.src >= arc.dst {
160 return Err(LatticeError::InvalidArcOrder {
161 src: arc.src,
162 dst: arc.dst,
163 });
164 }
165 incoming[arc.dst].push(idx);
166 outgoing[arc.src].push(idx);
167 }
168
169 Ok(Self {
170 node_count,
171 start,
172 end,
173 arcs,
174 incoming,
175 outgoing,
176 })
177 }
178
179 pub fn from_paths<I, S>(paths: I) -> Self
187 where
188 I: IntoIterator<Item = S>,
189 S: AsRef<str>,
190 {
191 Self::try_from_paths(paths).expect("valid string path lattice")
192 }
193
194 pub fn try_from_paths<I, S>(paths: I) -> Result<Self, LatticeError>
196 where
197 I: IntoIterator<Item = S>,
198 S: AsRef<str>,
199 {
200 let symbol_paths = paths.into_iter().map(|path| {
201 path.as_ref()
202 .chars()
203 .map(|ch| ch as Symbol)
204 .collect::<Vec<_>>()
205 });
206 Self::try_from_symbol_paths(symbol_paths)
207 }
208
209 pub fn from_symbol_paths<I, P>(paths: I) -> Self
217 where
218 I: IntoIterator<Item = P>,
219 P: IntoIterator<Item = Symbol>,
220 {
221 Self::try_from_symbol_paths(paths).expect("valid symbol path lattice")
222 }
223
224 pub fn try_from_symbol_paths<I, P>(paths: I) -> Result<Self, LatticeError>
226 where
227 I: IntoIterator<Item = P>,
228 P: IntoIterator<Item = Symbol>,
229 {
230 let paths = paths
231 .into_iter()
232 .map(|path| path.into_iter().collect::<Vec<_>>())
233 .collect::<Vec<_>>();
234 if paths.is_empty() {
235 return Err(LatticeError::Empty);
236 }
237
238 if paths.len() == 1 && paths[0].is_empty() {
239 return Self::from_edges(1, 0, 0, Vec::new());
240 }
241 if !paths.iter().all(|path| !path.is_empty()) {
242 return Err(LatticeError::MixedEmptyAndNonEmptyPaths);
243 }
244
245 let start = 0;
246 let node_count = 2 + paths
247 .iter()
248 .map(|path| path.len().saturating_sub(1))
249 .sum::<usize>();
250 let end = node_count - 1;
251 let mut next_node = 1;
252 let mut arcs = Vec::new();
253
254 for path in paths {
255 let mut current = start;
256 for (idx, symbol) in path.iter().copied().enumerate() {
257 let dst = if idx + 1 == path.len() {
258 end
259 } else {
260 let node = next_node;
261 next_node += 1;
262 node
263 };
264 arcs.push(Arc::new(current, dst, symbol));
265 current = dst;
266 }
267 }
268
269 debug_assert_eq!(next_node, end);
270 Self::from_edges(node_count, start, end, arcs)
271 }
272
273 pub fn from_symbol_paths_compact<I, P>(paths: I) -> Self
281 where
282 I: IntoIterator<Item = P>,
283 P: IntoIterator<Item = Symbol>,
284 {
285 Self::try_from_symbol_paths_compact(paths).expect("valid compact symbol path lattice")
286 }
287
288 pub fn try_from_symbol_paths_compact<I, P>(paths: I) -> Result<Self, LatticeError>
290 where
291 I: IntoIterator<Item = P>,
292 P: IntoIterator<Item = Symbol>,
293 {
294 let paths = paths
295 .into_iter()
296 .map(|path| path.into_iter().collect::<Vec<_>>())
297 .collect::<Vec<_>>();
298 if paths.is_empty() {
299 return Err(LatticeError::Empty);
300 }
301
302 if paths.len() == 1 && paths[0].is_empty() {
303 return Self::from_edges(1, 0, 0, Vec::new());
304 }
305 if !paths.iter().all(|path| !path.is_empty()) {
306 return Err(LatticeError::MixedEmptyAndNonEmptyPaths);
307 }
308
309 let mut builder = CompactPathBuilder::default();
310 for path in paths {
311 builder.insert(&path);
312 }
313 Ok(builder.into_lattice())
314 }
315
316 pub fn node_count(&self) -> usize {
318 self.node_count
319 }
320
321 pub fn start(&self) -> usize {
323 self.start
324 }
325
326 pub fn end(&self) -> usize {
328 self.end
329 }
330
331 pub fn arcs(&self) -> &[Arc] {
333 &self.arcs
334 }
335
336 pub fn try_incoming_arcs(&self, node: usize) -> Option<impl Iterator<Item = &Arc>> {
339 self.incoming
340 .get(node)
341 .map(|indices| indices.iter().map(|&idx| &self.arcs[idx]))
342 }
343
344 pub fn incoming_arcs(&self, node: usize) -> impl Iterator<Item = &Arc> {
350 self.try_incoming_arcs(node)
351 .expect("node should be inside lattice")
352 }
353
354 pub fn try_outgoing_arcs(&self, node: usize) -> Option<impl Iterator<Item = &Arc>> {
357 self.outgoing
358 .get(node)
359 .map(|indices| indices.iter().map(|&idx| &self.arcs[idx]))
360 }
361
362 pub fn outgoing_arcs(&self, node: usize) -> impl Iterator<Item = &Arc> {
368 self.try_outgoing_arcs(node)
369 .expect("node should be inside lattice")
370 }
371}
372
373#[derive(Clone, Debug, Default)]
374struct TrieNode {
375 children: BTreeMap<Symbol, usize>,
376 terminal_symbols: BTreeSet<Symbol>,
377}
378
379#[derive(Clone, Debug, Default)]
380struct CompactPathBuilder {
381 nodes: Vec<TrieNode>,
382}
383
384#[derive(Clone, Debug, Eq, Hash, PartialEq)]
385struct CompactSignature {
386 terminal_symbols: Vec<Symbol>,
387 children: Vec<(Symbol, usize)>,
388}
389
390#[derive(Clone, Debug)]
391struct CompactNode {
392 terminal_symbols: Vec<Symbol>,
393 children: Vec<(Symbol, usize)>,
394}
395
396impl CompactPathBuilder {
397 fn insert(&mut self, path: &[Symbol]) {
398 if self.nodes.is_empty() {
399 self.nodes.push(TrieNode::default());
400 }
401
402 let mut current = 0;
403 for (idx, symbol) in path.iter().copied().enumerate() {
404 if idx + 1 == path.len() {
405 self.nodes[current].terminal_symbols.insert(symbol);
406 break;
407 }
408
409 let next = if let Some(&next) = self.nodes[current].children.get(&symbol) {
410 next
411 } else {
412 let next = self.nodes.len();
413 self.nodes.push(TrieNode::default());
414 self.nodes[current].children.insert(symbol, next);
415 next
416 };
417 current = next;
418 }
419 }
420
421 fn into_lattice(self) -> Lattice {
422 let mut minimizer = CompactMinimizer {
423 trie_nodes: self.nodes,
424 memo: HashMap::new(),
425 interned: HashMap::new(),
426 compact_nodes: Vec::new(),
427 };
428 let root = minimizer.compact_trie_node(0);
429 build_compact_lattice(root, minimizer.compact_nodes)
430 }
431}
432
433struct CompactMinimizer {
434 trie_nodes: Vec<TrieNode>,
435 memo: HashMap<usize, usize>,
436 interned: HashMap<CompactSignature, usize>,
437 compact_nodes: Vec<CompactNode>,
438}
439
440impl CompactMinimizer {
441 fn compact_trie_node(&mut self, trie_id: usize) -> usize {
442 if let Some(&compact_id) = self.memo.get(&trie_id) {
443 return compact_id;
444 }
445
446 let children = self.trie_nodes[trie_id]
447 .children
448 .clone()
449 .into_iter()
450 .map(|(symbol, child)| (symbol, self.compact_trie_node(child)))
451 .collect::<Vec<_>>();
452 let terminal_symbols = self.trie_nodes[trie_id]
453 .terminal_symbols
454 .iter()
455 .copied()
456 .collect::<Vec<_>>();
457 let signature = CompactSignature {
458 terminal_symbols,
459 children,
460 };
461
462 let compact_id = if let Some(&existing) = self.interned.get(&signature) {
463 existing
464 } else {
465 let compact_id = self.compact_nodes.len();
466 self.compact_nodes.push(CompactNode {
467 terminal_symbols: signature.terminal_symbols.clone(),
468 children: signature.children.clone(),
469 });
470 self.interned.insert(signature, compact_id);
471 compact_id
472 };
473
474 self.memo.insert(trie_id, compact_id);
475 compact_id
476 }
477}
478
479fn build_compact_lattice(root: usize, compact_nodes: Vec<CompactNode>) -> Lattice {
480 let mut reachable = Vec::new();
481 collect_reachable_compact_nodes(root, &compact_nodes, &mut BTreeSet::new(), &mut reachable);
482
483 let mut heights = HashMap::new();
484 for &node in &reachable {
485 compact_height(node, &compact_nodes, &mut heights);
486 }
487
488 reachable.sort_by_key(|node| {
489 (
490 usize::from(*node != root),
491 std::cmp::Reverse(*heights.get(node).expect("height should be known")),
492 *node,
493 )
494 });
495
496 let node_ids = reachable
497 .iter()
498 .enumerate()
499 .map(|(node_id, &compact_id)| (compact_id, node_id))
500 .collect::<HashMap<_, _>>();
501 let end = reachable.len();
502 let mut arcs = Vec::new();
503
504 for &compact_id in &reachable {
505 let src = node_ids[&compact_id];
506 let node = &compact_nodes[compact_id];
507 for &symbol in &node.terminal_symbols {
508 arcs.push(Arc::new(src, end, symbol));
509 }
510 for &(symbol, child) in &node.children {
511 arcs.push(Arc::new(src, node_ids[&child], symbol));
512 }
513 }
514
515 Lattice::from_edges(end + 1, 0, end, arcs).expect("valid compact path lattice")
516}
517
518fn collect_reachable_compact_nodes(
519 node: usize,
520 compact_nodes: &[CompactNode],
521 seen: &mut BTreeSet<usize>,
522 output: &mut Vec<usize>,
523) {
524 if !seen.insert(node) {
525 return;
526 }
527 output.push(node);
528 for &(_, child) in &compact_nodes[node].children {
529 collect_reachable_compact_nodes(child, compact_nodes, seen, output);
530 }
531}
532
533fn compact_height(
534 node: usize,
535 compact_nodes: &[CompactNode],
536 memo: &mut HashMap<usize, usize>,
537) -> usize {
538 if let Some(&height) = memo.get(&node) {
539 return height;
540 }
541
542 let child_height = compact_nodes[node]
543 .children
544 .iter()
545 .map(|&(_, child)| compact_height(child, compact_nodes, memo) + 1)
546 .max()
547 .unwrap_or(0);
548 let terminal_height = usize::from(!compact_nodes[node].terminal_symbols.is_empty());
549 let height = child_height.max(terminal_height);
550 memo.insert(node, height);
551 height
552}
553
554#[derive(Clone, Copy, Debug, Eq, PartialEq)]
556pub enum EditOp {
557 Match,
559 Substitute,
561 Delete,
563 Insert,
565}
566
567#[derive(Clone, Debug, Eq, PartialEq)]
569pub struct TraceStep {
570 pub op: EditOp,
572 pub left: Option<Symbol>,
574 pub right: Option<Symbol>,
576}
577
578#[derive(Clone, Debug, Eq, PartialEq)]
580pub struct DistanceTrace {
581 pub distance: usize,
583 pub steps: Vec<TraceStep>,
585}
586
587impl DistanceTrace {
588 pub fn left_symbols(&self) -> Vec<Symbol> {
590 self.steps.iter().filter_map(|step| step.left).collect()
591 }
592
593 pub fn right_symbols(&self) -> Vec<Symbol> {
595 self.steps.iter().filter_map(|step| step.right).collect()
596 }
597}
598
599pub const MAX_DISTANCE_MATRIX_CELLS: usize = 16_000_000;
601pub const MAX_TRACE_MATRIX_CELLS: usize = 2_000_000;
603
604#[derive(Clone, Debug, Eq, PartialEq)]
606pub enum DistanceError {
607 MatrixTooLarge {
609 rows: usize,
611 cols: usize,
613 max_cells: usize,
615 },
616}
617
618impl fmt::Display for DistanceError {
619 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
620 match self {
621 Self::MatrixTooLarge {
622 rows,
623 cols,
624 max_cells,
625 } => write!(
626 f,
627 "distance matrix {rows}x{cols} exceeds the maximum of {max_cells} cells"
628 ),
629 }
630 }
631}
632
633impl Error for DistanceError {}
634
635#[derive(Clone, Debug, Eq, PartialEq)]
636struct Backpointer {
637 prev_i: usize,
638 prev_j: usize,
639 step: TraceStep,
640}
641
642const INF: usize = usize::MAX / 4;
643
644#[derive(Debug, Default)]
651pub struct DistanceWorkspace {
652 dp: Vec<usize>,
653 queued: Vec<bool>,
654 queue: VecDeque<(usize, usize)>,
655}
656
657impl DistanceWorkspace {
658 pub fn new() -> Self {
660 Self::default()
661 }
662
663 pub fn try_distance(
665 &mut self,
666 left: &Lattice,
667 right: &Lattice,
668 ) -> Result<usize, DistanceError> {
669 let rows = left.node_count();
670 let cols = right.node_count();
671 let cells = checked_distance_matrix_cells(rows, cols)?;
672 Ok(distance_impl(left, right, cells, self))
673 }
674
675 pub fn try_distance_with_cutoff(
677 &mut self,
678 left: &Lattice,
679 right: &Lattice,
680 threshold: usize,
681 ) -> Result<Option<usize>, DistanceError> {
682 let rows = left.node_count();
683 let cols = right.node_count();
684 let cells = checked_distance_matrix_cells(rows, cols)?;
685 Ok(cutoff_distance_impl(left, right, threshold, cells, self))
686 }
687
688 pub fn try_damerau_distance(
690 &mut self,
691 left: &Lattice,
692 right: &Lattice,
693 ) -> Result<usize, DistanceError> {
694 let rows = left.node_count();
695 let cols = right.node_count();
696 let cells = checked_distance_matrix_cells(rows, cols)?;
697 Ok(damerau_distance_impl(left, right, cells, self))
698 }
699
700 pub fn try_damerau_distance_with_cutoff(
703 &mut self,
704 left: &Lattice,
705 right: &Lattice,
706 threshold: usize,
707 ) -> Result<Option<usize>, DistanceError> {
708 let rows = left.node_count();
709 let cols = right.node_count();
710 let cells = checked_distance_matrix_cells(rows, cols)?;
711 Ok(cutoff_damerau_distance_impl(
712 left, right, threshold, cells, self,
713 ))
714 }
715
716 fn reset_dp(&mut self, cells: usize) {
717 self.dp.clear();
718 self.dp.resize(cells, INF);
719 }
720
721 fn reset_threshold_search(&mut self, cells: usize) {
722 self.reset_dp(cells);
723 self.queued.clear();
724 self.queued.resize(cells, false);
725 self.queue.clear();
726 }
727}
728
729#[derive(Debug, Default)]
735pub struct StringDistanceWorkspace {
736 previous2: Vec<usize>,
737 previous: Vec<usize>,
738 current: Vec<usize>,
739}
740
741impl StringDistanceWorkspace {
742 pub fn new() -> Self {
744 Self::default()
745 }
746
747 pub fn levenshtein(&mut self, left: &[char], right: &[char]) -> usize {
749 let (left, right) = trim_common_affixes(left, right);
750 levenshtein_chars_impl(left, right, self)
751 }
752
753 pub fn levenshtein_with_cutoff(
758 &mut self,
759 left: &[char],
760 right: &[char],
761 threshold: usize,
762 ) -> Option<usize> {
763 let (left, right) = trim_common_affixes(left, right);
764 levenshtein_chars_with_cutoff_impl(left, right, threshold, self)
765 }
766
767 pub fn damerau_levenshtein(&mut self, left: &[char], right: &[char]) -> usize {
770 damerau_levenshtein_chars_impl(left, right, self)
771 }
772
773 pub fn damerau_levenshtein_with_cutoff(
779 &mut self,
780 left: &[char],
781 right: &[char],
782 threshold: usize,
783 ) -> Option<usize> {
784 damerau_levenshtein_chars_with_cutoff_impl(left, right, threshold, self)
785 }
786}
787
788pub fn distance(left: &Lattice, right: &Lattice) -> usize {
795 try_distance(left, right).expect("distance matrix should fit")
796}
797
798pub fn try_distance(left: &Lattice, right: &Lattice) -> Result<usize, DistanceError> {
800 DistanceWorkspace::new().try_distance(left, right)
801}
802
803pub fn try_distance_with_cutoff(
810 left: &Lattice,
811 right: &Lattice,
812 threshold: usize,
813) -> Result<Option<usize>, DistanceError> {
814 DistanceWorkspace::new().try_distance_with_cutoff(left, right, threshold)
815}
816
817fn distance_impl(
818 left: &Lattice,
819 right: &Lattice,
820 cells: usize,
821 workspace: &mut DistanceWorkspace,
822) -> usize {
823 let cols = right.node_count();
824 workspace.reset_dp(cells);
825 let dp = workspace.dp.as_mut_slice();
826 dp[distance_index(left.start(), right.start(), cols)] = 0;
827
828 for i in left.start()..=left.end() {
829 for j in right.start()..=right.end() {
830 if i == left.start() && j == right.start() {
831 continue;
832 }
833
834 let mut best = dp[distance_index(i, j, cols)];
835
836 for left_arc in left.incoming_arcs(i) {
837 for right_arc in right.incoming_arcs(j) {
838 let cost = usize::from(left_arc.symbol != right_arc.symbol);
839 let candidate =
840 dp[distance_index(left_arc.src, right_arc.src, cols)].saturating_add(cost);
841 best = best.min(candidate);
842 }
843 }
844
845 for left_arc in left.incoming_arcs(i) {
846 let candidate = dp[distance_index(left_arc.src, j, cols)].saturating_add(1);
847 best = best.min(candidate);
848 }
849
850 for right_arc in right.incoming_arcs(j) {
851 let candidate = dp[distance_index(i, right_arc.src, cols)].saturating_add(1);
852 best = best.min(candidate);
853 }
854
855 dp[distance_index(i, j, cols)] = best;
856 }
857 }
858
859 dp[distance_index(left.end(), right.end(), cols)]
860}
861
862pub fn damerau_distance(left: &Lattice, right: &Lattice) -> usize {
869 try_damerau_distance(left, right).expect("distance matrix should fit")
870}
871
872pub fn try_damerau_distance(left: &Lattice, right: &Lattice) -> Result<usize, DistanceError> {
875 DistanceWorkspace::new().try_damerau_distance(left, right)
876}
877
878pub fn try_damerau_distance_with_cutoff(
886 left: &Lattice,
887 right: &Lattice,
888 threshold: usize,
889) -> Result<Option<usize>, DistanceError> {
890 DistanceWorkspace::new().try_damerau_distance_with_cutoff(left, right, threshold)
891}
892
893fn damerau_distance_impl(
894 left: &Lattice,
895 right: &Lattice,
896 cells: usize,
897 workspace: &mut DistanceWorkspace,
898) -> usize {
899 let cols = right.node_count();
900 workspace.reset_dp(cells);
901 let dp = workspace.dp.as_mut_slice();
902 dp[distance_index(left.start(), right.start(), cols)] = 0;
903
904 for i in left.start()..=left.end() {
905 for j in right.start()..=right.end() {
906 if i == left.start() && j == right.start() {
907 continue;
908 }
909
910 let mut best = dp[distance_index(i, j, cols)];
911
912 for left_arc in left.incoming_arcs(i) {
913 for right_arc in right.incoming_arcs(j) {
914 let cost = usize::from(left_arc.symbol != right_arc.symbol);
915 let candidate =
916 dp[distance_index(left_arc.src, right_arc.src, cols)].saturating_add(cost);
917 best = best.min(candidate);
918 }
919 }
920
921 for left_arc in left.incoming_arcs(i) {
922 let candidate = dp[distance_index(left_arc.src, j, cols)].saturating_add(1);
923 best = best.min(candidate);
924 }
925
926 for right_arc in right.incoming_arcs(j) {
927 let candidate = dp[distance_index(i, right_arc.src, cols)].saturating_add(1);
928 best = best.min(candidate);
929 }
930
931 for left_second in left.incoming_arcs(i) {
932 for right_second in right.incoming_arcs(j) {
933 for left_first in left.incoming_arcs(left_second.src) {
934 for right_first in right.incoming_arcs(right_second.src) {
935 if left_first.symbol == right_second.symbol
936 && left_second.symbol == right_first.symbol
937 {
938 let candidate = dp
939 [distance_index(left_first.src, right_first.src, cols)]
940 .saturating_add(1);
941 best = best.min(candidate);
942 }
943 }
944 }
945 }
946 }
947
948 dp[distance_index(i, j, cols)] = best;
949 }
950 }
951
952 dp[distance_index(left.end(), right.end(), cols)]
953}
954
955pub fn distance_with_trace(left: &Lattice, right: &Lattice) -> DistanceTrace {
969 try_distance_with_trace(left, right).expect("distance matrix should fit")
970}
971
972pub fn try_distance_with_trace(
975 left: &Lattice,
976 right: &Lattice,
977) -> Result<DistanceTrace, DistanceError> {
978 let rows = left.node_count();
979 let cols = right.node_count();
980 let cells = checked_trace_matrix_cells(rows, cols)?;
981 Ok(distance_with_trace_impl(left, right, cells))
982}
983
984fn distance_with_trace_impl(left: &Lattice, right: &Lattice, cells: usize) -> DistanceTrace {
985 let cols = right.node_count();
986 let mut dp = vec![INF; cells];
987 let mut back = vec![None; cells];
988 dp[distance_index(left.start(), right.start(), cols)] = 0;
989
990 for i in left.start()..=left.end() {
991 for j in right.start()..=right.end() {
992 if i == left.start() && j == right.start() {
993 continue;
994 }
995
996 let index = distance_index(i, j, cols);
997 let mut best = dp[index];
998 let mut best_back = back[index].clone();
999
1000 for left_arc in left.incoming_arcs(i) {
1001 for right_arc in right.incoming_arcs(j) {
1002 let cost = usize::from(left_arc.symbol != right_arc.symbol);
1003 let candidate =
1004 dp[distance_index(left_arc.src, right_arc.src, cols)].saturating_add(cost);
1005 if candidate < best {
1006 best = candidate;
1007 best_back = Some(Backpointer {
1008 prev_i: left_arc.src,
1009 prev_j: right_arc.src,
1010 step: TraceStep {
1011 op: if cost == 0 {
1012 EditOp::Match
1013 } else {
1014 EditOp::Substitute
1015 },
1016 left: Some(left_arc.symbol),
1017 right: Some(right_arc.symbol),
1018 },
1019 });
1020 }
1021 }
1022 }
1023
1024 for left_arc in left.incoming_arcs(i) {
1025 let candidate = dp[distance_index(left_arc.src, j, cols)].saturating_add(1);
1026 if candidate < best {
1027 best = candidate;
1028 best_back = Some(Backpointer {
1029 prev_i: left_arc.src,
1030 prev_j: j,
1031 step: TraceStep {
1032 op: EditOp::Delete,
1033 left: Some(left_arc.symbol),
1034 right: None,
1035 },
1036 });
1037 }
1038 }
1039
1040 for right_arc in right.incoming_arcs(j) {
1041 let candidate = dp[distance_index(i, right_arc.src, cols)].saturating_add(1);
1042 if candidate < best {
1043 best = candidate;
1044 best_back = Some(Backpointer {
1045 prev_i: i,
1046 prev_j: right_arc.src,
1047 step: TraceStep {
1048 op: EditOp::Insert,
1049 left: None,
1050 right: Some(right_arc.symbol),
1051 },
1052 });
1053 }
1054 }
1055
1056 dp[index] = best;
1057 back[index] = best_back;
1058 }
1059 }
1060
1061 let mut steps = Vec::new();
1062 let mut i = left.end();
1063 let mut j = right.end();
1064 while i != left.start() || j != right.start() {
1065 let Some(prev) = &back[distance_index(i, j, cols)] else {
1066 break;
1067 };
1068 steps.push(prev.step.clone());
1069 i = prev.prev_i;
1070 j = prev.prev_j;
1071 }
1072 steps.reverse();
1073
1074 DistanceTrace {
1075 distance: dp[distance_index(left.end(), right.end(), cols)],
1076 steps,
1077 }
1078}
1079
1080pub fn within_distance(left: &Lattice, right: &Lattice, threshold: usize) -> bool {
1087 try_within_distance(left, right, threshold).expect("distance matrix should fit")
1088}
1089
1090pub fn try_within_distance(
1093 left: &Lattice,
1094 right: &Lattice,
1095 threshold: usize,
1096) -> Result<bool, DistanceError> {
1097 let rows = left.node_count();
1098 let cols = right.node_count();
1099 let cells = checked_distance_matrix_cells(rows, cols)?;
1100 Ok(within_distance_impl(left, right, threshold, cells))
1101}
1102
1103fn within_distance_impl(left: &Lattice, right: &Lattice, threshold: usize, cells: usize) -> bool {
1104 let cols = right.node_count();
1105 let mut workspace = DistanceWorkspace::new();
1106 workspace.reset_threshold_search(cells);
1107 let start = distance_index(left.start(), right.start(), cols);
1108 workspace.dp[start] = 0;
1109 workspace.queued[start] = true;
1110 workspace.queue.push_back((left.start(), right.start()));
1111 let mut search = ThresholdSearch {
1112 threshold,
1113 cols,
1114 dp: &mut workspace.dp,
1115 queued: &mut workspace.queued,
1116 queue: &mut workspace.queue,
1117 };
1118
1119 while let Some((i, j)) = search.queue.pop_front() {
1120 let index = distance_index(i, j, cols);
1121 search.queued[index] = false;
1122 let current = search.dp[index];
1123 if current > threshold {
1124 continue;
1125 }
1126 if i == left.end() && j == right.end() {
1127 return true;
1128 }
1129
1130 for right_arc in right.outgoing_arcs(j) {
1131 search.relax(i, right_arc.dst, current.saturating_add(1));
1132 }
1133
1134 for left_arc in left.outgoing_arcs(i) {
1135 search.relax(left_arc.dst, j, current.saturating_add(1));
1136 }
1137
1138 for left_arc in left.outgoing_arcs(i) {
1139 for right_arc in right.outgoing_arcs(j) {
1140 let cost = usize::from(left_arc.symbol != right_arc.symbol);
1141 search.relax(left_arc.dst, right_arc.dst, current.saturating_add(cost));
1142 }
1143 }
1144 }
1145
1146 false
1147}
1148
1149fn cutoff_distance_impl(
1150 left: &Lattice,
1151 right: &Lattice,
1152 threshold: usize,
1153 cells: usize,
1154 workspace: &mut DistanceWorkspace,
1155) -> Option<usize> {
1156 let cols = right.node_count();
1157 workspace.reset_threshold_search(cells);
1158 let start = distance_index(left.start(), right.start(), cols);
1159 workspace.dp[start] = 0;
1160 workspace.queued[start] = true;
1161 workspace.queue.push_back((left.start(), right.start()));
1162 let mut search = ThresholdSearch {
1163 threshold,
1164 cols,
1165 dp: &mut workspace.dp,
1166 queued: &mut workspace.queued,
1167 queue: &mut workspace.queue,
1168 };
1169
1170 while let Some((i, j)) = search.queue.pop_front() {
1171 let index = distance_index(i, j, cols);
1172 search.queued[index] = false;
1173 let current = search.dp[index];
1174 if current > threshold {
1175 continue;
1176 }
1177
1178 for right_arc in right.outgoing_arcs(j) {
1179 search.relax(i, right_arc.dst, current.saturating_add(1));
1180 }
1181
1182 for left_arc in left.outgoing_arcs(i) {
1183 search.relax(left_arc.dst, j, current.saturating_add(1));
1184 }
1185
1186 for left_arc in left.outgoing_arcs(i) {
1187 for right_arc in right.outgoing_arcs(j) {
1188 let cost = usize::from(left_arc.symbol != right_arc.symbol);
1189 search.relax(left_arc.dst, right_arc.dst, current.saturating_add(cost));
1190 }
1191 }
1192 }
1193
1194 let distance = search.dp[distance_index(left.end(), right.end(), cols)];
1195 (distance <= threshold).then_some(distance)
1196}
1197
1198pub fn within_damerau_distance(left: &Lattice, right: &Lattice, threshold: usize) -> bool {
1207 try_within_damerau_distance(left, right, threshold).expect("distance matrix should fit")
1208}
1209
1210pub fn try_within_damerau_distance(
1213 left: &Lattice,
1214 right: &Lattice,
1215 threshold: usize,
1216) -> Result<bool, DistanceError> {
1217 let rows = left.node_count();
1218 let cols = right.node_count();
1219 let cells = checked_distance_matrix_cells(rows, cols)?;
1220 Ok(within_damerau_distance_impl(left, right, threshold, cells))
1221}
1222
1223fn within_damerau_distance_impl(
1224 left: &Lattice,
1225 right: &Lattice,
1226 threshold: usize,
1227 cells: usize,
1228) -> bool {
1229 let cols = right.node_count();
1230 let mut workspace = DistanceWorkspace::new();
1231 workspace.reset_threshold_search(cells);
1232 let start = distance_index(left.start(), right.start(), cols);
1233 workspace.dp[start] = 0;
1234 workspace.queued[start] = true;
1235 workspace.queue.push_back((left.start(), right.start()));
1236 let mut search = ThresholdSearch {
1237 threshold,
1238 cols,
1239 dp: &mut workspace.dp,
1240 queued: &mut workspace.queued,
1241 queue: &mut workspace.queue,
1242 };
1243
1244 while let Some((i, j)) = search.queue.pop_front() {
1245 let index = distance_index(i, j, cols);
1246 search.queued[index] = false;
1247 let current = search.dp[index];
1248 if current > threshold {
1249 continue;
1250 }
1251 if i == left.end() && j == right.end() {
1252 return true;
1253 }
1254
1255 for right_arc in right.outgoing_arcs(j) {
1256 search.relax(i, right_arc.dst, current.saturating_add(1));
1257 }
1258
1259 for left_arc in left.outgoing_arcs(i) {
1260 search.relax(left_arc.dst, j, current.saturating_add(1));
1261 }
1262
1263 for left_arc in left.outgoing_arcs(i) {
1264 for right_arc in right.outgoing_arcs(j) {
1265 let cost = usize::from(left_arc.symbol != right_arc.symbol);
1266 search.relax(left_arc.dst, right_arc.dst, current.saturating_add(cost));
1267 }
1268 }
1269
1270 for left_first in left.outgoing_arcs(i) {
1271 for right_first in right.outgoing_arcs(j) {
1272 for left_second in left.outgoing_arcs(left_first.dst) {
1273 for right_second in right.outgoing_arcs(right_first.dst) {
1274 if left_first.symbol == right_second.symbol
1275 && left_second.symbol == right_first.symbol
1276 {
1277 search.relax(
1278 left_second.dst,
1279 right_second.dst,
1280 current.saturating_add(1),
1281 );
1282 }
1283 }
1284 }
1285 }
1286 }
1287 }
1288
1289 false
1290}
1291
1292fn cutoff_damerau_distance_impl(
1293 left: &Lattice,
1294 right: &Lattice,
1295 threshold: usize,
1296 cells: usize,
1297 workspace: &mut DistanceWorkspace,
1298) -> Option<usize> {
1299 let cols = right.node_count();
1300 workspace.reset_threshold_search(cells);
1301 let start = distance_index(left.start(), right.start(), cols);
1302 workspace.dp[start] = 0;
1303 workspace.queued[start] = true;
1304 workspace.queue.push_back((left.start(), right.start()));
1305 let mut search = ThresholdSearch {
1306 threshold,
1307 cols,
1308 dp: &mut workspace.dp,
1309 queued: &mut workspace.queued,
1310 queue: &mut workspace.queue,
1311 };
1312
1313 while let Some((i, j)) = search.queue.pop_front() {
1314 let index = distance_index(i, j, cols);
1315 search.queued[index] = false;
1316 let current = search.dp[index];
1317 if current > threshold {
1318 continue;
1319 }
1320
1321 for right_arc in right.outgoing_arcs(j) {
1322 search.relax(i, right_arc.dst, current.saturating_add(1));
1323 }
1324
1325 for left_arc in left.outgoing_arcs(i) {
1326 search.relax(left_arc.dst, j, current.saturating_add(1));
1327 }
1328
1329 for left_arc in left.outgoing_arcs(i) {
1330 for right_arc in right.outgoing_arcs(j) {
1331 let cost = usize::from(left_arc.symbol != right_arc.symbol);
1332 search.relax(left_arc.dst, right_arc.dst, current.saturating_add(cost));
1333 }
1334 }
1335
1336 for left_first in left.outgoing_arcs(i) {
1337 for right_first in right.outgoing_arcs(j) {
1338 for left_second in left.outgoing_arcs(left_first.dst) {
1339 for right_second in right.outgoing_arcs(right_first.dst) {
1340 if left_first.symbol == right_second.symbol
1341 && left_second.symbol == right_first.symbol
1342 {
1343 search.relax(
1344 left_second.dst,
1345 right_second.dst,
1346 current.saturating_add(1),
1347 );
1348 }
1349 }
1350 }
1351 }
1352 }
1353 }
1354
1355 let distance = search.dp[distance_index(left.end(), right.end(), cols)];
1356 (distance <= threshold).then_some(distance)
1357}
1358
1359fn checked_distance_matrix_cells(rows: usize, cols: usize) -> Result<usize, DistanceError> {
1360 checked_matrix_cells(rows, cols, MAX_DISTANCE_MATRIX_CELLS)
1361}
1362
1363fn checked_trace_matrix_cells(rows: usize, cols: usize) -> Result<usize, DistanceError> {
1364 checked_matrix_cells(rows, cols, MAX_TRACE_MATRIX_CELLS)
1365}
1366
1367fn checked_matrix_cells(
1368 rows: usize,
1369 cols: usize,
1370 max_cells: usize,
1371) -> Result<usize, DistanceError> {
1372 let cells = rows
1373 .checked_mul(cols)
1374 .ok_or(DistanceError::MatrixTooLarge {
1375 rows,
1376 cols,
1377 max_cells,
1378 })?;
1379 if cells > max_cells {
1380 return Err(DistanceError::MatrixTooLarge {
1381 rows,
1382 cols,
1383 max_cells,
1384 });
1385 }
1386 Ok(cells)
1387}
1388
1389fn distance_index(i: usize, j: usize, cols: usize) -> usize {
1390 i * cols + j
1391}
1392
1393struct ThresholdSearch<'a> {
1394 threshold: usize,
1395 cols: usize,
1396 dp: &'a mut [usize],
1397 queued: &'a mut [bool],
1398 queue: &'a mut VecDeque<(usize, usize)>,
1399}
1400
1401impl ThresholdSearch<'_> {
1402 fn relax(&mut self, i: usize, j: usize, candidate: usize) {
1403 if candidate > self.threshold {
1404 return;
1405 }
1406 let index = distance_index(i, j, self.cols);
1407 if candidate >= self.dp[index] {
1408 return;
1409 }
1410 self.dp[index] = candidate;
1411 if !self.queued[index] {
1412 self.queued[index] = true;
1413 self.queue.push_back((i, j));
1414 }
1415 }
1416}
1417
1418pub fn normalized_similarity_from_distance(
1422 distance: usize,
1423 left_len: usize,
1424 right_len: usize,
1425) -> f64 {
1426 let max_len = left_len.max(right_len);
1427 if max_len == 0 {
1428 return 1.0;
1429 }
1430
1431 (1.0 - distance as f64 / max_len as f64).clamp(0.0, 1.0)
1432}
1433
1434pub fn normalized_similarity_str(left: &str, right: &str) -> f64 {
1436 let left = left.chars().collect::<Vec<_>>();
1437 let right = right.chars().collect::<Vec<_>>();
1438 let mut workspace = StringDistanceWorkspace::new();
1439 normalized_similarity_chars(&left, &right, &mut workspace)
1440}
1441
1442pub fn levenshtein_str(left: &str, right: &str) -> usize {
1444 let left = left.chars().collect::<Vec<_>>();
1445 let right = right.chars().collect::<Vec<_>>();
1446 StringDistanceWorkspace::new().levenshtein(&left, &right)
1447}
1448
1449pub fn levenshtein_str_with_cutoff(left: &str, right: &str, threshold: usize) -> Option<usize> {
1454 let left = left.chars().collect::<Vec<_>>();
1455 let right = right.chars().collect::<Vec<_>>();
1456 StringDistanceWorkspace::new().levenshtein_with_cutoff(&left, &right, threshold)
1457}
1458
1459pub fn damerau_levenshtein_str(left: &str, right: &str) -> usize {
1461 try_damerau_levenshtein_str(left, right).expect("plain string distance should fit")
1462}
1463
1464pub fn try_damerau_levenshtein_str(left: &str, right: &str) -> Result<usize, DistanceError> {
1467 let left = left.chars().collect::<Vec<_>>();
1468 let right = right.chars().collect::<Vec<_>>();
1469 Ok(StringDistanceWorkspace::new().damerau_levenshtein(&left, &right))
1470}
1471
1472pub fn try_damerau_levenshtein_str_with_cutoff(
1478 left: &str,
1479 right: &str,
1480 threshold: usize,
1481) -> Result<Option<usize>, DistanceError> {
1482 let left = left.chars().collect::<Vec<_>>();
1483 let right = right.chars().collect::<Vec<_>>();
1484 Ok(StringDistanceWorkspace::new().damerau_levenshtein_with_cutoff(&left, &right, threshold))
1485}
1486
1487fn normalized_similarity_chars(
1488 left: &[char],
1489 right: &[char],
1490 workspace: &mut StringDistanceWorkspace,
1491) -> f64 {
1492 normalized_similarity_from_distance(workspace.levenshtein(left, right), left.len(), right.len())
1493}
1494
1495fn levenshtein_chars_impl(
1496 left: &[char],
1497 right: &[char],
1498 workspace: &mut StringDistanceWorkspace,
1499) -> usize {
1500 let (shorter, longer) = if left.len() <= right.len() {
1501 (left, right)
1502 } else {
1503 (right, left)
1504 };
1505 workspace.previous.clear();
1506 workspace.previous.extend(0..=shorter.len());
1507 workspace.current.clear();
1508 workspace.current.resize(shorter.len() + 1, 0);
1509
1510 for (i, &longer_ch) in longer.iter().enumerate() {
1511 workspace.current[0] = i + 1;
1512 for (j, &shorter_ch) in shorter.iter().enumerate() {
1513 let substitution_cost = usize::from(longer_ch != shorter_ch);
1514 workspace.current[j + 1] = (workspace.previous[j + 1] + 1)
1515 .min(workspace.current[j] + 1)
1516 .min(workspace.previous[j] + substitution_cost);
1517 }
1518 std::mem::swap(&mut workspace.previous, &mut workspace.current);
1519 }
1520
1521 workspace.previous[shorter.len()]
1522}
1523
1524fn levenshtein_chars_with_cutoff_impl(
1525 left: &[char],
1526 right: &[char],
1527 threshold: usize,
1528 workspace: &mut StringDistanceWorkspace,
1529) -> Option<usize> {
1530 if left.len().abs_diff(right.len()) > threshold {
1531 return None;
1532 }
1533
1534 let (shorter, longer) = if left.len() <= right.len() {
1535 (left, right)
1536 } else {
1537 (right, left)
1538 };
1539 workspace.previous.clear();
1540 workspace.previous.extend(0..=shorter.len());
1541 workspace.current.clear();
1542 workspace.current.resize(shorter.len() + 1, 0);
1543
1544 for (i, &longer_ch) in longer.iter().enumerate() {
1545 workspace.current[0] = i + 1;
1546 let mut row_min = workspace.current[0];
1547 for (j, &shorter_ch) in shorter.iter().enumerate() {
1548 let substitution_cost = usize::from(longer_ch != shorter_ch);
1549 let distance = (workspace.previous[j + 1] + 1)
1550 .min(workspace.current[j] + 1)
1551 .min(workspace.previous[j] + substitution_cost);
1552 workspace.current[j + 1] = distance;
1553 row_min = row_min.min(distance);
1554 }
1555 if row_min > threshold {
1556 return None;
1557 }
1558 std::mem::swap(&mut workspace.previous, &mut workspace.current);
1559 }
1560
1561 let distance = workspace.previous[shorter.len()];
1562 (distance <= threshold).then_some(distance)
1563}
1564
1565fn damerau_levenshtein_chars_impl(
1566 left: &[char],
1567 right: &[char],
1568 workspace: &mut StringDistanceWorkspace,
1569) -> usize {
1570 if left.is_empty() {
1571 return right.len();
1572 }
1573 if right.is_empty() {
1574 return left.len();
1575 }
1576
1577 workspace.previous2.clear();
1578 workspace.previous2.resize(right.len() + 1, 0);
1579 workspace.previous.clear();
1580 workspace.previous.extend(0..=right.len());
1581 workspace.current.clear();
1582 workspace.current.resize(right.len() + 1, 0);
1583
1584 for i in 1..=left.len() {
1585 workspace.current[0] = i;
1586 for j in 1..=right.len() {
1587 let substitution_cost = usize::from(left[i - 1] != right[j - 1]);
1588 let mut best = (workspace.previous[j] + 1)
1589 .min(workspace.current[j - 1] + 1)
1590 .min(workspace.previous[j - 1] + substitution_cost);
1591
1592 if i > 1 && j > 1 && left[i - 1] == right[j - 2] && left[i - 2] == right[j - 1] {
1593 best = best.min(workspace.previous2[j - 2] + 1);
1594 }
1595
1596 workspace.current[j] = best;
1597 }
1598 std::mem::swap(&mut workspace.previous2, &mut workspace.previous);
1599 std::mem::swap(&mut workspace.previous, &mut workspace.current);
1600 }
1601
1602 workspace.previous[right.len()]
1603}
1604
1605fn damerau_levenshtein_chars_with_cutoff_impl(
1606 left: &[char],
1607 right: &[char],
1608 threshold: usize,
1609 workspace: &mut StringDistanceWorkspace,
1610) -> Option<usize> {
1611 if left.len().abs_diff(right.len()) > threshold {
1612 return None;
1613 }
1614
1615 if left.is_empty() {
1616 return (right.len() <= threshold).then_some(right.len());
1617 }
1618 if right.is_empty() {
1619 return (left.len() <= threshold).then_some(left.len());
1620 }
1621
1622 workspace.previous2.clear();
1623 workspace.previous2.resize(right.len() + 1, 0);
1624 workspace.previous.clear();
1625 workspace.previous.extend(0..=right.len());
1626 workspace.current.clear();
1627 workspace.current.resize(right.len() + 1, 0);
1628
1629 for i in 1..=left.len() {
1630 workspace.current[0] = i;
1631 let mut row_min = workspace.current[0];
1632 for j in 1..=right.len() {
1633 let substitution_cost = usize::from(left[i - 1] != right[j - 1]);
1634 let mut best = (workspace.previous[j] + 1)
1635 .min(workspace.current[j - 1] + 1)
1636 .min(workspace.previous[j - 1] + substitution_cost);
1637
1638 if i > 1 && j > 1 && left[i - 1] == right[j - 2] && left[i - 2] == right[j - 1] {
1639 best = best.min(workspace.previous2[j - 2] + 1);
1640 }
1641
1642 workspace.current[j] = best;
1643 row_min = row_min.min(best);
1644 }
1645 if row_min > threshold {
1646 return None;
1647 }
1648 std::mem::swap(&mut workspace.previous2, &mut workspace.previous);
1649 std::mem::swap(&mut workspace.previous, &mut workspace.current);
1650 }
1651
1652 let distance = workspace.previous[right.len()];
1653 (distance <= threshold).then_some(distance)
1654}
1655
1656fn trim_common_affixes<'a>(left: &'a [char], right: &'a [char]) -> (&'a [char], &'a [char]) {
1657 let common_prefix_len = left
1658 .iter()
1659 .zip(right.iter())
1660 .take_while(|(left_ch, right_ch)| left_ch == right_ch)
1661 .count();
1662 let mut left_end = left.len();
1663 let mut right_end = right.len();
1664 while left_end > common_prefix_len
1665 && right_end > common_prefix_len
1666 && left[left_end - 1] == right[right_end - 1]
1667 {
1668 left_end -= 1;
1669 right_end -= 1;
1670 }
1671 (
1672 &left[common_prefix_len..left_end],
1673 &right[common_prefix_len..right_end],
1674 )
1675}
1676
1677#[cfg(test)]
1678mod tests {
1679 use super::*;
1680
1681 fn symbols_to_string(symbols: &[Symbol]) -> String {
1682 symbols
1683 .iter()
1684 .map(|&symbol| char::from_u32(symbol).expect("test symbol should be a char"))
1685 .collect()
1686 }
1687
1688 fn string_distance(left: &str, right: &str) -> usize {
1689 distance(&Lattice::from_paths([left]), &Lattice::from_paths([right]))
1690 }
1691
1692 fn string_damerau_distance(left: &str, right: &str) -> usize {
1693 damerau_distance(&Lattice::from_paths([left]), &Lattice::from_paths([right]))
1694 }
1695
1696 fn assert_close(left: f64, right: f64) {
1697 assert!((left - right).abs() < f64::EPSILON);
1698 }
1699
1700 #[test]
1701 fn try_path_constructors_report_invalid_inputs() {
1702 assert!(matches!(
1703 Lattice::try_from_paths(std::iter::empty::<&str>()),
1704 Err(LatticeError::Empty)
1705 ));
1706 assert!(matches!(
1707 Lattice::try_from_paths(["", "a"]),
1708 Err(LatticeError::MixedEmptyAndNonEmptyPaths)
1709 ));
1710 assert!(matches!(
1711 Lattice::try_from_symbol_paths_compact([Vec::<Symbol>::new(), vec![1]]),
1712 Err(LatticeError::MixedEmptyAndNonEmptyPaths)
1713 ));
1714 }
1715
1716 #[test]
1717 fn linear_lattice_matches_levenshtein_distance() {
1718 assert_eq!(string_distance("kitten", "sitting"), 3);
1719 assert_eq!(string_distance("insat", "insatu"), 1);
1720 assert_eq!(string_distance("abc", "abc"), 0);
1721 assert_eq!(string_distance("abc", "axc"), 1);
1722 }
1723
1724 #[test]
1725 fn normalized_similarity_uses_max_length() {
1726 assert_close(
1727 normalized_similarity_from_distance(1, "abc".chars().count(), "adc".chars().count()),
1728 2.0 / 3.0,
1729 );
1730 assert_close(normalized_similarity_str("abc", "adc"), 2.0 / 3.0);
1731 assert_eq!(normalized_similarity_str("", ""), 1.0);
1732 assert_eq!(normalized_similarity_from_distance(4, 1, 2), 0.0);
1733 }
1734
1735 #[test]
1736 fn parallel_paths_take_minimum_distance() {
1737 let left = Lattice::from_paths(["insat"]);
1738 let right = Lattice::from_paths(["insatu", "insat", "zzzzz"]);
1739
1740 assert_eq!(distance(&left, &right), 0);
1741 }
1742
1743 #[test]
1744 fn fallible_distance_apis_match_convenience_apis() {
1745 let left = Lattice::from_paths(["abcd"]);
1746 let right = Lattice::from_paths(["acbd"]);
1747
1748 assert_eq!(
1749 try_distance(&left, &right).unwrap(),
1750 distance(&left, &right)
1751 );
1752 assert_eq!(
1753 try_damerau_distance(&left, &right).unwrap(),
1754 damerau_distance(&left, &right)
1755 );
1756 assert_eq!(
1757 try_distance_with_trace(&left, &right).unwrap(),
1758 distance_with_trace(&left, &right)
1759 );
1760 assert_eq!(
1761 try_within_distance(&left, &right, 2).unwrap(),
1762 within_distance(&left, &right, 2)
1763 );
1764 assert_eq!(
1765 try_within_damerau_distance(&left, &right, 1).unwrap(),
1766 within_damerau_distance(&left, &right, 1)
1767 );
1768 }
1769
1770 #[test]
1771 fn fallible_distance_apis_reject_large_matrices() {
1772 let node_count = 4001;
1773 let lattice = Lattice::from_edges(node_count, 0, node_count - 1, Vec::new()).unwrap();
1774
1775 assert!(matches!(
1776 try_distance(&lattice, &lattice),
1777 Err(DistanceError::MatrixTooLarge {
1778 rows: 4001,
1779 cols: 4001,
1780 max_cells: MAX_DISTANCE_MATRIX_CELLS,
1781 })
1782 ));
1783 assert!(matches!(
1784 try_damerau_distance(&lattice, &lattice),
1785 Err(DistanceError::MatrixTooLarge { .. })
1786 ));
1787 assert!(matches!(
1788 try_distance_with_trace(&lattice, &lattice),
1789 Err(DistanceError::MatrixTooLarge { .. })
1790 ));
1791 assert!(matches!(
1792 try_within_distance(&lattice, &lattice, 1),
1793 Err(DistanceError::MatrixTooLarge { .. })
1794 ));
1795 assert!(matches!(
1796 try_within_damerau_distance(&lattice, &lattice, 1),
1797 Err(DistanceError::MatrixTooLarge { .. })
1798 ));
1799 assert!(matches!(
1800 try_distance_with_cutoff(&lattice, &lattice, 1),
1801 Err(DistanceError::MatrixTooLarge { .. })
1802 ));
1803 assert!(matches!(
1804 try_damerau_distance_with_cutoff(&lattice, &lattice, 1),
1805 Err(DistanceError::MatrixTooLarge { .. })
1806 ));
1807 }
1808
1809 #[test]
1810 fn trace_uses_lower_matrix_limit_than_plain_distance() {
1811 let node_count = 1415;
1812 let lattice = Lattice::from_edges(node_count, 0, node_count - 1, Vec::new()).unwrap();
1813
1814 assert!(try_distance(&lattice, &lattice).is_ok());
1815 assert!(matches!(
1816 try_distance_with_trace(&lattice, &lattice),
1817 Err(DistanceError::MatrixTooLarge {
1818 rows: 1415,
1819 cols: 1415,
1820 max_cells: MAX_TRACE_MATRIX_CELLS,
1821 })
1822 ));
1823 }
1824
1825 #[test]
1826 fn fallible_arc_accessors_report_out_of_range_nodes() {
1827 let lattice = Lattice::from_paths(["ab"]);
1828
1829 assert_eq!(
1830 lattice.try_incoming_arcs(999).map(|arcs| arcs.count()),
1831 None
1832 );
1833 assert_eq!(
1834 lattice.try_outgoing_arcs(999).map(|arcs| arcs.count()),
1835 None
1836 );
1837 assert_eq!(
1838 lattice.try_outgoing_arcs(0).map(|arcs| arcs.count()),
1839 Some(1)
1840 );
1841 }
1842
1843 #[test]
1844 fn trace_free_distance_matches_trace_distance() {
1845 let left = Lattice::from_paths(["insat", "insatu"]);
1846 let right = Lattice::from_paths(["inzatu", "insatsu"]);
1847
1848 assert_eq!(
1849 distance(&left, &right),
1850 distance_with_trace(&left, &right).distance
1851 );
1852 }
1853
1854 #[test]
1855 fn compact_paths_share_prefix_nodes() {
1856 let lattice = Lattice::from_symbol_paths_compact([
1857 "chadougu"
1858 .chars()
1859 .map(|ch| ch as Symbol)
1860 .collect::<Vec<_>>(),
1861 "chadoogu"
1862 .chars()
1863 .map(|ch| ch as Symbol)
1864 .collect::<Vec<_>>(),
1865 ]);
1866
1867 assert_eq!(distance(&lattice, &Lattice::from_paths(["chadougu"])), 0);
1868 assert_eq!(distance(&lattice, &Lattice::from_paths(["chadoogu"])), 0);
1869 assert!(lattice.node_count() < Lattice::from_paths(["chadougu", "chadoogu"]).node_count());
1870 }
1871
1872 #[test]
1873 fn compact_paths_share_equivalent_suffix_nodes() {
1874 let lattice = Lattice::from_symbol_paths_compact([
1875 "xab".chars().map(|ch| ch as Symbol).collect::<Vec<_>>(),
1876 "yab".chars().map(|ch| ch as Symbol).collect::<Vec<_>>(),
1877 ]);
1878
1879 assert_eq!(distance(&lattice, &Lattice::from_paths(["xab"])), 0);
1880 assert_eq!(distance(&lattice, &Lattice::from_paths(["yab"])), 0);
1881 assert_eq!(distance(&lattice, &Lattice::from_paths(["zab"])), 1);
1882 assert!(lattice.node_count() < Lattice::from_paths(["xab", "yab"]).node_count());
1883 }
1884
1885 #[test]
1886 fn compact_paths_preserve_prefix_words() {
1887 let lattice = Lattice::from_symbol_paths_compact([
1888 "a".chars().map(|ch| ch as Symbol).collect::<Vec<_>>(),
1889 "ab".chars().map(|ch| ch as Symbol).collect::<Vec<_>>(),
1890 ]);
1891
1892 assert_eq!(distance(&lattice, &Lattice::from_paths(["a"])), 0);
1893 assert_eq!(distance(&lattice, &Lattice::from_paths(["ab"])), 0);
1894 }
1895
1896 #[test]
1897 fn distance_with_trace_returns_best_path_pair() {
1898 let left = Lattice::from_paths(["chadougu"]);
1899 let right = Lattice::from_paths(["tyadougu", "chadougu"]);
1900
1901 let trace = distance_with_trace(&left, &right);
1902
1903 assert_eq!(trace.distance, 0);
1904 assert_eq!(symbols_to_string(&trace.left_symbols()), "chadougu");
1905 assert_eq!(symbols_to_string(&trace.right_symbols()), "chadougu");
1906 assert!(trace.steps.iter().all(|step| step.op == EditOp::Match));
1907 }
1908
1909 #[test]
1910 fn trace_includes_insertions_and_deletions() {
1911 let trace = distance_with_trace(
1912 &Lattice::from_paths(["insat"]),
1913 &Lattice::from_paths(["insatu"]),
1914 );
1915
1916 assert_eq!(trace.distance, 1);
1917 assert_eq!(symbols_to_string(&trace.left_symbols()), "insat");
1918 assert_eq!(symbols_to_string(&trace.right_symbols()), "insatu");
1919 assert_eq!(trace.steps.last().map(|step| step.op), Some(EditOp::Insert));
1920 }
1921
1922 #[test]
1923 fn threshold_check_uses_distance() {
1924 let left = Lattice::from_paths(["insat"]);
1925 let right = Lattice::from_paths(["insatu"]);
1926
1927 assert!(within_distance(&left, &right, 1));
1928 assert!(!within_distance(&left, &right, 0));
1929 assert_eq!(try_distance_with_cutoff(&left, &right, 1).unwrap(), Some(1));
1930 assert_eq!(try_distance_with_cutoff(&left, &right, 0).unwrap(), None);
1931 }
1932
1933 #[test]
1934 fn threshold_check_prunes_but_preserves_lattice_paths() {
1935 let left = Lattice::from_paths(["chadougu", "tyadougu"]);
1936 let right = Lattice::from_paths(["chadoogu", "zzzzzzzz"]);
1937
1938 assert_eq!(distance(&left, &right), 1);
1939 assert!(within_distance(&left, &right, 1));
1940 assert!(!within_distance(&left, &right, 0));
1941 assert_eq!(try_distance_with_cutoff(&left, &right, 1).unwrap(), Some(1));
1942 assert_eq!(try_distance_with_cutoff(&left, &right, 0).unwrap(), None);
1943 }
1944
1945 #[test]
1946 fn linear_lattice_damerau_matches_string_damerau_distance() {
1947 for (left, right) in [
1948 ("ca", "ac"),
1949 ("abc", "acb"),
1950 ("abcdef", "abcedf"),
1951 ("moine", "mione"),
1952 ("マトリッツォ", "マリトッツォ"),
1953 ] {
1954 assert_eq!(
1955 string_damerau_distance(left, right),
1956 damerau_levenshtein_str(left, right),
1957 "{left:?} / {right:?}"
1958 );
1959 }
1960 }
1961
1962 #[test]
1963 fn lattice_damerau_takes_transposition_across_candidate_paths() {
1964 let left = Lattice::from_paths(["abc", "axc"]);
1965 let right = Lattice::from_paths(["acb"]);
1966
1967 assert_eq!(distance(&left, &right), 2);
1968 assert_eq!(damerau_distance(&left, &right), 1);
1969 }
1970
1971 #[test]
1972 fn lattice_damerau_supports_branched_two_arc_transposition() {
1973 let left = Lattice::from_edges(
1974 4,
1975 0,
1976 3,
1977 vec![
1978 Arc::new(0, 1, 'a' as Symbol),
1979 Arc::new(0, 1, 'x' as Symbol),
1980 Arc::new(1, 3, 'b' as Symbol),
1981 Arc::new(1, 3, 'y' as Symbol),
1982 ],
1983 )
1984 .unwrap();
1985 let right = Lattice::from_paths(["ba"]);
1986
1987 assert_eq!(damerau_distance(&left, &right), 1);
1988 }
1989
1990 #[test]
1991 fn threshold_damerau_check_uses_lattice_damerau_distance() {
1992 let left = Lattice::from_paths(["abc", "axc"]);
1993 let right = Lattice::from_paths(["acb"]);
1994
1995 assert!(within_damerau_distance(&left, &right, 1));
1996 assert!(!within_damerau_distance(&left, &right, 0));
1997 assert_eq!(
1998 try_damerau_distance_with_cutoff(&left, &right, 1).unwrap(),
1999 Some(1)
2000 );
2001 assert_eq!(
2002 try_damerau_distance_with_cutoff(&left, &right, 0).unwrap(),
2003 None
2004 );
2005 }
2006
2007 #[test]
2008 fn from_edges_rejects_non_topological_arcs() {
2009 let result = Lattice::from_edges(2, 0, 1, vec![Arc::new(1, 0, 'a' as Symbol)]);
2010
2011 assert!(matches!(
2012 result,
2013 Err(LatticeError::InvalidArcOrder { src: 1, dst: 0 })
2014 ));
2015 }
2016
2017 #[test]
2018 fn surface_levenshtein_counts_unicode_chars() {
2019 assert_eq!(levenshtein_str("kitten", "sitting"), 3);
2020 assert_eq!(levenshtein_str("いんさt", "印刷"), 4);
2021 assert_eq!(levenshtein_str("マトリッツォ", "マリトッツォ"), 2);
2022 assert_eq!(levenshtein_str("prefix-abc-suffix", "prefix-axc-suffix"), 1);
2023 assert_eq!(levenshtein_str_with_cutoff("kitten", "sitting", 3), Some(3));
2024 assert_eq!(levenshtein_str_with_cutoff("kitten", "sitting", 2), None);
2025 assert_eq!(
2026 levenshtein_str_with_cutoff("prefix-abc-suffix", "prefix-axc-suffix", 1),
2027 Some(1)
2028 );
2029 assert_eq!(
2030 levenshtein_str_with_cutoff("蒸留所", "ジョウリュウショ", 1),
2031 None
2032 );
2033 }
2034
2035 #[test]
2036 fn surface_damerau_counts_adjacent_transposition() {
2037 assert_eq!(damerau_levenshtein_str("ca", "ac"), 1);
2038 assert_eq!(try_damerau_levenshtein_str("ca", "ac").unwrap(), 1);
2039 assert_eq!(damerau_levenshtein_str("マトリッツォ", "マリトッツォ"), 1);
2040 assert_eq!(damerau_levenshtein_str("いんさt", "印刷"), 4);
2041 assert_eq!(
2042 try_damerau_levenshtein_str_with_cutoff("abc", "acb", 1).unwrap(),
2043 Some(1)
2044 );
2045 assert_eq!(
2046 try_damerau_levenshtein_str_with_cutoff("abc", "acb", 0).unwrap(),
2047 None
2048 );
2049 assert_eq!(
2050 try_damerau_levenshtein_str_with_cutoff("蒸留所", "ジョウリュウショ", 1).unwrap(),
2051 None
2052 );
2053 }
2054}