1#![deny(missing_docs)]
26
27use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque};
28use std::error::Error;
29use std::fmt;
30
31pub type Symbol = u32;
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub struct Arc {
39 pub src: usize,
41 pub dst: usize,
43 pub symbol: Symbol,
45}
46
47impl Arc {
48 pub fn new(src: usize, dst: usize, symbol: Symbol) -> Self {
50 Self { src, dst, symbol }
51 }
52}
53
54#[derive(Clone, Debug, Eq, PartialEq)]
60pub struct Lattice {
61 node_count: usize,
62 start: usize,
63 end: usize,
64 arcs: Vec<Arc>,
65 incoming: Vec<Vec<usize>>,
66 outgoing: Vec<Vec<usize>>,
67}
68
69#[derive(Clone, Debug, Eq, PartialEq)]
71pub enum LatticeError {
72 Empty,
74 MixedEmptyAndNonEmptyPaths,
76 InvalidNode {
78 node: usize,
80 node_count: usize,
82 },
83 InvalidArcOrder {
85 src: usize,
87 dst: usize,
89 },
90 InvalidEndpoint {
92 start: usize,
94 end: usize,
96 },
97}
98
99impl fmt::Display for LatticeError {
100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101 match self {
102 Self::Empty => write!(f, "lattice must contain at least one node"),
103 Self::MixedEmptyAndNonEmptyPaths => write!(
104 f,
105 "mixed empty and non-empty paths need epsilon arcs, which moine-core does not model yet"
106 ),
107 Self::InvalidNode { node, node_count } => {
108 write!(f, "node {node} is outside 0..{node_count}")
109 }
110 Self::InvalidArcOrder { src, dst } => {
111 write!(f, "arc {src}->{dst} violates topological node order")
112 }
113 Self::InvalidEndpoint { start, end } => {
114 write!(f, "invalid endpoints start={start}, end={end}")
115 }
116 }
117 }
118}
119
120impl Error for LatticeError {}
121
122impl Lattice {
123 pub fn from_edges(
128 node_count: usize,
129 start: usize,
130 end: usize,
131 arcs: Vec<Arc>,
132 ) -> Result<Self, LatticeError> {
133 if node_count == 0 {
134 return Err(LatticeError::Empty);
135 }
136 if start >= node_count || end >= node_count || start > end {
137 return Err(LatticeError::InvalidEndpoint { start, end });
138 }
139
140 let mut incoming = vec![Vec::new(); node_count];
141 let mut outgoing = vec![Vec::new(); node_count];
142 for (idx, arc) in arcs.iter().enumerate() {
143 if arc.src >= node_count {
144 return Err(LatticeError::InvalidNode {
145 node: arc.src,
146 node_count,
147 });
148 }
149 if arc.dst >= node_count {
150 return Err(LatticeError::InvalidNode {
151 node: arc.dst,
152 node_count,
153 });
154 }
155 if arc.src >= arc.dst {
156 return Err(LatticeError::InvalidArcOrder {
157 src: arc.src,
158 dst: arc.dst,
159 });
160 }
161 incoming[arc.dst].push(idx);
162 outgoing[arc.src].push(idx);
163 }
164
165 Ok(Self {
166 node_count,
167 start,
168 end,
169 arcs,
170 incoming,
171 outgoing,
172 })
173 }
174
175 pub fn from_paths<I, S>(paths: I) -> Self
183 where
184 I: IntoIterator<Item = S>,
185 S: AsRef<str>,
186 {
187 Self::try_from_paths(paths).expect("valid string path lattice")
188 }
189
190 pub fn try_from_paths<I, S>(paths: I) -> Result<Self, LatticeError>
192 where
193 I: IntoIterator<Item = S>,
194 S: AsRef<str>,
195 {
196 let symbol_paths = paths.into_iter().map(|path| {
197 path.as_ref()
198 .chars()
199 .map(|ch| ch as Symbol)
200 .collect::<Vec<_>>()
201 });
202 Self::try_from_symbol_paths(symbol_paths)
203 }
204
205 pub fn from_symbol_paths<I, P>(paths: I) -> Self
213 where
214 I: IntoIterator<Item = P>,
215 P: IntoIterator<Item = Symbol>,
216 {
217 Self::try_from_symbol_paths(paths).expect("valid symbol path lattice")
218 }
219
220 pub fn try_from_symbol_paths<I, P>(paths: I) -> Result<Self, LatticeError>
222 where
223 I: IntoIterator<Item = P>,
224 P: IntoIterator<Item = Symbol>,
225 {
226 let paths = paths
227 .into_iter()
228 .map(|path| path.into_iter().collect::<Vec<_>>())
229 .collect::<Vec<_>>();
230 if paths.is_empty() {
231 return Err(LatticeError::Empty);
232 }
233
234 if paths.len() == 1 && paths[0].is_empty() {
235 return Self::from_edges(1, 0, 0, Vec::new());
236 }
237 if !paths.iter().all(|path| !path.is_empty()) {
238 return Err(LatticeError::MixedEmptyAndNonEmptyPaths);
239 }
240
241 let start = 0;
242 let node_count = 2 + paths
243 .iter()
244 .map(|path| path.len().saturating_sub(1))
245 .sum::<usize>();
246 let end = node_count - 1;
247 let mut next_node = 1;
248 let mut arcs = Vec::new();
249
250 for path in paths {
251 let mut current = start;
252 for (idx, symbol) in path.iter().copied().enumerate() {
253 let dst = if idx + 1 == path.len() {
254 end
255 } else {
256 let node = next_node;
257 next_node += 1;
258 node
259 };
260 arcs.push(Arc::new(current, dst, symbol));
261 current = dst;
262 }
263 }
264
265 debug_assert_eq!(next_node, end);
266 Self::from_edges(node_count, start, end, arcs)
267 }
268
269 pub fn from_symbol_paths_compact<I, P>(paths: I) -> Self
277 where
278 I: IntoIterator<Item = P>,
279 P: IntoIterator<Item = Symbol>,
280 {
281 Self::try_from_symbol_paths_compact(paths).expect("valid compact symbol path lattice")
282 }
283
284 pub fn try_from_symbol_paths_compact<I, P>(paths: I) -> Result<Self, LatticeError>
286 where
287 I: IntoIterator<Item = P>,
288 P: IntoIterator<Item = Symbol>,
289 {
290 let paths = paths
291 .into_iter()
292 .map(|path| path.into_iter().collect::<Vec<_>>())
293 .collect::<Vec<_>>();
294 if paths.is_empty() {
295 return Err(LatticeError::Empty);
296 }
297
298 if paths.len() == 1 && paths[0].is_empty() {
299 return Self::from_edges(1, 0, 0, Vec::new());
300 }
301 if !paths.iter().all(|path| !path.is_empty()) {
302 return Err(LatticeError::MixedEmptyAndNonEmptyPaths);
303 }
304
305 let mut builder = CompactPathBuilder::default();
306 for path in paths {
307 builder.insert(&path);
308 }
309 Ok(builder.into_lattice())
310 }
311
312 pub fn node_count(&self) -> usize {
314 self.node_count
315 }
316
317 pub fn start(&self) -> usize {
319 self.start
320 }
321
322 pub fn end(&self) -> usize {
324 self.end
325 }
326
327 pub fn arcs(&self) -> &[Arc] {
329 &self.arcs
330 }
331
332 pub fn try_incoming_arcs(&self, node: usize) -> Option<impl Iterator<Item = &Arc>> {
335 self.incoming
336 .get(node)
337 .map(|indices| indices.iter().map(|&idx| &self.arcs[idx]))
338 }
339
340 pub fn incoming_arcs(&self, node: usize) -> impl Iterator<Item = &Arc> {
346 self.try_incoming_arcs(node)
347 .expect("node should be inside lattice")
348 }
349
350 pub fn try_outgoing_arcs(&self, node: usize) -> Option<impl Iterator<Item = &Arc>> {
353 self.outgoing
354 .get(node)
355 .map(|indices| indices.iter().map(|&idx| &self.arcs[idx]))
356 }
357
358 pub fn outgoing_arcs(&self, node: usize) -> impl Iterator<Item = &Arc> {
364 self.try_outgoing_arcs(node)
365 .expect("node should be inside lattice")
366 }
367}
368
369#[derive(Clone, Debug, Default)]
370struct TrieNode {
371 children: BTreeMap<Symbol, usize>,
372 terminal_symbols: BTreeSet<Symbol>,
373}
374
375#[derive(Clone, Debug, Default)]
376struct CompactPathBuilder {
377 nodes: Vec<TrieNode>,
378}
379
380#[derive(Clone, Debug, Eq, Hash, PartialEq)]
381struct CompactSignature {
382 terminal_symbols: Vec<Symbol>,
383 children: Vec<(Symbol, usize)>,
384}
385
386#[derive(Clone, Debug)]
387struct CompactNode {
388 terminal_symbols: Vec<Symbol>,
389 children: Vec<(Symbol, usize)>,
390}
391
392impl CompactPathBuilder {
393 fn insert(&mut self, path: &[Symbol]) {
394 if self.nodes.is_empty() {
395 self.nodes.push(TrieNode::default());
396 }
397
398 let mut current = 0;
399 for (idx, symbol) in path.iter().copied().enumerate() {
400 if idx + 1 == path.len() {
401 self.nodes[current].terminal_symbols.insert(symbol);
402 break;
403 }
404
405 let next = if let Some(&next) = self.nodes[current].children.get(&symbol) {
406 next
407 } else {
408 let next = self.nodes.len();
409 self.nodes.push(TrieNode::default());
410 self.nodes[current].children.insert(symbol, next);
411 next
412 };
413 current = next;
414 }
415 }
416
417 fn into_lattice(self) -> Lattice {
418 let mut minimizer = CompactMinimizer {
419 trie_nodes: self.nodes,
420 memo: HashMap::new(),
421 interned: HashMap::new(),
422 compact_nodes: Vec::new(),
423 };
424 let root = minimizer.compact_trie_node(0);
425 build_compact_lattice(root, minimizer.compact_nodes)
426 }
427}
428
429struct CompactMinimizer {
430 trie_nodes: Vec<TrieNode>,
431 memo: HashMap<usize, usize>,
432 interned: HashMap<CompactSignature, usize>,
433 compact_nodes: Vec<CompactNode>,
434}
435
436impl CompactMinimizer {
437 fn compact_trie_node(&mut self, trie_id: usize) -> usize {
438 if let Some(&compact_id) = self.memo.get(&trie_id) {
439 return compact_id;
440 }
441
442 let children = self.trie_nodes[trie_id]
443 .children
444 .clone()
445 .into_iter()
446 .map(|(symbol, child)| (symbol, self.compact_trie_node(child)))
447 .collect::<Vec<_>>();
448 let terminal_symbols = self.trie_nodes[trie_id]
449 .terminal_symbols
450 .iter()
451 .copied()
452 .collect::<Vec<_>>();
453 let signature = CompactSignature {
454 terminal_symbols,
455 children,
456 };
457
458 let compact_id = if let Some(&existing) = self.interned.get(&signature) {
459 existing
460 } else {
461 let compact_id = self.compact_nodes.len();
462 self.compact_nodes.push(CompactNode {
463 terminal_symbols: signature.terminal_symbols.clone(),
464 children: signature.children.clone(),
465 });
466 self.interned.insert(signature, compact_id);
467 compact_id
468 };
469
470 self.memo.insert(trie_id, compact_id);
471 compact_id
472 }
473}
474
475fn build_compact_lattice(root: usize, compact_nodes: Vec<CompactNode>) -> Lattice {
476 let mut reachable = Vec::new();
477 collect_reachable_compact_nodes(root, &compact_nodes, &mut BTreeSet::new(), &mut reachable);
478
479 let mut heights = HashMap::new();
480 for &node in &reachable {
481 compact_height(node, &compact_nodes, &mut heights);
482 }
483
484 reachable.sort_by_key(|node| {
485 (
486 usize::from(*node != root),
487 std::cmp::Reverse(*heights.get(node).expect("height should be known")),
488 *node,
489 )
490 });
491
492 let node_ids = reachable
493 .iter()
494 .enumerate()
495 .map(|(node_id, &compact_id)| (compact_id, node_id))
496 .collect::<HashMap<_, _>>();
497 let end = reachable.len();
498 let mut arcs = Vec::new();
499
500 for &compact_id in &reachable {
501 let src = node_ids[&compact_id];
502 let node = &compact_nodes[compact_id];
503 for &symbol in &node.terminal_symbols {
504 arcs.push(Arc::new(src, end, symbol));
505 }
506 for &(symbol, child) in &node.children {
507 arcs.push(Arc::new(src, node_ids[&child], symbol));
508 }
509 }
510
511 Lattice::from_edges(end + 1, 0, end, arcs).expect("valid compact path lattice")
512}
513
514fn collect_reachable_compact_nodes(
515 node: usize,
516 compact_nodes: &[CompactNode],
517 seen: &mut BTreeSet<usize>,
518 output: &mut Vec<usize>,
519) {
520 if !seen.insert(node) {
521 return;
522 }
523 output.push(node);
524 for &(_, child) in &compact_nodes[node].children {
525 collect_reachable_compact_nodes(child, compact_nodes, seen, output);
526 }
527}
528
529fn compact_height(
530 node: usize,
531 compact_nodes: &[CompactNode],
532 memo: &mut HashMap<usize, usize>,
533) -> usize {
534 if let Some(&height) = memo.get(&node) {
535 return height;
536 }
537
538 let child_height = compact_nodes[node]
539 .children
540 .iter()
541 .map(|&(_, child)| compact_height(child, compact_nodes, memo) + 1)
542 .max()
543 .unwrap_or(0);
544 let terminal_height = usize::from(!compact_nodes[node].terminal_symbols.is_empty());
545 let height = child_height.max(terminal_height);
546 memo.insert(node, height);
547 height
548}
549
550#[derive(Clone, Copy, Debug, Eq, PartialEq)]
552pub enum EditOp {
553 Match,
555 Substitute,
557 Delete,
559 Insert,
561}
562
563#[derive(Clone, Debug, Eq, PartialEq)]
565pub struct TraceStep {
566 pub op: EditOp,
568 pub left: Option<Symbol>,
570 pub right: Option<Symbol>,
572}
573
574#[derive(Clone, Debug, Eq, PartialEq)]
576pub struct DistanceTrace {
577 pub distance: usize,
579 pub steps: Vec<TraceStep>,
581}
582
583impl DistanceTrace {
584 pub fn left_symbols(&self) -> Vec<Symbol> {
586 self.steps.iter().filter_map(|step| step.left).collect()
587 }
588
589 pub fn right_symbols(&self) -> Vec<Symbol> {
591 self.steps.iter().filter_map(|step| step.right).collect()
592 }
593}
594
595pub const MAX_DISTANCE_MATRIX_CELLS: usize = 16_000_000;
597pub const MAX_TRACE_MATRIX_CELLS: usize = 2_000_000;
599
600#[derive(Clone, Debug, Eq, PartialEq)]
602pub enum DistanceError {
603 MatrixTooLarge {
605 rows: usize,
607 cols: usize,
609 max_cells: usize,
611 },
612}
613
614impl fmt::Display for DistanceError {
615 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
616 match self {
617 Self::MatrixTooLarge {
618 rows,
619 cols,
620 max_cells,
621 } => write!(
622 f,
623 "distance matrix {rows}x{cols} exceeds the maximum of {max_cells} cells"
624 ),
625 }
626 }
627}
628
629impl Error for DistanceError {}
630
631#[derive(Clone, Debug, Eq, PartialEq)]
632struct Backpointer {
633 prev_i: usize,
634 prev_j: usize,
635 step: TraceStep,
636}
637
638const INF: usize = usize::MAX / 4;
639
640pub fn distance(left: &Lattice, right: &Lattice) -> usize {
647 try_distance(left, right).expect("distance matrix should fit")
648}
649
650pub fn try_distance(left: &Lattice, right: &Lattice) -> Result<usize, DistanceError> {
652 let rows = left.node_count();
653 let cols = right.node_count();
654 let cells = checked_distance_matrix_cells(rows, cols)?;
655 Ok(distance_impl(left, right, cells))
656}
657
658fn distance_impl(left: &Lattice, right: &Lattice, cells: usize) -> usize {
659 let cols = right.node_count();
660 let mut dp = vec![INF; cells];
661 dp[distance_index(left.start(), right.start(), cols)] = 0;
662
663 for i in left.start()..=left.end() {
664 for j in right.start()..=right.end() {
665 if i == left.start() && j == right.start() {
666 continue;
667 }
668
669 let mut best = dp[distance_index(i, j, cols)];
670
671 for left_arc in left.incoming_arcs(i) {
672 for right_arc in right.incoming_arcs(j) {
673 let cost = usize::from(left_arc.symbol != right_arc.symbol);
674 let candidate =
675 dp[distance_index(left_arc.src, right_arc.src, cols)].saturating_add(cost);
676 best = best.min(candidate);
677 }
678 }
679
680 for left_arc in left.incoming_arcs(i) {
681 let candidate = dp[distance_index(left_arc.src, j, cols)].saturating_add(1);
682 best = best.min(candidate);
683 }
684
685 for right_arc in right.incoming_arcs(j) {
686 let candidate = dp[distance_index(i, right_arc.src, cols)].saturating_add(1);
687 best = best.min(candidate);
688 }
689
690 dp[distance_index(i, j, cols)] = best;
691 }
692 }
693
694 dp[distance_index(left.end(), right.end(), cols)]
695}
696
697pub fn damerau_distance(left: &Lattice, right: &Lattice) -> usize {
704 try_damerau_distance(left, right).expect("distance matrix should fit")
705}
706
707pub fn try_damerau_distance(left: &Lattice, right: &Lattice) -> Result<usize, DistanceError> {
710 let rows = left.node_count();
711 let cols = right.node_count();
712 let cells = checked_distance_matrix_cells(rows, cols)?;
713 Ok(damerau_distance_impl(left, right, cells))
714}
715
716fn damerau_distance_impl(left: &Lattice, right: &Lattice, cells: usize) -> usize {
717 let cols = right.node_count();
718 let mut dp = vec![INF; cells];
719 dp[distance_index(left.start(), right.start(), cols)] = 0;
720
721 for i in left.start()..=left.end() {
722 for j in right.start()..=right.end() {
723 if i == left.start() && j == right.start() {
724 continue;
725 }
726
727 let mut best = dp[distance_index(i, j, cols)];
728
729 for left_arc in left.incoming_arcs(i) {
730 for right_arc in right.incoming_arcs(j) {
731 let cost = usize::from(left_arc.symbol != right_arc.symbol);
732 let candidate =
733 dp[distance_index(left_arc.src, right_arc.src, cols)].saturating_add(cost);
734 best = best.min(candidate);
735 }
736 }
737
738 for left_arc in left.incoming_arcs(i) {
739 let candidate = dp[distance_index(left_arc.src, j, cols)].saturating_add(1);
740 best = best.min(candidate);
741 }
742
743 for right_arc in right.incoming_arcs(j) {
744 let candidate = dp[distance_index(i, right_arc.src, cols)].saturating_add(1);
745 best = best.min(candidate);
746 }
747
748 for left_second in left.incoming_arcs(i) {
749 for right_second in right.incoming_arcs(j) {
750 for left_first in left.incoming_arcs(left_second.src) {
751 for right_first in right.incoming_arcs(right_second.src) {
752 if left_first.symbol == right_second.symbol
753 && left_second.symbol == right_first.symbol
754 {
755 let candidate = dp
756 [distance_index(left_first.src, right_first.src, cols)]
757 .saturating_add(1);
758 best = best.min(candidate);
759 }
760 }
761 }
762 }
763 }
764
765 dp[distance_index(i, j, cols)] = best;
766 }
767 }
768
769 dp[distance_index(left.end(), right.end(), cols)]
770}
771
772pub fn distance_with_trace(left: &Lattice, right: &Lattice) -> DistanceTrace {
786 try_distance_with_trace(left, right).expect("distance matrix should fit")
787}
788
789pub fn try_distance_with_trace(
792 left: &Lattice,
793 right: &Lattice,
794) -> Result<DistanceTrace, DistanceError> {
795 let rows = left.node_count();
796 let cols = right.node_count();
797 let cells = checked_trace_matrix_cells(rows, cols)?;
798 Ok(distance_with_trace_impl(left, right, cells))
799}
800
801fn distance_with_trace_impl(left: &Lattice, right: &Lattice, cells: usize) -> DistanceTrace {
802 let cols = right.node_count();
803 let mut dp = vec![INF; cells];
804 let mut back = vec![None; cells];
805 dp[distance_index(left.start(), right.start(), cols)] = 0;
806
807 for i in left.start()..=left.end() {
808 for j in right.start()..=right.end() {
809 if i == left.start() && j == right.start() {
810 continue;
811 }
812
813 let index = distance_index(i, j, cols);
814 let mut best = dp[index];
815 let mut best_back = back[index].clone();
816
817 for left_arc in left.incoming_arcs(i) {
818 for right_arc in right.incoming_arcs(j) {
819 let cost = usize::from(left_arc.symbol != right_arc.symbol);
820 let candidate =
821 dp[distance_index(left_arc.src, right_arc.src, cols)].saturating_add(cost);
822 if candidate < best {
823 best = candidate;
824 best_back = Some(Backpointer {
825 prev_i: left_arc.src,
826 prev_j: right_arc.src,
827 step: TraceStep {
828 op: if cost == 0 {
829 EditOp::Match
830 } else {
831 EditOp::Substitute
832 },
833 left: Some(left_arc.symbol),
834 right: Some(right_arc.symbol),
835 },
836 });
837 }
838 }
839 }
840
841 for left_arc in left.incoming_arcs(i) {
842 let candidate = dp[distance_index(left_arc.src, j, cols)].saturating_add(1);
843 if candidate < best {
844 best = candidate;
845 best_back = Some(Backpointer {
846 prev_i: left_arc.src,
847 prev_j: j,
848 step: TraceStep {
849 op: EditOp::Delete,
850 left: Some(left_arc.symbol),
851 right: None,
852 },
853 });
854 }
855 }
856
857 for right_arc in right.incoming_arcs(j) {
858 let candidate = dp[distance_index(i, right_arc.src, cols)].saturating_add(1);
859 if candidate < best {
860 best = candidate;
861 best_back = Some(Backpointer {
862 prev_i: i,
863 prev_j: right_arc.src,
864 step: TraceStep {
865 op: EditOp::Insert,
866 left: None,
867 right: Some(right_arc.symbol),
868 },
869 });
870 }
871 }
872
873 dp[index] = best;
874 back[index] = best_back;
875 }
876 }
877
878 let mut steps = Vec::new();
879 let mut i = left.end();
880 let mut j = right.end();
881 while i != left.start() || j != right.start() {
882 let Some(prev) = &back[distance_index(i, j, cols)] else {
883 break;
884 };
885 steps.push(prev.step.clone());
886 i = prev.prev_i;
887 j = prev.prev_j;
888 }
889 steps.reverse();
890
891 DistanceTrace {
892 distance: dp[distance_index(left.end(), right.end(), cols)],
893 steps,
894 }
895}
896
897pub fn within_distance(left: &Lattice, right: &Lattice, threshold: usize) -> bool {
904 try_within_distance(left, right, threshold).expect("distance matrix should fit")
905}
906
907pub fn try_within_distance(
910 left: &Lattice,
911 right: &Lattice,
912 threshold: usize,
913) -> Result<bool, DistanceError> {
914 let rows = left.node_count();
915 let cols = right.node_count();
916 let cells = checked_distance_matrix_cells(rows, cols)?;
917 Ok(within_distance_impl(left, right, threshold, cells))
918}
919
920fn within_distance_impl(left: &Lattice, right: &Lattice, threshold: usize, cells: usize) -> bool {
921 let cols = right.node_count();
922 let mut dp = vec![INF; cells];
923 let mut queued = vec![false; cells];
924 let mut queue = VecDeque::new();
925 let start = distance_index(left.start(), right.start(), cols);
926 dp[start] = 0;
927 queued[start] = true;
928 queue.push_back((left.start(), right.start()));
929 let mut search = ThresholdSearch {
930 threshold,
931 cols,
932 dp: &mut dp,
933 queued: &mut queued,
934 queue: &mut queue,
935 };
936
937 while let Some((i, j)) = search.queue.pop_front() {
938 let index = distance_index(i, j, cols);
939 search.queued[index] = false;
940 let current = search.dp[index];
941 if current > threshold {
942 continue;
943 }
944 if i == left.end() && j == right.end() {
945 return true;
946 }
947
948 for right_arc in right.outgoing_arcs(j) {
949 search.relax(i, right_arc.dst, current.saturating_add(1));
950 }
951
952 for left_arc in left.outgoing_arcs(i) {
953 search.relax(left_arc.dst, j, current.saturating_add(1));
954 }
955
956 for left_arc in left.outgoing_arcs(i) {
957 for right_arc in right.outgoing_arcs(j) {
958 let cost = usize::from(left_arc.symbol != right_arc.symbol);
959 search.relax(left_arc.dst, right_arc.dst, current.saturating_add(cost));
960 }
961 }
962 }
963
964 false
965}
966
967pub fn within_damerau_distance(left: &Lattice, right: &Lattice, threshold: usize) -> bool {
976 try_within_damerau_distance(left, right, threshold).expect("distance matrix should fit")
977}
978
979pub fn try_within_damerau_distance(
982 left: &Lattice,
983 right: &Lattice,
984 threshold: usize,
985) -> Result<bool, DistanceError> {
986 let rows = left.node_count();
987 let cols = right.node_count();
988 let cells = checked_distance_matrix_cells(rows, cols)?;
989 Ok(within_damerau_distance_impl(left, right, threshold, cells))
990}
991
992fn within_damerau_distance_impl(
993 left: &Lattice,
994 right: &Lattice,
995 threshold: usize,
996 cells: usize,
997) -> bool {
998 let cols = right.node_count();
999 let mut dp = vec![INF; cells];
1000 let mut queued = vec![false; cells];
1001 let mut queue = VecDeque::new();
1002 let start = distance_index(left.start(), right.start(), cols);
1003 dp[start] = 0;
1004 queued[start] = true;
1005 queue.push_back((left.start(), right.start()));
1006 let mut search = ThresholdSearch {
1007 threshold,
1008 cols,
1009 dp: &mut dp,
1010 queued: &mut queued,
1011 queue: &mut queue,
1012 };
1013
1014 while let Some((i, j)) = search.queue.pop_front() {
1015 let index = distance_index(i, j, cols);
1016 search.queued[index] = false;
1017 let current = search.dp[index];
1018 if current > threshold {
1019 continue;
1020 }
1021 if i == left.end() && j == right.end() {
1022 return true;
1023 }
1024
1025 for right_arc in right.outgoing_arcs(j) {
1026 search.relax(i, right_arc.dst, current.saturating_add(1));
1027 }
1028
1029 for left_arc in left.outgoing_arcs(i) {
1030 search.relax(left_arc.dst, j, current.saturating_add(1));
1031 }
1032
1033 for left_arc in left.outgoing_arcs(i) {
1034 for right_arc in right.outgoing_arcs(j) {
1035 let cost = usize::from(left_arc.symbol != right_arc.symbol);
1036 search.relax(left_arc.dst, right_arc.dst, current.saturating_add(cost));
1037 }
1038 }
1039
1040 for left_first in left.outgoing_arcs(i) {
1041 for right_first in right.outgoing_arcs(j) {
1042 for left_second in left.outgoing_arcs(left_first.dst) {
1043 for right_second in right.outgoing_arcs(right_first.dst) {
1044 if left_first.symbol == right_second.symbol
1045 && left_second.symbol == right_first.symbol
1046 {
1047 search.relax(
1048 left_second.dst,
1049 right_second.dst,
1050 current.saturating_add(1),
1051 );
1052 }
1053 }
1054 }
1055 }
1056 }
1057 }
1058
1059 false
1060}
1061
1062fn checked_distance_matrix_cells(rows: usize, cols: usize) -> Result<usize, DistanceError> {
1063 checked_matrix_cells(rows, cols, MAX_DISTANCE_MATRIX_CELLS)
1064}
1065
1066fn checked_trace_matrix_cells(rows: usize, cols: usize) -> Result<usize, DistanceError> {
1067 checked_matrix_cells(rows, cols, MAX_TRACE_MATRIX_CELLS)
1068}
1069
1070fn checked_matrix_cells(
1071 rows: usize,
1072 cols: usize,
1073 max_cells: usize,
1074) -> Result<usize, DistanceError> {
1075 let cells = rows
1076 .checked_mul(cols)
1077 .ok_or(DistanceError::MatrixTooLarge {
1078 rows,
1079 cols,
1080 max_cells,
1081 })?;
1082 if cells > max_cells {
1083 return Err(DistanceError::MatrixTooLarge {
1084 rows,
1085 cols,
1086 max_cells,
1087 });
1088 }
1089 Ok(cells)
1090}
1091
1092fn distance_index(i: usize, j: usize, cols: usize) -> usize {
1093 i * cols + j
1094}
1095
1096struct ThresholdSearch<'a> {
1097 threshold: usize,
1098 cols: usize,
1099 dp: &'a mut [usize],
1100 queued: &'a mut [bool],
1101 queue: &'a mut VecDeque<(usize, usize)>,
1102}
1103
1104impl ThresholdSearch<'_> {
1105 fn relax(&mut self, i: usize, j: usize, candidate: usize) {
1106 if candidate > self.threshold {
1107 return;
1108 }
1109 let index = distance_index(i, j, self.cols);
1110 if candidate >= self.dp[index] {
1111 return;
1112 }
1113 self.dp[index] = candidate;
1114 if !self.queued[index] {
1115 self.queued[index] = true;
1116 self.queue.push_back((i, j));
1117 }
1118 }
1119}
1120
1121pub fn normalized_similarity_from_distance(
1125 distance: usize,
1126 left_len: usize,
1127 right_len: usize,
1128) -> f64 {
1129 let max_len = left_len.max(right_len);
1130 if max_len == 0 {
1131 return 1.0;
1132 }
1133
1134 (1.0 - distance as f64 / max_len as f64).clamp(0.0, 1.0)
1135}
1136
1137pub fn normalized_similarity_str(left: &str, right: &str) -> f64 {
1139 normalized_similarity_from_distance(
1140 levenshtein_str(left, right),
1141 left.chars().count(),
1142 right.chars().count(),
1143 )
1144}
1145
1146pub fn levenshtein_str(left: &str, right: &str) -> usize {
1148 let left = left.chars().collect::<Vec<_>>();
1149 let right = right.chars().collect::<Vec<_>>();
1150 levenshtein_chars(&left, &right)
1151}
1152
1153pub fn damerau_levenshtein_str(left: &str, right: &str) -> usize {
1161 try_damerau_levenshtein_str(left, right).expect("distance matrix should fit")
1162}
1163
1164pub fn try_damerau_levenshtein_str(left: &str, right: &str) -> Result<usize, DistanceError> {
1167 let left = left.chars().collect::<Vec<_>>();
1168 let right = right.chars().collect::<Vec<_>>();
1169 damerau_levenshtein_chars(&left, &right)
1170}
1171
1172fn levenshtein_chars(left: &[char], right: &[char]) -> usize {
1173 let (shorter, longer) = if left.len() <= right.len() {
1174 (left, right)
1175 } else {
1176 (right, left)
1177 };
1178 let mut previous = (0..=shorter.len()).collect::<Vec<_>>();
1179 let mut current = vec![0; shorter.len() + 1];
1180
1181 for (i, &longer_ch) in longer.iter().enumerate() {
1182 current[0] = i + 1;
1183 for (j, &shorter_ch) in shorter.iter().enumerate() {
1184 let substitution_cost = usize::from(longer_ch != shorter_ch);
1185 current[j + 1] = (previous[j + 1] + 1)
1186 .min(current[j] + 1)
1187 .min(previous[j] + substitution_cost);
1188 }
1189 std::mem::swap(&mut previous, &mut current);
1190 }
1191
1192 previous[shorter.len()]
1193}
1194
1195fn damerau_levenshtein_chars(left: &[char], right: &[char]) -> Result<usize, DistanceError> {
1196 let rows = left.len() + 1;
1197 let cols = right.len() + 1;
1198 let cells = checked_distance_matrix_cells(rows, cols)?;
1199 let mut dp = vec![0; cells];
1200
1201 for i in 0..rows {
1202 dp[distance_index(i, 0, cols)] = i;
1203 }
1204 for j in 0..cols {
1205 dp[distance_index(0, j, cols)] = j;
1206 }
1207
1208 for i in 1..rows {
1209 for j in 1..cols {
1210 let substitution_cost = usize::from(left[i - 1] != right[j - 1]);
1211 let mut best = (dp[distance_index(i - 1, j, cols)] + 1)
1212 .min(dp[distance_index(i, j - 1, cols)] + 1)
1213 .min(dp[distance_index(i - 1, j - 1, cols)] + substitution_cost);
1214
1215 if i > 1 && j > 1 && left[i - 1] == right[j - 2] && left[i - 2] == right[j - 1] {
1216 best = best.min(dp[distance_index(i - 2, j - 2, cols)] + 1);
1217 }
1218
1219 dp[distance_index(i, j, cols)] = best;
1220 }
1221 }
1222
1223 Ok(dp[distance_index(left.len(), right.len(), cols)])
1224}
1225
1226#[cfg(test)]
1227mod tests {
1228 use super::*;
1229
1230 fn symbols_to_string(symbols: &[Symbol]) -> String {
1231 symbols
1232 .iter()
1233 .map(|&symbol| char::from_u32(symbol).expect("test symbol should be a char"))
1234 .collect()
1235 }
1236
1237 fn string_distance(left: &str, right: &str) -> usize {
1238 distance(&Lattice::from_paths([left]), &Lattice::from_paths([right]))
1239 }
1240
1241 fn string_damerau_distance(left: &str, right: &str) -> usize {
1242 damerau_distance(&Lattice::from_paths([left]), &Lattice::from_paths([right]))
1243 }
1244
1245 fn assert_close(left: f64, right: f64) {
1246 assert!((left - right).abs() < f64::EPSILON);
1247 }
1248
1249 #[test]
1250 fn try_path_constructors_report_invalid_inputs() {
1251 assert!(matches!(
1252 Lattice::try_from_paths(std::iter::empty::<&str>()),
1253 Err(LatticeError::Empty)
1254 ));
1255 assert!(matches!(
1256 Lattice::try_from_paths(["", "a"]),
1257 Err(LatticeError::MixedEmptyAndNonEmptyPaths)
1258 ));
1259 assert!(matches!(
1260 Lattice::try_from_symbol_paths_compact([Vec::<Symbol>::new(), vec![1]]),
1261 Err(LatticeError::MixedEmptyAndNonEmptyPaths)
1262 ));
1263 }
1264
1265 #[test]
1266 fn linear_lattice_matches_levenshtein_distance() {
1267 assert_eq!(string_distance("kitten", "sitting"), 3);
1268 assert_eq!(string_distance("insat", "insatu"), 1);
1269 assert_eq!(string_distance("abc", "abc"), 0);
1270 assert_eq!(string_distance("abc", "axc"), 1);
1271 }
1272
1273 #[test]
1274 fn normalized_similarity_uses_max_length() {
1275 assert_close(
1276 normalized_similarity_from_distance(1, "abc".chars().count(), "adc".chars().count()),
1277 2.0 / 3.0,
1278 );
1279 assert_close(normalized_similarity_str("abc", "adc"), 2.0 / 3.0);
1280 assert_eq!(normalized_similarity_str("", ""), 1.0);
1281 assert_eq!(normalized_similarity_from_distance(4, 1, 2), 0.0);
1282 }
1283
1284 #[test]
1285 fn parallel_paths_take_minimum_distance() {
1286 let left = Lattice::from_paths(["insat"]);
1287 let right = Lattice::from_paths(["insatu", "insat", "zzzzz"]);
1288
1289 assert_eq!(distance(&left, &right), 0);
1290 }
1291
1292 #[test]
1293 fn fallible_distance_apis_match_convenience_apis() {
1294 let left = Lattice::from_paths(["abcd"]);
1295 let right = Lattice::from_paths(["acbd"]);
1296
1297 assert_eq!(
1298 try_distance(&left, &right).unwrap(),
1299 distance(&left, &right)
1300 );
1301 assert_eq!(
1302 try_damerau_distance(&left, &right).unwrap(),
1303 damerau_distance(&left, &right)
1304 );
1305 assert_eq!(
1306 try_distance_with_trace(&left, &right).unwrap(),
1307 distance_with_trace(&left, &right)
1308 );
1309 assert_eq!(
1310 try_within_distance(&left, &right, 2).unwrap(),
1311 within_distance(&left, &right, 2)
1312 );
1313 assert_eq!(
1314 try_within_damerau_distance(&left, &right, 1).unwrap(),
1315 within_damerau_distance(&left, &right, 1)
1316 );
1317 }
1318
1319 #[test]
1320 fn fallible_distance_apis_reject_large_matrices() {
1321 let node_count = 4001;
1322 let lattice = Lattice::from_edges(node_count, 0, node_count - 1, Vec::new()).unwrap();
1323
1324 assert!(matches!(
1325 try_distance(&lattice, &lattice),
1326 Err(DistanceError::MatrixTooLarge {
1327 rows: 4001,
1328 cols: 4001,
1329 max_cells: MAX_DISTANCE_MATRIX_CELLS,
1330 })
1331 ));
1332 assert!(matches!(
1333 try_damerau_distance(&lattice, &lattice),
1334 Err(DistanceError::MatrixTooLarge { .. })
1335 ));
1336 assert!(matches!(
1337 try_distance_with_trace(&lattice, &lattice),
1338 Err(DistanceError::MatrixTooLarge { .. })
1339 ));
1340 assert!(matches!(
1341 try_within_distance(&lattice, &lattice, 1),
1342 Err(DistanceError::MatrixTooLarge { .. })
1343 ));
1344 assert!(matches!(
1345 try_within_damerau_distance(&lattice, &lattice, 1),
1346 Err(DistanceError::MatrixTooLarge { .. })
1347 ));
1348 }
1349
1350 #[test]
1351 fn trace_uses_lower_matrix_limit_than_plain_distance() {
1352 let node_count = 1415;
1353 let lattice = Lattice::from_edges(node_count, 0, node_count - 1, Vec::new()).unwrap();
1354
1355 assert!(try_distance(&lattice, &lattice).is_ok());
1356 assert!(matches!(
1357 try_distance_with_trace(&lattice, &lattice),
1358 Err(DistanceError::MatrixTooLarge {
1359 rows: 1415,
1360 cols: 1415,
1361 max_cells: MAX_TRACE_MATRIX_CELLS,
1362 })
1363 ));
1364 }
1365
1366 #[test]
1367 fn fallible_arc_accessors_report_out_of_range_nodes() {
1368 let lattice = Lattice::from_paths(["ab"]);
1369
1370 assert_eq!(
1371 lattice.try_incoming_arcs(999).map(|arcs| arcs.count()),
1372 None
1373 );
1374 assert_eq!(
1375 lattice.try_outgoing_arcs(999).map(|arcs| arcs.count()),
1376 None
1377 );
1378 assert_eq!(
1379 lattice.try_outgoing_arcs(0).map(|arcs| arcs.count()),
1380 Some(1)
1381 );
1382 }
1383
1384 #[test]
1385 fn trace_free_distance_matches_trace_distance() {
1386 let left = Lattice::from_paths(["insat", "insatu"]);
1387 let right = Lattice::from_paths(["inzatu", "insatsu"]);
1388
1389 assert_eq!(
1390 distance(&left, &right),
1391 distance_with_trace(&left, &right).distance
1392 );
1393 }
1394
1395 #[test]
1396 fn compact_paths_share_prefix_nodes() {
1397 let lattice = Lattice::from_symbol_paths_compact([
1398 "chadougu"
1399 .chars()
1400 .map(|ch| ch as Symbol)
1401 .collect::<Vec<_>>(),
1402 "chadoogu"
1403 .chars()
1404 .map(|ch| ch as Symbol)
1405 .collect::<Vec<_>>(),
1406 ]);
1407
1408 assert_eq!(distance(&lattice, &Lattice::from_paths(["chadougu"])), 0);
1409 assert_eq!(distance(&lattice, &Lattice::from_paths(["chadoogu"])), 0);
1410 assert!(lattice.node_count() < Lattice::from_paths(["chadougu", "chadoogu"]).node_count());
1411 }
1412
1413 #[test]
1414 fn compact_paths_share_equivalent_suffix_nodes() {
1415 let lattice = Lattice::from_symbol_paths_compact([
1416 "xab".chars().map(|ch| ch as Symbol).collect::<Vec<_>>(),
1417 "yab".chars().map(|ch| ch as Symbol).collect::<Vec<_>>(),
1418 ]);
1419
1420 assert_eq!(distance(&lattice, &Lattice::from_paths(["xab"])), 0);
1421 assert_eq!(distance(&lattice, &Lattice::from_paths(["yab"])), 0);
1422 assert_eq!(distance(&lattice, &Lattice::from_paths(["zab"])), 1);
1423 assert!(lattice.node_count() < Lattice::from_paths(["xab", "yab"]).node_count());
1424 }
1425
1426 #[test]
1427 fn compact_paths_preserve_prefix_words() {
1428 let lattice = Lattice::from_symbol_paths_compact([
1429 "a".chars().map(|ch| ch as Symbol).collect::<Vec<_>>(),
1430 "ab".chars().map(|ch| ch as Symbol).collect::<Vec<_>>(),
1431 ]);
1432
1433 assert_eq!(distance(&lattice, &Lattice::from_paths(["a"])), 0);
1434 assert_eq!(distance(&lattice, &Lattice::from_paths(["ab"])), 0);
1435 }
1436
1437 #[test]
1438 fn distance_with_trace_returns_best_path_pair() {
1439 let left = Lattice::from_paths(["chadougu"]);
1440 let right = Lattice::from_paths(["tyadougu", "chadougu"]);
1441
1442 let trace = distance_with_trace(&left, &right);
1443
1444 assert_eq!(trace.distance, 0);
1445 assert_eq!(symbols_to_string(&trace.left_symbols()), "chadougu");
1446 assert_eq!(symbols_to_string(&trace.right_symbols()), "chadougu");
1447 assert!(trace.steps.iter().all(|step| step.op == EditOp::Match));
1448 }
1449
1450 #[test]
1451 fn trace_includes_insertions_and_deletions() {
1452 let trace = distance_with_trace(
1453 &Lattice::from_paths(["insat"]),
1454 &Lattice::from_paths(["insatu"]),
1455 );
1456
1457 assert_eq!(trace.distance, 1);
1458 assert_eq!(symbols_to_string(&trace.left_symbols()), "insat");
1459 assert_eq!(symbols_to_string(&trace.right_symbols()), "insatu");
1460 assert_eq!(trace.steps.last().map(|step| step.op), Some(EditOp::Insert));
1461 }
1462
1463 #[test]
1464 fn threshold_check_uses_distance() {
1465 let left = Lattice::from_paths(["insat"]);
1466 let right = Lattice::from_paths(["insatu"]);
1467
1468 assert!(within_distance(&left, &right, 1));
1469 assert!(!within_distance(&left, &right, 0));
1470 }
1471
1472 #[test]
1473 fn threshold_check_prunes_but_preserves_lattice_paths() {
1474 let left = Lattice::from_paths(["chadougu", "tyadougu"]);
1475 let right = Lattice::from_paths(["chadoogu", "zzzzzzzz"]);
1476
1477 assert_eq!(distance(&left, &right), 1);
1478 assert!(within_distance(&left, &right, 1));
1479 assert!(!within_distance(&left, &right, 0));
1480 }
1481
1482 #[test]
1483 fn linear_lattice_damerau_matches_string_damerau_distance() {
1484 for (left, right) in [
1485 ("ca", "ac"),
1486 ("abc", "acb"),
1487 ("abcdef", "abcedf"),
1488 ("moine", "mione"),
1489 ("マトリッツォ", "マリトッツォ"),
1490 ] {
1491 assert_eq!(
1492 string_damerau_distance(left, right),
1493 damerau_levenshtein_str(left, right),
1494 "{left:?} / {right:?}"
1495 );
1496 }
1497 }
1498
1499 #[test]
1500 fn lattice_damerau_takes_transposition_across_candidate_paths() {
1501 let left = Lattice::from_paths(["abc", "axc"]);
1502 let right = Lattice::from_paths(["acb"]);
1503
1504 assert_eq!(distance(&left, &right), 2);
1505 assert_eq!(damerau_distance(&left, &right), 1);
1506 }
1507
1508 #[test]
1509 fn lattice_damerau_supports_branched_two_arc_transposition() {
1510 let left = Lattice::from_edges(
1511 4,
1512 0,
1513 3,
1514 vec![
1515 Arc::new(0, 1, 'a' as Symbol),
1516 Arc::new(0, 1, 'x' as Symbol),
1517 Arc::new(1, 3, 'b' as Symbol),
1518 Arc::new(1, 3, 'y' as Symbol),
1519 ],
1520 )
1521 .unwrap();
1522 let right = Lattice::from_paths(["ba"]);
1523
1524 assert_eq!(damerau_distance(&left, &right), 1);
1525 }
1526
1527 #[test]
1528 fn threshold_damerau_check_uses_lattice_damerau_distance() {
1529 let left = Lattice::from_paths(["abc", "axc"]);
1530 let right = Lattice::from_paths(["acb"]);
1531
1532 assert!(within_damerau_distance(&left, &right, 1));
1533 assert!(!within_damerau_distance(&left, &right, 0));
1534 }
1535
1536 #[test]
1537 fn from_edges_rejects_non_topological_arcs() {
1538 let result = Lattice::from_edges(2, 0, 1, vec![Arc::new(1, 0, 'a' as Symbol)]);
1539
1540 assert!(matches!(
1541 result,
1542 Err(LatticeError::InvalidArcOrder { src: 1, dst: 0 })
1543 ));
1544 }
1545
1546 #[test]
1547 fn surface_levenshtein_counts_unicode_chars() {
1548 assert_eq!(levenshtein_str("kitten", "sitting"), 3);
1549 assert_eq!(levenshtein_str("いんさt", "印刷"), 4);
1550 assert_eq!(levenshtein_str("マトリッツォ", "マリトッツォ"), 2);
1551 }
1552
1553 #[test]
1554 fn surface_damerau_counts_adjacent_transposition() {
1555 assert_eq!(damerau_levenshtein_str("ca", "ac"), 1);
1556 assert_eq!(try_damerau_levenshtein_str("ca", "ac").unwrap(), 1);
1557 assert_eq!(damerau_levenshtein_str("マトリッツォ", "マリトッツォ"), 1);
1558 assert_eq!(damerau_levenshtein_str("いんさt", "印刷"), 4);
1559 }
1560}