Merge pull request #14697 from keymanapp/change/web/edit-path-edge-data

change(web): edit path edges should report alignment indices instead of texts 🚂
This commit is contained in:
Joshua Horton 2025-09-23 23:25:11 +07:00 committed by GitHub
commit 3dba25cf7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 638 additions and 168 deletions

View file

@ -9,7 +9,8 @@
*/
import { SENTINEL_CODE_UNIT } from '@keymanapp/models-templates';
import { ClassicalDistanceCalculation, EditOperation } from "./classical-calculation.js";
import { ClassicalDistanceCalculation, EditTuple } from "./classical-calculation.js";
import { ExtendedEditOperation } from './segmentable-calculation.js';
/**
* Represents token-count values resulting from an alignment attempt between two
@ -26,12 +27,20 @@ export type ContextStateAlignment = {
*
* The edit path does not include actual user text and is sanitized.
*/
editPath: EditOperation[];
editPath: EditTuple<ExtendedEditOperation>[];
} | {
/**
* Denotes whether or not alignment is possible between two contexts.
*/
canAlign: true,
/**
* Indicates the edit path that could not be handled. (Useful for error reporting)
*
* The edit path does not include actual user text and is sanitized.
*/
editPath: EditTuple<ExtendedEditOperation>[];
/**
* Notes the number of tokens added to the head of the 'incoming'/'new' context
* of the contexts being aligned. If negative, the incoming context deleted
@ -88,7 +97,7 @@ export type ContextStateAlignment = {
* @param editPath
* @returns
*/
export function getEditPathLastMatch(editPath: EditOperation[], forAppliedSuggestion?: boolean) {
export function getEditPathLastMatch(editPath: ExtendedEditOperation[], forAppliedSuggestion?: boolean) {
// Assertion: for a long context, the bulk of the edit path should be a
// continuous block of 'match' entries. If there's anything but a substitution
// in the middle, we have a context mismatch.
@ -261,7 +270,7 @@ export function computeAlignment(
const failure: ContextStateAlignment = {
canAlign: false,
editPath
editPath: editPaths[0]
};
// Special case: new context bootstrapping - first token often substitutes.
@ -291,6 +300,7 @@ export function computeAlignment(
return {
canAlign: true,
editPath: editPaths[0],
matchLength: matchCount,
leadTokenShift: 0,
leadEditLength: 0,
@ -344,6 +354,7 @@ export function computeAlignment(
if(firstMatch == 0 && lastMatch == editPath.length - 1) {
return {
canAlign: true,
editPath: editPaths[0],
leadTokenShift: 0,
leadEditLength: 0,
matchLength,
@ -418,6 +429,7 @@ export function computeAlignment(
return {
canAlign: true,
editPath: editPaths[0],
// leadTokensRemoved represents the number of tokens that must be removed from the base context
// when aligning the contexts. Externally, it's more helpful to think in terms of the count added
// to the incoming context.

View file

@ -9,13 +9,13 @@ export type EditOperation = 'insert' | 'delete' | 'match' | 'substitute' | 'tran
* Represents individual nodes on calculated edit paths and the relevant
* edited value(s) at each step.
*/
export interface EditTuple<TUnit, TOpSet = EditOperation> {
export interface EditTuple<TOpSet = EditOperation> {
/** The edit operation taking place at this position in the edit path */
op: TOpSet | EditOperation,
/** The value for the `input` source at this position in the edit path */
input?: TUnit,
/** The value for the `match` source at this position in the edit path */
match?: TUnit
/** The index for the `input` source at this position in the edit path */
input?: number,
/** The index for the `match` source at this position in the edit path */
match?: number
}
// Implemented externally from the class so that it may be more easily
@ -53,7 +53,7 @@ export interface EditTuple<TUnit, TOpSet = EditOperation> {
* visualization
* @returns
*/
export function visualizeCalculation<TUnit, TOpSet>(calc: ClassicalDistanceCalculation<TUnit, TOpSet>, path?: EditTuple<TUnit, TOpSet>[]) {
export function visualizeCalculation<TUnit, TOpSet>(calc: ClassicalDistanceCalculation<TUnit, TOpSet>, path?: EditTuple<TOpSet>[]) {
path = (path ?? calc.editPath()[0]).slice();
const inputs = calc.inputSequence.map(i => '' + i);
@ -107,27 +107,29 @@ export function visualizeCalculation<TUnit, TOpSet>(calc: ClassicalDistanceCalcu
rowStr += " ".repeat(sparseCount > 0 ? sparseCount : 1);
let edits: string[] = [];
const printEdit = (edit: EditTuple<TUnit, TOpSet>) => {
const printEdit = (edit: EditTuple<TOpSet>) => {
let tokenText: string;
const input = calc.inputSequence[edit.input];
const match = calc.matchSequence[edit.match];
switch(edit.op) {
case 'delete':
case 'transpose-delete':
tokenText = `'${edit.input}'`;
tokenText = `'${input}'`;
break;
case 'insert':
case 'transpose-insert':
tokenText = `'${edit.match}'`;
tokenText = `'${match}'`;
break;
case 'substitute':
case 'match':
case 'split':
case 'merge':
const op = edit.op == 'match' ? '==' : '=>';
tokenText = tokenText || `'${edit.input}' ${op} '${edit.match}'`;
tokenText = tokenText || `'${input}' ${op} '${match}'`;
break;
// transpose-start, transpose-end
default:
tokenText = `'${edit.input}' vs '${edit.match}'`;
tokenText = `'${input}' vs '${match}'`;
}
return `${edit.op}(${tokenText})`;
}
@ -337,7 +339,7 @@ export class ClassicalDistanceCalculation<TUnit = string, TOpSet = EditOperation
* @param row
* @param col
*/
public editPath(): EditTuple<TUnit, TOpSet>[][] {
public editPath(): EditTuple<TOpSet>[][] {
const results = this._buildPath();
if(results.length <= 1) {
@ -415,7 +417,7 @@ export class ClassicalDistanceCalculation<TUnit = string, TOpSet = EditOperation
* @param row
* @param col
*/
protected _buildPath(pathBuilder?: PathBuilder<TUnit, TOpSet>): EditTuple<TUnit, TOpSet>[][] {
protected _buildPath(pathBuilder?: PathBuilder<TUnit, TOpSet>): EditTuple<TOpSet>[][] {
pathBuilder = pathBuilder ?? new PathBuilder(this, []);
pathBuilder.addEdgeFinder(findBaseEdges);
pathBuilder.addEdgeFinder(findTransposeEdges);
@ -825,21 +827,21 @@ export function findTransposeEdges<TUnit, TOpSet>(
// This transposition includes either 'transpose-insert' or 'transpose-delete' operations.
let i = row;
let m = col;
let ops: EditTuple<TUnit, TOpSet>[] = [];
let ops: EditTuple<TOpSet>[] = [];
if(lastInputIndex != row-1) {
let count = row - lastInputIndex;
ops.push({
op: 'transpose-start',
input: calc.inputSequence[i-count],
match: calc.matchSequence[lastMatchIndex]
input: i-count,
match: lastMatchIndex
});
// Intentional fallthrough on 0 - index 0 is covered by 'transpose-end'
// after the if-else.
for(let x=count-1; x > 0; x--) {
ops.push({
op: 'transpose-delete',
input: calc.inputSequence[i-x]
input: i-x
});
}
expectedCost += count-1;
@ -847,15 +849,15 @@ export function findTransposeEdges<TUnit, TOpSet>(
let count = col - lastMatchIndex;
ops.push({
op: 'transpose-start',
input: calc.inputSequence[lastInputIndex],
match: calc.matchSequence[m-count]
input: lastInputIndex,
match: m-count
});
// Intentional fallthrough on 0 - index 0 is covered by 'transpose-end'
// after the if-else.
for(let y=count-1; y > 0; y--) {
ops.push({
op: 'transpose-insert',
match: calc.matchSequence[m-y]
match: m-y
});
}
expectedCost += count - 1;
@ -863,8 +865,8 @@ export function findTransposeEdges<TUnit, TOpSet>(
ops.push({
op: 'transpose-end',
input: calc.inputSequence[i],
match: calc.matchSequence[m]
input: i,
match: m
});
// Double-check our expectations.
@ -899,28 +901,28 @@ export function findBaseEdges<TUnit, TOpSet>(
const insertParentCost = calc.getCostAt(row, col-1);
if(insertParentCost == currentCost - 1) {
pathBuilder.backtracePath(row, col-1, [{op: 'insert', match}]);
pathBuilder.backtracePath(row, col-1, [{op: 'insert', match: col}]);
}
const deleteParentCost = calc.getCostAt(row-1, col);
if(deleteParentCost == currentCost - 1) {
pathBuilder.backtracePath(row-1, col, [{op: 'delete', input}]);
pathBuilder.backtracePath(row-1, col, [{op: 'delete', input: row}]);
}
const substitutionParentCost = calc.getCostAt(row-1, col-1);
if(substitutionParentCost == currentCost - 1) {
pathBuilder.backtracePath(row-1, col-1, [{op: 'substitute', input, match}]);
pathBuilder.backtracePath(row-1, col-1, [{op: 'substitute', input: row, match: col}]);
// VERY IMPORTANT: validate the match. The path can go "off the rails" if
// we don't validate this!
} else if(substitutionParentCost == currentCost && input == match) {
pathBuilder.backtracePath(row-1, col-1, [{op: 'match', input, match}]);
pathBuilder.backtracePath(row-1, col-1, [{op: 'match', input: row, match: col}]);
}
}
export class PathBuilder<TUnit, TOpSet = EditOperation> {
readonly calc: ClassicalDistanceCalculation<TUnit, TOpSet>;
readonly edgeFinders: (EdgeFinder<TUnit, TOpSet>)[];
readonly validPaths: EditTuple<TUnit, TOpSet>[][] = [];
readonly validPaths: EditTuple<TOpSet>[][] = [];
constructor(calc: ClassicalDistanceCalculation<TUnit, TOpSet>, edgeFinders: (EdgeFinder<TUnit, TOpSet>)[]) {
this.calc = calc;
@ -931,11 +933,11 @@ export class PathBuilder<TUnit, TOpSet = EditOperation> {
this.edgeFinders.push(finder);
}
backtracePath(row: number, col: number, recentEdge: EditTuple<TUnit, TOpSet>[]) {
backtracePath(row: number, col: number, recentEdge: EditTuple<TOpSet>[]) {
const calc = this.calc;
// Recursively build the edit path.
let results: EditTuple<TUnit, TOpSet>[][];
let results: EditTuple<TOpSet>[][];
if(row >= 0 && col >= 0) {
const parentBuilder = new PathBuilder<TUnit, TOpSet>(calc, this.edgeFinders);
if(calc.getCostAt(row, col) == Number.MAX_VALUE) {
@ -951,14 +953,14 @@ export class PathBuilder<TUnit, TOpSet = EditOperation> {
// There are initial deletions.
result.push({
op: 'delete',
input: calc.inputSequence[r]
input: r
});
}
for(let c = 0; c <= col; c++) {
// There are initial insertions.
result.push({
op: 'insert',
match: calc.matchSequence[c]
match: c
});
}
}

View file

@ -89,7 +89,7 @@ export class SegmentableDistanceCalculation extends ClassicalDistanceCalculation
throw new Error("Not yet supported for this edit-distance calculation type.");
}
protected _buildPath(pathBuilder?: PathBuilder<string, ExtendedEditOperation>): EditTuple<string, ExtendedEditOperation>[][] {
protected _buildPath(pathBuilder?: PathBuilder<string, ExtendedEditOperation>): EditTuple<ExtendedEditOperation>[][] {
pathBuilder = pathBuilder ?? new PathBuilder<string, ExtendedEditOperation>(this, []);
pathBuilder.addEdgeFinder(findSplitMergeEdges);
super._buildPath(pathBuilder); // actually evaluates the edit-path.
@ -159,22 +159,19 @@ export function findSplitMergeEdges<TOpSet>(
throw new Error("Cannot find path - diagonal width is not large enough.")
}
const input = calc.inputSequence[row];
const match = calc.matchSequence[col];
const [lastMergeIndex, lastSplitIndex] = getMergeSplitParent(calc, row, col);
if(lastMergeIndex != -1) {
const ops: EditTuple<string, ExtendedEditOperation>[] = [];
const ops: EditTuple<ExtendedEditOperation>[] = [];
for(let r = lastMergeIndex; r <= row; r++) {
ops.push({ input: calc.inputSequence[r], match, op: 'merge' });
ops.push({ input: r, match: col, op: 'merge' });
}
pathBuilder.backtracePath(lastMergeIndex - 1, col - 1, ops);
}
if(lastSplitIndex != -1) {
const ops: EditTuple<string, ExtendedEditOperation>[] = [];
const ops: EditTuple<ExtendedEditOperation>[] = [];
for(let c = lastSplitIndex; c <= col; c++) {
ops.push({ input, match: calc.matchSequence[c], op: 'split' });
ops.push({ input: row, match: c, op: 'split' });
}
pathBuilder.backtracePath(row - 1, lastSplitIndex - 1, ops);
}

View file

@ -133,6 +133,13 @@ describe('computeAlignment', () => {
const computedAlignment = computeAlignment(baseContext, newContext, false);
assert.deepEqual(computedAlignment, {
canAlign: true,
editPath: [
{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}
],
leadTokenShift: 0,
leadEditLength: 0,
matchLength: 5,
@ -151,6 +158,13 @@ describe('computeAlignment', () => {
const computedAlignment = computeAlignment(baseContext, newContext, false);
assert.deepEqual(computedAlignment, {
canAlign: true,
editPath: [
{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: 'substitute', input: 4, match: 4}
],
leadTokenShift: 0,
leadEditLength: 0,
matchLength: 4,
@ -172,6 +186,11 @@ describe('computeAlignment', () => {
const computedAlignment = computeAlignment(baseContext, newContext, false, true);
assert.deepEqual(computedAlignment, {
canAlign: true,
editPath: [
{op: 'substitute', input: 0, match: 0},
{op: 'insert', match: 1},
{op: 'insert', match: 2}
],
leadTokenShift: 0,
leadEditLength: 0,
matchLength: 0,
@ -189,7 +208,16 @@ describe('computeAlignment', () => {
];
const computedAlignment = computeAlignment(baseContext, newContext, false);
assert.deepEqual(computedAlignment, {canAlign: false, editPath: ['substitute', 'substitute', 'substitute', 'substitute', 'substitute']});
assert.deepEqual(computedAlignment, {
canAlign: false,
editPath: [
{ op: 'substitute', input: 0, match: 0 },
{ op: 'substitute', input: 1, match: 1 },
{ op: 'substitute', input: 2, match: 2 },
{ op: 'substitute', input: 3, match: 3 },
{ op: 'substitute', input: 4, match: 4 }
]
});
});
it("detects unalignable contexts - too many mismatching tokens", () => {
@ -201,7 +229,16 @@ describe('computeAlignment', () => {
];
const computedAlignment = computeAlignment(baseContext, newContext, false);
assert.deepEqual(computedAlignment, {canAlign: false, editPath: ['substitute', 'substitute', 'match', 'match', 'match']});
assert.deepEqual(computedAlignment, {
canAlign: false,
editPath: [
{op: 'substitute', input: 0, match: 0},
{op: 'substitute', input: 1, match: 1},
{op: 'match', input: 2, match: 2},
{op: 'match', input: 3, match: 3},
{op: 'match', input: 4, match: 4}
],
});
});
it("fails alignment for leading-edge word substitutions", () => {
@ -213,7 +250,16 @@ describe('computeAlignment', () => {
];
const computedAlignment = computeAlignment(baseContext, newContext, false);
assert.deepEqual(computedAlignment, {canAlign: false, editPath: ['substitute', 'match', 'match', 'match', 'match']});
assert.deepEqual(computedAlignment, {
canAlign: false,
editPath: [
{op: 'substitute', 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}
]
});
});
it("fails alignment for small leading-edge word substitutions", () => {
@ -225,7 +271,16 @@ describe('computeAlignment', () => {
];
const computedAlignment = computeAlignment(baseContext, newContext, false);
assert.deepEqual(computedAlignment, {canAlign: false, editPath: ['substitute', 'match', 'match', 'match', 'match']});
assert.deepEqual(computedAlignment, {
canAlign: false,
editPath: [
{op: 'substitute', 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}
]
});
});
it("properly matches and aligns when lead token is modified", () => {
@ -239,6 +294,13 @@ describe('computeAlignment', () => {
const computedAlignment = computeAlignment(baseContext, newContext, false);
assert.deepEqual(computedAlignment, {
canAlign: true,
editPath: [
{op: 'substitute', 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}
],
leadTokenShift: 0,
leadEditLength: 1,
matchLength: 4,
@ -258,6 +320,13 @@ describe('computeAlignment', () => {
const computedAlignment = computeAlignment(baseContext, newContext, false);
assert.deepEqual(computedAlignment, {
canAlign: true,
editPath: [
{op: 'delete', input: 0},
{op: 'match', input: 1, match: 0},
{op: 'match', input: 2, match: 1},
{op: 'match', input: 3, match: 2},
{op: 'match', input: 4, match: 3}
],
leadTokenShift: -1,
leadEditLength: 0,
matchLength: 4,
@ -277,6 +346,13 @@ describe('computeAlignment', () => {
const computedAlignment = computeAlignment(baseContext, newContext, false);
assert.deepEqual(computedAlignment, {
canAlign: true,
editPath: [
{op: 'insert', match: 0},
{op: 'match', input: 0, match: 1},
{op: 'match', input: 1, match: 2},
{op: 'match', input: 2, match: 3},
{op: 'match', input: 3, match: 4}
],
leadTokenShift: 1,
leadEditLength: 0,
matchLength: 4,
@ -296,6 +372,13 @@ describe('computeAlignment', () => {
const computedAlignment = computeAlignment(baseContext, newContext, false);
assert.deepEqual(computedAlignment, {
canAlign: true,
editPath: [
{op: 'delete', input: 0},
{op: 'delete', input: 1},
{op: 'substitute', input: 2, match: 0},
{op: 'match', input: 3, match: 1},
{op: 'match', input: 4, match: 2},
],
leadTokenShift: -2,
leadEditLength: 1,
matchLength: 2,
@ -315,6 +398,13 @@ describe('computeAlignment', () => {
const computedAlignment = computeAlignment(baseContext, newContext, false);
assert.deepEqual(computedAlignment, {
canAlign: true,
editPath: [
{op: 'insert', match: 0},
{op: 'substitute', input: 0, match: 1},
{op: 'match', input: 1, match: 2},
{op: 'match', input: 2, match: 3},
{op: 'match', input: 3, match: 4},
],
leadTokenShift: 1,
leadEditLength: 1,
matchLength: 3,
@ -334,6 +424,14 @@ describe('computeAlignment', () => {
const computedAlignment = computeAlignment(baseContext, newContext, false);
assert.deepEqual(computedAlignment, {
canAlign: true,
editPath: [
{op: 'delete', input: 0},
{op: 'match', input: 1, match: 0},
{op: 'match', input: 2, match: 1},
{op: 'match', input: 3, match: 2},
{op: 'match', input: 4, match: 3},
{op: 'insert', match: 4}
],
leadTokenShift: -1,
leadEditLength: 0,
matchLength: 4,
@ -353,6 +451,13 @@ describe('computeAlignment', () => {
const computedAlignment = computeAlignment(baseContext, newContext, false);
assert.deepEqual(computedAlignment, {
canAlign: true,
editPath: [
{op: 'substitute', input: 0, match: 0},
{op: 'match', input: 1, match: 1},
{op: 'match', input: 2, match: 2},
{op: 'match', input: 3, match: 3},
{op: 'substitute', input: 4, match: 4}
],
leadTokenShift: 0,
leadEditLength: 1,
matchLength: 3,
@ -372,6 +477,14 @@ describe('computeAlignment', () => {
const computedAlignment = computeAlignment(baseContext, newContext, false);
assert.deepEqual(computedAlignment, {
canAlign: true,
editPath: [
{op: 'substitute', input: 0, match: 0},
{op: 'match', input: 1, match: 1},
{op: 'match', input: 2, match: 2},
{op: 'match', input: 3, match: 3},
{op: 'substitute', input: 4, match: 4},
{op: 'insert', match: 5}
],
leadTokenShift: 0,
leadEditLength: 1,
matchLength: 3,
@ -391,9 +504,17 @@ describe('computeAlignment', () => {
const computedAlignment = computeAlignment(baseContext, newContext, false);
assert.deepEqual(computedAlignment, {
canAlign: true,
editPath: [
{op: 'insert', match: 0},
{op: 'match', input: 0, match: 1},
{op: 'match', input: 1, match: 2},
{op: 'match', input: 2, match: 3},
{op: 'match', input: 3, match: 4},
{op: 'substitute', input: 4, match: 5}
],
leadTokenShift: 1,
leadEditLength: 0,
matchLength: 4, // we treat 'quick' and 'uick' as the same
matchLength: 4,
tailEditLength: 1,
tailTokenShift: 0
});
@ -410,9 +531,17 @@ describe('computeAlignment', () => {
const computedAlignment = computeAlignment(baseContext, newContext, false);
assert.deepEqual(computedAlignment, {
canAlign: true,
editPath: [
{op: 'insert', match: 0},
{op: 'match', input: 0, match: 1},
{op: 'match', input: 1, match: 2},
{op: 'match', input: 2, match: 3},
{op: 'match', input: 3, match: 4},
{op: 'delete', input: 4}
],
leadTokenShift: 1,
leadEditLength: 0,
matchLength: 4, // we treat 'quick' and 'uick' as the same
matchLength: 4,
tailEditLength: 0,
tailTokenShift: -1
});
@ -429,9 +558,17 @@ describe('computeAlignment', () => {
const computedAlignment = computeAlignment(baseContext, newContext, false);
assert.deepEqual(computedAlignment, {
canAlign: true,
editPath: [
{op: 'insert', match: 0},
{op: 'match', input: 0, match: 1},
{op: 'match', input: 1, match: 2},
{op: 'match', input: 2, match: 3},
{op: 'substitute', input: 3, match: 4},
{op: 'delete', input: 4}
],
leadTokenShift: 1,
leadEditLength: 0,
matchLength: 3, // we treat 'quick' and 'uick' as the same
matchLength: 3,
tailEditLength: 1,
tailTokenShift: -1
});
@ -446,7 +583,16 @@ describe('computeAlignment', () => {
];
const computedAlignment = computeAlignment(baseContext, newContext, false);
assert.deepEqual(computedAlignment, {canAlign: false, editPath: ["match", "delete", "match", "match", "match"]});
assert.deepEqual(computedAlignment, {
canAlign: false,
editPath: [
{op: 'match', input: 0, match: 0},
{op: 'delete', input: 1},
{op: 'match', input: 2, match: 1},
{op: 'match', input: 3, match: 2},
{op: 'match', input: 4, match: 3}
]
});
});
it("fails alignment for mid-head insertion", () => {
@ -458,7 +604,16 @@ describe('computeAlignment', () => {
];
const computedAlignment = computeAlignment(baseContext, newContext, false);
assert.deepEqual(computedAlignment, {canAlign: false, editPath: ["match", "insert", "match", "match", "match"]});
assert.deepEqual(computedAlignment, {
canAlign: false,
editPath: [
{op: 'match', input: 0, match: 0},
{op: 'insert', match: 1},
{op: 'match', input: 1, match: 2},
{op: 'match', input: 2, match: 3},
{op: 'match', input: 3, match: 4}
]
});
});
it("fails alignment for mid-tail deletion", () => {
@ -470,7 +625,16 @@ describe('computeAlignment', () => {
];
const computedAlignment = computeAlignment(baseContext, newContext, false);
assert.deepEqual(computedAlignment, {canAlign: false, editPath: ["match", "match", "match", "delete", "match"]});
assert.deepEqual(computedAlignment, {
canAlign: false,
editPath: [
{op: 'match', input: 0, match: 0},
{op: 'match', input: 1, match: 1},
{op: 'match', input: 2, match: 2},
{op: 'delete', input: 3},
{op: 'match', input: 4, match: 3}
]
});
});
it("fails alignment for mid-tail insertion", () => {
@ -482,7 +646,17 @@ describe('computeAlignment', () => {
];
const computedAlignment = computeAlignment(baseContext, newContext, false);
assert.deepEqual(computedAlignment, {canAlign: false, editPath: ["match", "match", "match", "match", "insert", "match"]});
assert.deepEqual(computedAlignment, {
canAlign: false,
editPath: [
{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: 'insert', match: 4},
{op: 'match', input: 4, match: 5}
]
});
});
it("handles late-context suggestion application after backspace", () => {
@ -496,6 +670,19 @@ describe('computeAlignment', () => {
const computedAlignment = computeAlignment(baseContext, newContext, false);
assert.deepEqual(computedAlignment, {
canAlign: true,
editPath: [
{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: 'match', input: 5, match: 5},
{op: 'match', input: 6, match: 6},
{op: 'match', input: 7, match: 7},
{op: 'substitute', input: 8, match: 8},
{op: 'match', input: 9, match: 9},
{op: 'match', input: 10, match: 10}
],
leadTokenShift: 0,
leadEditLength: 0,
matchLength: 8,
@ -516,6 +703,21 @@ describe('computeAlignment', () => {
assert.deepEqual(computedAlignment, {
canAlign: true,
editPath: [
{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: 'match', input: 5, match: 5},
{op: 'match', input: 6, match: 6},
{op: 'match', input: 7, match: 7},
{op: 'match', input: 8, match: 8},
{op: 'match', input: 9, match: 9},
{op: 'substitute', input: 10, match: 10},
{op: 'insert', match: 11},
{op: 'insert', match: 12}
],
leadTokenShift: 0,
leadEditLength: 0,
matchLength: 10,
@ -549,6 +751,31 @@ describe('computeAlignment', () => {
assert.deepEqual(computeAlignment(baseContext1, incomingContext1, true), {
canAlign: true,
editPath: [
{op: 'substitute', 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: 'match', input: 5, match: 5},
{op: 'match', input: 6, match: 6},
{op: 'match', input: 7, match: 7},
{op: 'match', input: 8, match: 8},
{op: 'match', input: 9, match: 9},
{op: 'match', input: 10, match: 10},
{op: 'match', input: 11, match: 11},
{op: 'match', input: 12, match: 12},
{op: 'match', input: 13, match: 13},
{op: 'match', input: 14, match: 14},
{op: 'match', input: 15, match: 15},
{op: 'match', input: 16, match: 16},
{op: 'match', input: 17, match: 17},
{op: 'match', input: 18, match: 18},
{op: 'match', input: 19, match: 19},
{op: 'match', input: 20, match: 20},
{op: 'match', input: 21, match: 21},
{op: 'substitute', input: 22, match: 22}
],
leadTokenShift: 0,
leadEditLength: 1,
matchLength: 21,
@ -577,6 +804,33 @@ describe('computeAlignment', () => {
assert.deepEqual(computeAlignment(baseContext2, incomingContext2, true), {
canAlign: true,
editPath: [
{op: 'substitute', 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: 'match', input: 5, match: 5},
{op: 'match', input: 6, match: 6},
{op: 'match', input: 7, match: 7},
{op: 'match', input: 8, match: 8},
{op: 'match', input: 9, match: 9},
{op: 'match', input: 10, match: 10},
{op: 'match', input: 11, match: 11},
{op: 'match', input: 12, match: 12},
{op: 'match', input: 13, match: 13},
{op: 'match', input: 14, match: 14},
{op: 'match', input: 15, match: 15},
{op: 'match', input: 16, match: 16},
{op: 'match', input: 17, match: 17},
{op: 'match', input: 18, match: 18},
{op: 'match', input: 19, match: 19},
{op: 'match', input: 20, match: 20},
{op: 'match', input: 21, match: 21},
{op: 'match', input: 22, match: 22},
{op: 'match', input: 23, match: 23},
{op: 'substitute', input: 24, match: 24}
],
leadTokenShift: 0,
leadEditLength: 1,
matchLength: 23,
@ -601,6 +855,33 @@ describe('computeAlignment', () => {
assert.deepEqual(computeAlignment(baseContext3, incomingContext3, true), {
canAlign: true,
editPath: [
{op: 'delete', input: 0},
{op: 'match', input: 1, match: 0},
{op: 'match', input: 2, match: 1},
{op: 'match', input: 3, match: 2},
{op: 'match', input: 4, match: 3},
{op: 'match', input: 5, match: 4},
{op: 'match', input: 6, match: 5},
{op: 'match', input: 7, match: 6},
{op: 'match', input: 8, match: 7},
{op: 'match', input: 9, match: 8},
{op: 'match', input: 10, match: 9},
{op: 'match', input: 11, match: 10},
{op: 'match', input: 12, match: 11},
{op: 'match', input: 13, match: 12},
{op: 'match', input: 14, match: 13},
{op: 'match', input: 15, match: 14},
{op: 'match', input: 16, match: 15},
{op: 'match', input: 17, match: 16},
{op: 'match', input: 18, match: 17},
{op: 'match', input: 19, match: 18},
{op: 'match', input: 20, match: 19},
{op: 'match', input: 21, match: 20},
{op: 'match', input: 22, match: 21},
{op: 'match', input: 23, match: 22},
{op: 'substitute', input: 24, match: 23}
],
leadTokenShift: -1,
leadEditLength: 0,
matchLength: 23,

View file

@ -45,6 +45,15 @@ describe('ContextTokenization', function() {
const rawTextTokens = ['an', ' ', 'apple', ' ', 'a', ' ', 'day'];
let alignment: ContextStateAlignment = {
canAlign: true,
editPath: [
{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: 'match', input: 5, match: 5},
{op: 'match', input: 6, match: 6}
],
leadTokenShift: 0,
leadEditLength: 0,
matchLength: 6,
@ -67,6 +76,15 @@ describe('ContextTokenization', function() {
let baseTokenization = new ContextTokenization(rawTextTokens.map((text => toToken(text))), {
canAlign: true,
editPath: [
{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: 'match', input: 5, match: 5},
{op: 'match', input: 6, match: 6}
],
leadTokenShift: 0,
leadEditLength: 0,
matchLength: 6,
@ -112,6 +130,17 @@ describe('ContextTokenization', function() {
const tokenization = baseTokenization.transitionTo(
targetTokens, {
canAlign: true,
editPath: [
{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: 'match', input: 5, match: 5},
{op: 'match', input: 6, match: 6},
{op: 'insert', match: 7},
{op: 'insert', match: 8}
],
leadTokenShift: 0,
leadEditLength: 0,
matchLength: 7,
@ -140,6 +169,15 @@ describe('ContextTokenization', function() {
const tokenization = baseTokenization.transitionTo(
targetTokens, {
canAlign: true,
editPath: [
{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: 'match', input: 5, match: 5},
{op: 'substitute', input: 6, match: 6}
],
leadTokenShift: 0,
leadEditLength: 0,
matchLength: 6,
@ -171,6 +209,17 @@ describe('ContextTokenization', function() {
const tokenization = baseTokenization.transitionTo(
targetTokens, {
canAlign: true,
editPath: [
{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: 'match', input: 5, match: 5},
{op: 'match', input: 6, match: 6},
{op: 'substitute', input: 7, match: 7},
{op: 'substitute', input: 8, match: 8}
],
leadTokenShift: 0,
leadEditLength: 0,
matchLength: 7,
@ -210,6 +259,31 @@ describe('ContextTokenization', function() {
const tokenization = baseTokenization.transitionTo(
targetTokens, {
canAlign: true,
editPath: [
{op: 'substitute', 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: 'match', input: 5, match: 5},
{op: 'match', input: 6, match: 6},
{op: 'match', input: 7, match: 7},
{op: 'match', input: 8, match: 8},
{op: 'match', input: 9, match: 9},
{op: 'match', input: 10, match: 10},
{op: 'match', input: 11, match: 11},
{op: 'match', input: 12, match: 12},
{op: 'match', input: 13, match: 13},
{op: 'match', input: 14, match: 14},
{op: 'match', input: 15, match: 15},
{op: 'match', input: 16, match: 16},
{op: 'match', input: 17, match: 17},
{op: 'match', input: 18, match: 18},
{op: 'match', input: 19, match: 19},
{op: 'match', input: 20, match: 20},
{op: 'match', input: 21, match: 21},
{op: 'substitute', input: 22, match: 22}
],
leadTokenShift: 0,
leadEditLength: 1,
matchLength: 21,
@ -247,6 +321,33 @@ describe('ContextTokenization', function() {
const tokenization = baseTokenization.transitionTo(
targetTokens, {
canAlign: true,
editPath: [
{op: 'substitute', 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: 'match', input: 5, match: 5},
{op: 'match', input: 6, match: 6},
{op: 'match', input: 7, match: 7},
{op: 'match', input: 8, match: 8},
{op: 'match', input: 9, match: 9},
{op: 'match', input: 10, match: 10},
{op: 'match', input: 11, match: 11},
{op: 'match', input: 12, match: 12},
{op: 'match', input: 13, match: 13},
{op: 'match', input: 14, match: 14},
{op: 'match', input: 15, match: 15},
{op: 'match', input: 16, match: 16},
{op: 'match', input: 17, match: 17},
{op: 'match', input: 18, match: 18},
{op: 'match', input: 19, match: 19},
{op: 'match', input: 20, match: 20},
{op: 'match', input: 21, match: 21},
{op: 'match', input: 22, match: 22},
{op: 'match', input: 23, match: 23},
{op: 'substitute', input: 24, match: 24}
],
leadTokenShift: 0,
leadEditLength: 1,
matchLength: 23,
@ -282,6 +383,19 @@ describe('ContextTokenization', function() {
// properly.
targetTokens, {
canAlign: true,
editPath: [
{op: 'substitute', 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: 'match', input: 5, match: 5},
{op: 'match', input: 6, match: 6},
{op: 'match', input: 7, match: 7},
{op: 'match', input: 8, match: 8},
{op: 'match', input: 9, match: 9},
{op: 'match', input: 10, match: 10}
],
leadTokenShift: 0,
leadEditLength: 1,
matchLength: baseTexts.length - 1,
@ -321,6 +435,21 @@ describe('ContextTokenization', function() {
// properly.
targetTokens, {
canAlign: true,
editPath: [
{op: 'insert', match: 0},
{op: 'insert', match: 1},
{op: 'match', input: 0, match: 2},
{op: 'match', input: 1, match: 3},
{op: 'match', input: 2, match: 4},
{op: 'match', input: 3, match: 5},
{op: 'match', input: 4, match: 6},
{op: 'match', input: 5, match: 7},
{op: 'match', input: 6, match: 8},
{op: 'match', input: 7, match: 9},
{op: 'match', input: 8, match: 10},
{op: 'match', input: 9, match: 11},
{op: 'match', input: 10, match: 12}
],
leadTokenShift: 2, // "applesauce", " "
leadEditLength: 1, // "nd" / "and"
matchLength: baseTexts.length - 2,
@ -360,6 +489,21 @@ describe('ContextTokenization', function() {
const tokenization = baseTokenization.transitionTo(
targetTokens, {
canAlign: true,
editPath: [
{op: 'delete', input: 0},
{op: 'substitute', input: 1, match: 0},
{op: 'substitute', input: 2, match: 1},
{op: 'substitute', input: 3, match: 2},
{op: 'substitute', input: 4, match: 3},
{op: 'substitute', input: 5, match: 4},
{op: 'substitute', input: 6, match: 5},
{op: 'substitute', input: 7, match: 6},
{op: 'substitute', input: 8, match: 7},
{op: 'match', input: 9, match: 8},
{op: 'match', input: 10, match: 9},
{op: 'match', input: 11, match: 10},
{op: 'match', input: 12, match: 11}
],
leadTokenShift: -1, // "'",
leadEditLength: 1, // "t" / "n't"
matchLength: baseTexts.length - 3,
@ -399,6 +543,23 @@ describe('ContextTokenization', function() {
const tokenization = baseTokenization.transitionTo(
targetTokens, {
canAlign: true,
editPath: [
{op: 'delete', input: 0},
{op: 'substitute', input: 1, match: 0},
{op: 'substitute', input: 2, match: 1},
{op: 'substitute', input: 3, match: 2},
{op: 'substitute', input: 4, match: 3},
{op: 'substitute', input: 5, match: 4},
{op: 'substitute', input: 6, match: 5},
{op: 'substitute', input: 7, match: 6},
{op: 'substitute', input: 8, match: 7},
{op: 'match', input: 9, match: 8},
{op: 'match', input: 10, match: 9},
{op: 'match', input: 11, match: 10},
{op: 'match', input: 12, match: 11},
{op: 'delete', input: 13},
{op: 'delete', input: 14}
],
leadTokenShift: -1, // "'",
leadEditLength: 1, // "t" / "n't"
matchLength: baseTexts.length - 4,
@ -439,6 +600,23 @@ describe('ContextTokenization', function() {
const tokenization = baseTokenization.transitionTo(
targetTokens, {
canAlign: true,
editPath: [
{op: 'delete', input: 0},
{op: 'substitute', input: 1, match: 0},
{op: 'substitute', input: 2, match: 1},
{op: 'substitute', input: 3, match: 2},
{op: 'substitute', input: 4, match: 3},
{op: 'substitute', input: 5, match: 4},
{op: 'substitute', input: 6, match: 5},
{op: 'substitute', input: 7, match: 6},
{op: 'substitute', input: 8, match: 7},
{op: 'match', input: 9, match: 8},
{op: 'match', input: 10, match: 9},
{op: 'match', input: 11, match: 10},
{op: 'match', input: 12, match: 11},
{op: 'insert', match: 12},
{op: 'insert', match: 13}
],
leadTokenShift: 1, // "'",
leadEditLength: 1, // "n't" / "t"
matchLength: baseTexts.length - 1,

View file

@ -1,5 +1,5 @@
import { assert } from 'chai';
import { ClassicalDistanceCalculation, EditTuple, forNewIndices } from '@keymanapp/lm-worker/test-index';
import { ClassicalDistanceCalculation, EditOperation, EditTuple, forNewIndices } from '@keymanapp/lm-worker/test-index';
import sinon from 'sinon';
// Very useful for diagnosing unit test issues when path inspection is needed.
@ -596,30 +596,30 @@ describe('Classical Damerau-Levenshtein edit-distance calculation', function() {
// While the two 'm's are interchangable, the path will favor
// a result with the longest contiguous set of matches.
let editSequences: EditTuple<string>[][] = [[
{ op: 'match', input: 'a', match: 'a'},
{ op: 'match', input: 'c', match: 'c'},
{ op: 'match', input: 'c', match: 'c'},
{ op: 'match', input: 'o', match: 'o'},
{ op: 'insert', match: 'm'}, // insert is earlier
{ op: 'match', input: 'm', match: 'm'}, // longer contiguous match sequence
{ op: 'match', input: 'o', match: 'o'},
{ op: 'match', input: 'd', match: 'd'},
{ op: 'match', input: 'a', match: 'a'},
{ op: 'match', input: 't', match: 't'},
{ op: 'match', input: 'e', match: 'e'},
let editSequences: EditTuple<EditOperation>[][] = [[
{ op: 'match', input: 0 /* 'a' */, match: 0 /* 'a' */},
{ op: 'match', input: 1 /* 'c' */, match: 1 /* 'c' */},
{ op: 'match', input: 2 /* 'c' */, match: 2 /* 'c' */},
{ op: 'match', input: 3 /* 'o' */, match: 3 /* 'o' */},
{ op: 'insert', match: 4 /* 'm' */}, // insert is earlier
{ op: 'match', input: 4 /* 'm' */, match: 5 /* 'm' */}, // longer contiguous match sequence
{ op: 'match', input: 5 /* 'o' */, match: 6 /* 'o' */},
{ op: 'match', input: 6 /* 'd' */, match: 7 /* 'd' */},
{ op: 'match', input: 7 /* 'a' */, match: 8 /* 'a' */},
{ op: 'match', input: 8 /* 't' */, match: 9 /* 't' */},
{ op: 'match', input: 9 /* 'e' */, match: 10 /* 'e' */}
], [
{ op: 'match', input: 'a', match: 'a'},
{ op: 'match', input: 'c', match: 'c'},
{ op: 'match', input: 'c', match: 'c'},
{ op: 'match', input: 'o', match: 'o'},
{ op: 'match', input: 'm', match: 'm'},
{ op: 'insert', match: 'm'}, // now inserting later.
{ op: 'match', input: 'o', match: 'o'},
{ op: 'match', input: 'd', match: 'd'},
{ op: 'match', input: 'a', match: 'a'},
{ op: 'match', input: 't', match: 't'},
{ op: 'match', input: 'e', match: 'e'},
{ op: 'match', input: 0 /* 'a' */, match: 0 /* 'a' */},
{ op: 'match', input: 1 /* 'c' */, match: 1 /* 'c' */},
{ op: 'match', input: 2 /* 'c' */, match: 2 /* 'c' */},
{ op: 'match', input: 3 /* 'o' */, match: 3 /* 'o' */},
{ op: 'match', input: 4 /* 'm' */, match: 4 /* 'm' */},
{ op: 'insert', match: 5 /* 'm' */}, // now inserting later.
{ op: 'match', input: 5 /* 'o' */, match: 6 /* 'o' */},
{ op: 'match', input: 6 /* 'd' */, match: 7 /* 'd' */},
{ op: 'match', input: 7 /* 'a' */, match: 8 /* 'a' */},
{ op: 'match', input: 8 /* 't' */, match: 9 /* 't' */},
{ op: 'match', input: 9 /* 'e' */, match: 10 /* 'e' */}
]];
assert.sameDeepOrderedMembers(buffer.editPath(), editSequences);
@ -629,22 +629,22 @@ describe('Classical Damerau-Levenshtein edit-distance calculation', function() {
let buffer = compute("harras", "harass", "InputThenMatch");
let bestEditSequence: EditTuple<string>[] = [
{ op: 'match', input: 'h', match: 'h'},
{ op: 'match', input: 'a', match: 'a'},
{ op: 'match', input: 'r', match: 'r'},
{ op: 'substitute', input: 'r', match: 'a'},
{ op: 'substitute', input: 'a', match: 's'},
{ op: 'match', input: 's', match: 's'}
{ op: 'match', input: 0, match: 0}, // h, h
{ op: 'match', input: 1, match: 1}, // a, a
{ op: 'match', input: 2, match: 2}, // r, r
{ op: 'substitute', input: 3, match: 3}, // r => a
{ op: 'substitute', input: 4, match: 4}, // a => s
{ op: 'match', input: 5, match: 5}
];
let altSequence: EditTuple<string>[] = [
{ op: 'match', input: 'h', match: 'h'},
{ op: 'match', input: 'a', match: 'a'},
{ op: 'match', input: 'r', match: 'r'},
{ op: 'delete', input: 'r'},
{ op: 'match', input: 'a', match: 'a'},
{ op: 'match', input: 's', match: 's'},
{ op: 'insert', match: 's'}
{ op: 'match', input: 0, match: 0},
{ op: 'match', input: 1, match: 1},
{ op: 'match', input: 2, match: 2},
{ op: 'delete', input: 3},
{ op: 'match', input: 4, match: 3},
{ op: 'match', input: 5, match: 4},
{ op: 'insert', match: 5}
];
const viablePaths = buffer.editPath();
@ -660,12 +660,12 @@ describe('Classical Damerau-Levenshtein edit-distance calculation', function() {
let buffer = compute("access", "assess", "InputThenMatch");
let editSequence: EditTuple<string>[] = [
{ op: 'match', input: 'a', match: 'a'},
{ op: 'substitute', input: 'c', match: 's'},
{ op: 'substitute', input: 'c', match: 's'},
{ op: 'match', input: 'e', match: 'e'},
{ op: 'match', input: 's', match: 's'},
{ op: 'match', input: 's', match: 's'}
{ op: 'match', input: 0, match: 0},
{ op: 'substitute', input: 1, match: 1},
{ op: 'substitute', input: 2, match: 2},
{ op: 'match', input: 3, match: 3},
{ op: 'match', input: 4, match: 4},
{ op: 'match', input: 5, match: 5}
];
const viablePaths = buffer.editPath();
@ -677,10 +677,10 @@ describe('Classical Damerau-Levenshtein edit-distance calculation', function() {
let buffer = compute("ifhs", "fish");
let editSequence: EditTuple<string>[] = [
{ op: 'transpose-start', input: 'i', match: 'f'},
{ op: 'transpose-end', input: 'f', match: 'i'},
{ op: 'transpose-start', input: 'h', match: 's'},
{ op: 'transpose-end', input: 's', match: 'h'}
{ op: 'transpose-start', input: 0, match: 0},
{ op: 'transpose-end', input: 1, match: 1},
{ op: 'transpose-start', input: 2, match: 2},
{ op: 'transpose-end', input: 3, match: 3}
];
const viablePaths = buffer.editPath();
@ -692,19 +692,19 @@ describe('Classical Damerau-Levenshtein edit-distance calculation', function() {
let buffer = compute("then", "their");
let editSequence: EditTuple<string>[] = [
{ op: 'match', input: 't', match: 't'},
{ op: 'match', input: 'h', match: 'h'}, // best sequence: continguous matches
{ op: 'match', input: 'e', match: 'e'}, // + adjacent substitutes
{ op: 'substitute', input: 'n', match: 'i'},
{ op: 'insert', match: 'r'},
{ op: 'match', input: 0, match: 0},
{ op: 'match', input: 1, match: 1}, // best sequence: continguous matches
{ op: 'match', input: 2, match: 2}, // + adjacent substitutes
{ op: 'substitute', input: 3, match: 3},
{ op: 'insert', match: 4},
];
let altSequence: EditTuple<string>[] = [
{ op: 'match', input: 't', match: 't'},
{ op: 'match', input: 'h', match: 'h'},
{ op: 'match', input: 'e', match: 'e'},
{ op: 'insert', match: 'i'},
{ op: 'substitute', input: 'n', match: 'r'},
{ op: 'match', input: 0, match: 0},
{ op: 'match', input: 1, match: 1},
{ op: 'match', input: 2, match: 2},
{ op: 'insert', match: 3},
{ op: 'substitute', input: 3, match: 4},
];
const viablePaths = buffer.editPath();
@ -718,16 +718,16 @@ describe('Classical Damerau-Levenshtein edit-distance calculation', function() {
let buffer = compute("abczdefig", "cazefghi", "InputThenMatch", 2);
let editSequence: EditTuple<string>[] = [
{ op: 'transpose-start', input: 'a', match: 'c'},
{ op: 'transpose-delete', input: 'b'},
{ op: 'transpose-end', input: 'c', match: 'a'},
{ op: 'match', input: 'z', match: 'z'},
{ op: 'delete', input: 'd'},
{ op: 'match', input: 'e', match: 'e'},
{ op: 'match', input: 'f', match: 'f'},
{ op: 'transpose-start', input: 'i', match: 'g'},
{ op: 'transpose-insert', match: 'h'},
{ op: 'transpose-end', input: 'g', match: 'i'}
{ op: 'transpose-start', input: 0, match: 0},
{ op: 'transpose-delete', input: 1},
{ op: 'transpose-end', input: 2, match: 1},
{ op: 'match', input: 3, match: 2},
{ op: 'delete', input: 4},
{ op: 'match', input: 5, match: 3},
{ op: 'match', input: 6, match: 4},
{ op: 'transpose-start', input: 7, match: 5},
{ op: 'transpose-insert', match: 6},
{ op: 'transpose-end', input: 8, match: 7}
];
const viablePaths = buffer.editPath();
@ -744,39 +744,39 @@ describe('Classical Damerau-Levenshtein edit-distance calculation', function() {
const buffer = compute(src, dst, "InputThenMatch", 4);
let editSequence: EditTuple<string>[] = [
{ op: 'delete', input: 'a'},
{ op: 'delete', input: ' '},
{ op: 'substitute', input: 'b', match: 'q'},
{ op: 'match', input: ' ', match: ' '},
{ op: 'match', input: 'c', match: 'c'},
{ op: 'match', input: ' ', match: ' '},
{ op: 'match', input: 'd', match: 'd'},
{ op: 'match', input: '?', match: '?'},
{ op: 'match', input: ';', match: ';'},
{ op: 'match', input: 'e', match: 'e'},
{ op: 'match', input: ' ', match: ' '},
{ op: 'match', input: 'f', match: 'f'},
{ op: 'match', input: ' ', match: ' '},
{ op: 'match', input: 'g', match: 'g'},
{ op: 'match', input: ',', match: ','},
{ op: 'match', input: ' ', match: ' '},
{ op: 'match', input: 'h', match: 'h'},
{ op: 'match', input: ' ', match: ' '},
{ op: 'match', input: 'i', match: 'i'},
{ op: 'match', input: ' ', match: ' '},
{ op: 'match', input: 'j', match: 'j'},
{ op: 'match', input: ' ', match: ' '},
{ op: 'match', input: 'k', match: 'k'},
{ op: 'match', input: ' ', match: ' '},
{ op: 'match', input: 'l', match: 'l'},
{ op: 'match', input: ' ', match: ' '},
{ op: 'match', input: 'm', match: 'm'},
{ op: 'match', input: ' ', match: ' '},
{ op: 'match', input: 'n', match: 'n'},
{ op: 'match', input: ' ', match: ' '},
{ op: 'match', input: 'o', match: 'o'},
{ op: 'match', input: ' ', match: ' '},
{ op: 'substitute', input: 'p', match: 'r'},
{ op: 'delete', input: 0},
{ op: 'delete', input: 1},
{ op: 'substitute', input: 2, match: 0},
{ op: 'match', input: 3, match: 1},
{ op: 'match', input: 4, match: 2},
{ op: 'match', input: 5, match: 3},
{ op: 'match', input: 6, match: 4},
{ op: 'match', input: 7, match: 5},
{ op: 'match', input: 8, match: 6},
{ op: 'match', input: 9, match: 7},
{ op: 'match', input: 10, match: 8},
{ op: 'match', input: 11, match: 9},
{ op: 'match', input: 12, match: 10},
{ op: 'match', input: 13, match: 11},
{ op: 'match', input: 14, match: 12},
{ op: 'match', input: 15, match: 13},
{ op: 'match', input: 16, match: 14},
{ op: 'match', input: 17, match: 15},
{ op: 'match', input: 18, match: 16},
{ op: 'match', input: 19, match: 17},
{ op: 'match', input: 20, match: 18},
{ op: 'match', input: 21, match: 19},
{ op: 'match', input: 22, match: 20},
{ op: 'match', input: 23, match: 21},
{ op: 'match', input: 24, match: 22},
{ op: 'match', input: 25, match: 23},
{ op: 'match', input: 26, match: 24},
{ op: 'match', input: 27, match: 25},
{ op: 'match', input: 28, match: 26},
{ op: 'match', input: 29, match: 27},
{ op: 'match', input: 30, match: 28},
{ op: 'match', input: 31, match: 29},
{ op: 'substitute', input: 32, match: 30},
];
const viablePaths = buffer.editPath();

View file

@ -69,13 +69,13 @@ describe('Split/merge aware edit-distance calculation', () => {
const editPaths = calc.editPath();
assert.equal(editPaths.length, 1);
assert.sameDeepOrderedMembers(editPaths[0], [
{ op: 'split', input: 'ab', match: 'a' },
{ op: 'split', input: 'ab', match: 'b' },
{ op: 'transpose-start', input: 'd', match: 'c' },
{ op: 'transpose-end', input: 'c', match: 'd' },
{ op: 'substitute', input: 'e', match: 'f' },
{ op: 'merge', input: 'g', match: 'gh' },
{ op: 'merge', input: 'h', match: 'gh' }
{ op: 'split', input: 0, match: 0 },
{ op: 'split', input: 0, match: 1 },
{ op: 'transpose-start', input: 1, match: 2 },
{ op: 'transpose-end', input: 2, match: 3 },
{ op: 'substitute', input: 3, match: 4 },
{ op: 'merge', input: 4, match: 5 },
{ op: 'merge', input: 5, match: 5 }
]);
});
@ -91,13 +91,13 @@ describe('Split/merge aware edit-distance calculation', () => {
const editPaths = calc.editPath();
assert.equal(editPaths.length, 1);
assert.sameDeepOrderedMembers(editPaths[0], [
{ op: 'split', input: 'abc', match: 'a' },
{ op: 'split', input: 'abc', match: 'b' },
{ op: 'split', input: 'abc', match: 'c' },
{ op: 'match', input: 'd', match: 'd' },
{ op: 'merge', input: 'f', match: 'fgh' },
{ op: 'merge', input: 'g', match: 'fgh' },
{ op: 'merge', input: 'h', match: 'fgh' }
{ op: 'split', input: 0, match: 0 },
{ op: 'split', input: 0, match: 1 },
{ op: 'split', input: 0, match: 2 },
{ op: 'match', input: 1, match: 3 },
{ op: 'merge', input: 2, match: 4 },
{ op: 'merge', input: 3, match: 4 },
{ op: 'merge', input: 4, match: 4 }
]);
});
});