Module: Kotoshu::Algorithms::EditDistance
- Defined in:
- lib/kotoshu/algorithms/edit_distance.rb
Overview
Damerau-Levenshtein edit distance.
Counts the minimum number of operations (insertion, deletion, substitution, or transposition of adjacent characters) needed to transform one string into another. The transposition extension distinguishes this from plain Levenshtein — a transposition (e.g. "teh" → "the") costs 1 instead of 2.
Extracted from EditDistanceStrategy so that the algorithm is reusable independent of the strategy pipeline and testable without send-to-private.
Class Method Summary collapse
-
.distance(str1, str2) ⇒ Integer
Compute the Damerau-Levenshtein distance between two strings.
-
.distance_with_threshold(str1, str2, threshold) ⇒ Integer?
Compute edit distance with early-exit threshold.
Class Method Details
.distance(str1, str2) ⇒ Integer
Compute the Damerau-Levenshtein distance between two strings.
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/kotoshu/algorithms/edit_distance.rb', line 24 def distance(str1, str2) return str2.length if str1.empty? return str1.length if str2.empty? len1 = str1.length len2 = str2.length d = Array.new(len1 + 1) { Array.new(len2 + 1, 0) } (0..len1).each { |i| d[i][0] = i } (0..len2).each { |j| d[0][j] = j } (1..len1).each do |i| (1..len2).each do |j| cost = str1[i - 1] == str2[j - 1] ? 0 : 1 d[i][j] = [ d[i - 1][j] + 1, # deletion d[i][j - 1] + 1, # insertion d[i - 1][j - 1] + cost # substitution ].min next unless i > 1 && j > 1 && str1[i - 1] == str2[j - 2] && str1[i - 2] == str2[j - 1] d[i][j] = [d[i][j], d[i - 2][j - 2] + 1].min end end d[len1][len2] end |
.distance_with_threshold(str1, str2, threshold) ⇒ Integer?
Compute edit distance with early-exit threshold.
Returns nil when the true distance exceeds threshold. Uses
the row-minimum early-termination technique: after each DP
row is computed, if every cell in the row exceeds threshold,
the final distance must exceed threshold (the last-row cell
is bounded below by the row minimum) — so we bail without
computing the rest.
Combined with the length pre-filter (|len1-len2| > threshold implies distance > threshold), this prunes clearly-different pairs in O(threshold * min(len1, len2)) instead of the full O(len1 * len2) Damerau-Levenshtein DP.
Note: the full matrix is kept (not 2-row DP) because the Damerau transposition step needs d[j-2] which a 2-row implementation doesn't retain. The early termination is the primary win, not memory reduction.
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
# File 'lib/kotoshu/algorithms/edit_distance.rb', line 80 def distance_with_threshold(str1, str2, threshold) return 0 if str1 == str2 # The length filter must run before the empty-string shortcuts: # an empty str1 gives distance str2.length, which still has to # honor the nil-above-threshold contract. return nil if (str1.length - str2.length).abs > threshold return str2.length if str1.empty? return str1.length if str2.empty? len1 = str1.length len2 = str2.length d = Array.new(len1 + 1) { Array.new(len2 + 1, 0) } (0..len1).each { |i| d[i][0] = i } (0..len2).each { |j| d[0][j] = j } (1..len1).each do |i| row_min = Float::INFINITY (1..len2).each do |j| cost = str1[i - 1] == str2[j - 1] ? 0 : 1 d[i][j] = [ d[i - 1][j] + 1, # deletion d[i][j - 1] + 1, # insertion d[i - 1][j - 1] + cost # substitution ].min # Damerau transposition (needs d[i-2][j-2] which is why # we keep the full matrix). if i > 1 && j > 1 && str1[i - 1] == str2[j - 2] && str1[i - 2] == str2[j - 1] d[i][j] = [d[i][j], d[i - 2][j - 2] + 1].min end row_min = d[i][j] if d[i][j] < row_min end # Row minimum > threshold ⇒ final answer > threshold. return nil if row_min > threshold end result = d[len1][len2] result <= threshold ? result : nil end |