diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/classical-calculation.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/classical-calculation.ts index 6964673fad..8040d6f9c0 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/classical-calculation.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/classical-calculation.ts @@ -171,19 +171,16 @@ export function computeDistance): EditTuple[][] { pathBuilder = pathBuilder ?? new PathBuilder(this, []); pathBuilder.addEdgeFinder(findBaseEdges); - pathBuilder.addEdgeFinder(findTransposeEdges); + if(pathBuilder.calc.allowsTransposes) { + pathBuilder.addEdgeFinder(findTransposeEdges); + } pathBuilder.backtracePath(this.inputSequence.length - 1, this.matchSequence.length - 1, []); return pathBuilder.validPaths; } @@ -512,7 +515,7 @@ export class ClassicalDistanceCalculation< var deletionCost: number = deleteCost || buffer.getCostAt(r-1, c) + 1; // If set meaningfully, will never equal zero. var transpositionCost: number = Number.MAX_VALUE - if(r > 0 && c > 0) { // bypass when transpositions are known to be impossible. + if(buffer.allowsTransposes && r > 0 && c > 0) { // bypass when transpositions are known to be impossible. let [lastInputIndex, lastMatchIndex] = getTransposeParent(buffer, r, c); transpositionCost = buffer.getCostAt(lastInputIndex-1, lastMatchIndex-1) + (r - lastInputIndex - 1) + 1 + (c - lastMatchIndex - 1); } @@ -647,17 +650,19 @@ export class ClassicalDistanceCalculation< // We propagate the new added cost (via insertion) to the old left-most cell, which is one to our right. ClassicalDistanceCalculation.propagateUpdateFrom(returnBuffer, r, c+1, addedCost+1, 0); - // Only possible if insertions are also possible AND more conditions are met. - // cells (r+2, * > c+2): new transposition source - let transposeRow = r+2; - if(r+2 < this.inputSequence.length) { // Row to check for transposes must exist. - let rowChar = returnBuffer.inputSequence[r+1]; - // First possible match in input could be at index c + 2, which adjusts col c+2's cost. Except that entry in r+2 - // doesn't exist yet - so we start with c+3 instead. - forPossibleTranspositionsInDiagonal(c + 3, rowChar, returnBuffer.matchSequence, function(axisIndex, diagIndex) { - // Because (r+2, c+3) is root, not (r+2, c+2). Min cost of 2. - ClassicalDistanceCalculation.propagateUpdateFrom(returnBuffer, transposeRow, axisIndex, addedCost + diagIndex + 2, diagIndex); - }); + if(this.allowsTransposes) { + // Only possible if insertions are also possible AND more conditions are met. + // cells (r+2, * > c+2): new transposition source + let transposeRow = r+2; + if(r+2 < this.inputSequence.length) { // Row to check for transposes must exist. + let rowChar = returnBuffer.inputSequence[r+1]; + // First possible match in input could be at index c + 2, which adjusts col c+2's cost. Except that entry in r+2 + // doesn't exist yet - so we start with c+3 instead. + forPossibleTranspositionsInDiagonal(c + 3, rowChar, returnBuffer.matchSequence, function(axisIndex, diagIndex) { + // Because (r+2, c+3) is root, not (r+2, c+2). Min cost of 2. + ClassicalDistanceCalculation.propagateUpdateFrom(returnBuffer, transposeRow, axisIndex, addedCost + diagIndex + 2, diagIndex); + }); + } } } } @@ -683,18 +688,20 @@ export class ClassicalDistanceCalculation< // We propagate the new added cost (via deletion) to the old right-most cell, which is one to our right. ClassicalDistanceCalculation.propagateUpdateFrom(returnBuffer, r+1, c, addedCost + 1, 2 * this.diagonalWidth); - // Only possible if deletions are also possible AND more conditions are met. - // cells(* > r+2, c+2): new transposition source - let transposeCol = c+2; - if(c+2 < this.matchSequence.length) { // Row to check for transposes must exist. - let colChar = returnBuffer.matchSequence[r+1]; - // First possible match in input could be at index r + 2, which adjusts row r+2's cost. Except that entry in c+2 - // doesn't exist yet - so we start with r+3 instead. - forPossibleTranspositionsInDiagonal(r+3, colChar, returnBuffer.inputSequence, function(axisIndex, diagIndex) { - let diagColIndex = 2 * (returnBuffer.diagonalWidth - 1) - diagIndex; - // Because (r+3, c+2) is root, not (r+2, c+2). Min cost of 2. - ClassicalDistanceCalculation.propagateUpdateFrom(returnBuffer, axisIndex, transposeCol, addedCost + diagIndex + 2, diagColIndex); - }); + if(this.allowsTransposes) { + // Only possible if deletions are also possible AND more conditions are met. + // cells(* > r+2, c+2): new transposition source + let transposeCol = c+2; + if(c+2 < this.matchSequence.length) { // Row to check for transposes must exist. + let colChar = returnBuffer.matchSequence[r+1]; + // First possible match in input could be at index r + 2, which adjusts row r+2's cost. Except that entry in c+2 + // doesn't exist yet - so we start with r+3 instead. + forPossibleTranspositionsInDiagonal(r+3, colChar, returnBuffer.inputSequence, function(axisIndex, diagIndex) { + let diagColIndex = 2 * (returnBuffer.diagonalWidth - 1) - diagIndex; + // Because (r+3, c+2) is root, not (r+2, c+2). Min cost of 2. + ClassicalDistanceCalculation.propagateUpdateFrom(returnBuffer, axisIndex, transposeCol, addedCost + diagIndex + 2, diagColIndex); + }); + } } } } diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/classical-calculation.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/classical-calculation.tests.ts index c61f4d89b9..2b3d827947 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/classical-calculation.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/classical-calculation.tests.ts @@ -18,8 +18,8 @@ export function prettyPrintMatrix(matrix: number[][]) { } } -function compute(input: string, match: string, mode?: string, bandSize?: number) { - let buffer = new ClassicalDistanceCalculation({diagonalWidth: bandSize || 1}); +function compute(input: string, match: string, mode?: string, bandSize?: number, allowTransposes: boolean = true) { + let buffer = new ClassicalDistanceCalculation({noTransposes: !allowTransposes, diagonalWidth: bandSize || 1}); /* SUPPORTED MODES: * "InputThenMatch" // adds all input chars, then all match chars. @@ -262,6 +262,15 @@ describe('Classical Damerau-Levenshtein edit-distance calculation', function() { assert.equal(compute("jellyifhs", "jellyfish", "MatchThenInput").getFinalCost(), 2); }); + it("'jellyifhs' -> 'jellyfish' = 2 (no transposes)", function() { + // edits when without transposes: + // delete i (jellyfhs) + // substitute h -> i (jellyfis) + // insert h at end (jellyfish) + assert.equal(compute("jellyifhs", "jellyfish", "InputThenMatch", 3, false).getFinalCost(), 3); + assert.equal(compute("jellyifhs", "jellyfish", "MatchThenInput", 3, false).getFinalCost(), 3); + }); + it("'aadddres' -> 'address' = 3", function() { // If diagonal set to '1', cost is reported as 4. assert.equal(compute("aadddres", "address", "InputThenMatch").getFinalCost(), 3); // Error - is returning 5, not 4 (which would be correct for current implementation state) @@ -679,6 +688,31 @@ describe('Classical Damerau-Levenshtein edit-distance calculation', function() { assert.deepEqual(viablePaths[0], editSequence); }); + it("'jellyifhs' -> 'jellyfish' (no transposes)", function() { + // edits when without transposes: + // delete i (jellyfhs) + // substitute h -> i (jellyfis) + // insert h at end (jellyfish) + const buffer = compute("jellyifhs", "jellyfish", "InputThenMatch", 3, false); + + const viablePaths = buffer.editPath(); + assert.isAtLeast(viablePaths.length, 1); + + // Assert: the following path exists as one option. + assert.includeDeepMembers(viablePaths, [[ + { op: 'match', input: 0, match: 0}, + { op: 'match', input: 1, match: 1}, + { op: 'match', input: 2, match: 2}, + { op: 'match', input: 3, match: 3}, + { op: 'match', input: 4, match: 4}, + { op: 'delete', input: 5}, + { op: 'match', input: 6, match: 5}, + { op: 'substitute', input: 7, match: 6}, + { op: 'match', input: 8, match: 7}, + { op: 'insert', match: 8} + ]]); + }); + it("'then' -> 'their'", function() { let buffer = compute("then", "their");