From cdd2028e52ab588eb6bf1cefa9ec90805ed532af Mon Sep 17 00:00:00 2001 From: greymoth-jp Date: Mon, 29 Jun 2026 18:13:31 +0900 Subject: [PATCH] fix(distance): keep Damerau-Levenshtein symmetric for repeated characters The unrestricted Damerau-Levenshtein path updated lastRowMap (the da table) inside the inner column loop, so lastRowMap[targetElement] could resolve to the current row and feed a negative (row - lastRowMatch - 1) term into the transposition cost. That produced wrong, order-dependent distances such as DamerauLevenshteinDistance('0,1,10,11', '0,11,110,111') === 0. Update the table once per row, after the inner loop, so the transposition lookup only references earlier rows. Fixes #757. --- lib/natural/distance/levenshtein_distance.js | 13 ++++++++----- spec/damerau_levenshtein_spec.ts | 5 +++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/natural/distance/levenshtein_distance.js b/lib/natural/distance/levenshtein_distance.js index 291d70876..a2ce32b01 100644 --- a/lib/natural/distance/levenshtein_distance.js +++ b/lib/natural/distance/levenshtein_distance.js @@ -218,13 +218,16 @@ function levenshteinDistance (source, target, options) { distanceMatrix[row][column] = { cost: minCostParent.cost, parentCell: minCostParent.coordinates } - if (isUnrestrictedDamerau) { - lastRowMap[sourceElement] = row - if (sourceElement === targetElement) { - lastColMatch = column - } + if (isUnrestrictedDamerau && sourceElement === targetElement) { + lastColMatch = column } } + // da[c]: last row in which source character c occurred. Update after the + // inner loop so the transposition lookup only references earlier rows; the + // per-column update gave wrong distances for repeated characters (issue #757). + if (isUnrestrictedDamerau) { + lastRowMap[source[row - 1]] = row + } } if (!options.search) { diff --git a/spec/damerau_levenshtein_spec.ts b/spec/damerau_levenshtein_spec.ts index 9dcbca670..140425718 100644 --- a/spec/damerau_levenshtein_spec.ts +++ b/spec/damerau_levenshtein_spec.ts @@ -25,6 +25,11 @@ describe('DamerauLevenshtein', function () { expect(DamerauLevenshteinDistance('CA', 'ABC')).toBe(2) expect(DamerauLevenshteinDistance('a cat', 'a abct')).toBe(2) }) + + it('should stay symmetric for strings with repeated characters (issue #757)', function () { + expect(DamerauLevenshteinDistance('0,1,10,11', '0,11,110,111')).toBe(3) + expect(DamerauLevenshteinDistance('0,11,110,111', '0,1,10,11')).toBe(3) + }) }) describe('options.restricted = true', function () {