mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-31 12:47:43 +00:00
feat(web): add option to disable transpose edits
Tokens in the active context should not be transposable, so this commit adds an option that permits disabling transpose edits during edit-distance calculation. Build-bot: skip build:web Test-bot: skip
This commit is contained in:
parent
7ebfbfe54e
commit
f7e291a7c3
2 changed files with 73 additions and 32 deletions
|
|
@ -171,19 +171,16 @@ export function computeDistance<TUnit, TOpSet, TDistanceCalc extends ClassicalDi
|
|||
match: TUnit[]
|
||||
): TDistanceCalc {
|
||||
for(let i = 0; i < input.length; i++) {
|
||||
buffer = buffer.addInputChar(input[i]);
|
||||
buffer = buffer.addInputChar(input[i]) as TDistanceCalc;
|
||||
}
|
||||
|
||||
for(let j = 0; j < match.length; j++) {
|
||||
buffer = buffer.addMatchChar(match[j]);
|
||||
buffer = buffer.addMatchChar(match[j]) as TDistanceCalc;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialization options for ClassicalDistanceCalculation
|
||||
*/
|
||||
export interface DistanceCalcOptions {
|
||||
/**
|
||||
* When set to true, transpose edits will not be considered.
|
||||
|
|
@ -252,6 +249,8 @@ export class ClassicalDistanceCalculation<
|
|||
*/
|
||||
private _diagonalWidth: number;
|
||||
|
||||
readonly allowsTransposes: boolean;
|
||||
|
||||
// The sequence of characters input so far.
|
||||
private readonly _inputSequence: TUnit[] = [];
|
||||
private readonly _matchSequence: TUnit[] = [];
|
||||
|
|
@ -291,12 +290,14 @@ export class ClassicalDistanceCalculation<
|
|||
this._inputSequence = other._inputSequence.slice(0);
|
||||
this._matchSequence = other._matchSequence.slice(0);
|
||||
this._diagonalWidth = other._diagonalWidth;
|
||||
this.allowsTransposes = other.allowsTransposes;
|
||||
} else {
|
||||
const options = param1 ?? { };
|
||||
// We start at 2 as default for now as a naive workaround for multi-char
|
||||
// transform limitations; we don't want to dynamically change this a lot
|
||||
// during calculations.
|
||||
this._diagonalWidth = options.diagonalWidth ?? 2;
|
||||
this.allowsTransposes = !options.noTransposes;
|
||||
this.resolvedDistances = [];
|
||||
}
|
||||
}
|
||||
|
|
@ -494,7 +495,9 @@ export class ClassicalDistanceCalculation<
|
|||
protected _buildPath(pathBuilder?: PathBuilder<TUnit, TOpSet>): EditTuple<TOpSet>[][] {
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue