mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-31 12:47:43 +00:00
feat(web): support split, merge for edit-path construction
Relates-to: #14679 The goal of this PR is to facilitate better handling for token splits and token merges.
This commit is contained in:
parent
01d73079a9
commit
0fbf9c028e
3 changed files with 85 additions and 9 deletions
|
|
@ -120,7 +120,9 @@ export function visualizeCalculation<TUnit, TOpSet>(calc: ClassicalDistanceCalcu
|
|||
break;
|
||||
case 'substitute':
|
||||
case 'match':
|
||||
const op = edit.op == 'substitute' ? '=>' : '==';
|
||||
case 'split':
|
||||
case 'merge':
|
||||
const op = edit.op == 'match' ? '==' : '=>';
|
||||
tokenText = tokenText || `'${edit.input}' ${op} '${edit.match}'`;
|
||||
break;
|
||||
// transpose-start, transpose-end
|
||||
|
|
@ -129,9 +131,16 @@ export function visualizeCalculation<TUnit, TOpSet>(calc: ClassicalDistanceCalcu
|
|||
}
|
||||
return `${edit.op}(${tokenText})`;
|
||||
}
|
||||
|
||||
let lastEdit: string;
|
||||
do {
|
||||
edits.push(printEdit(path.shift()));
|
||||
} while(path.length > 0 && edits[edits.length-1].indexOf('insert') != -1);
|
||||
if(lastEdit && lastEdit.indexOf('split') != -1 && path[0].op != 'split') {
|
||||
break;
|
||||
}
|
||||
|
||||
lastEdit = printEdit(path.shift());
|
||||
edits.push(lastEdit);
|
||||
} while(path.length > 0 && (lastEdit.indexOf('insert') != -1 || lastEdit.indexOf('split') != -1));
|
||||
// If final row, dump the rest of the edit path into the current row.
|
||||
if(i == calc.inputSequence.length - 1) {
|
||||
// Capture final 'insert's!
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ClassicalDistanceCalculation, EditOperation, forNewIndices } from './classical-calculation.js';
|
||||
import { ClassicalDistanceCalculation, EditOperation, EditTuple, forNewIndices, PathBuilder } from './classical-calculation.js';
|
||||
|
||||
/**
|
||||
* The human-readable names for legal edit-operation edges on a merge-split
|
||||
|
|
@ -83,18 +83,22 @@ export class SegmentableDistanceCalculation extends ClassicalDistanceCalculation
|
|||
return returnBuffer;
|
||||
}
|
||||
|
||||
public increaseMaxDistance(): ClassicalDistanceCalculation<string> {
|
||||
public increaseMaxDistance(): ClassicalDistanceCalculation<string, ExtendedEditOperation> {
|
||||
// TODO: diagonal expansion
|
||||
// But it's not particularly needed for our use cases.
|
||||
throw new Error("Not yet supported for this edit-distance calculation type.");
|
||||
}
|
||||
|
||||
// TODO: edit path with 'split', 'merge' handling // progress: infrastructure is likely prepped?
|
||||
// TODO: visualization
|
||||
protected _buildPath(pathBuilder?: PathBuilder<string, ExtendedEditOperation>): EditTuple<string, ExtendedEditOperation>[][] {
|
||||
pathBuilder = pathBuilder ?? new PathBuilder<string, ExtendedEditOperation>(this, []);
|
||||
pathBuilder.addEdgeFinder(findSplitMergeEdges);
|
||||
super._buildPath(pathBuilder); // actually evaluates the edit-path.
|
||||
return pathBuilder.validPaths;
|
||||
}
|
||||
}
|
||||
|
||||
function getMergeSplitParent<TOpSet> (
|
||||
buffer: ClassicalDistanceCalculation<string, TOpSet>,
|
||||
function getMergeSplitParent<TOpEdit> (
|
||||
buffer: ClassicalDistanceCalculation<string, TOpEdit>,
|
||||
r: number,
|
||||
c: number
|
||||
): [number, number] {
|
||||
|
|
@ -135,4 +139,43 @@ function getMergeSplitParent<TOpSet> (
|
|||
}
|
||||
|
||||
return [lastMergeIndex, lastSplitIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the edit path used to obtain the optimal cost, distinguishing between zero-cost
|
||||
* substitutions ('match' operations) and actual substitutions.
|
||||
* @param row
|
||||
* @param col
|
||||
*/
|
||||
export function findSplitMergeEdges<TOpSet>(
|
||||
pathBuilder: PathBuilder<string, TOpSet | ExtendedEditOperation>,
|
||||
row: number,
|
||||
col: number
|
||||
): void {
|
||||
const calc = pathBuilder.calc;
|
||||
const currentCost = calc.getCostAt(row, col);
|
||||
if(currentCost == Number.MAX_VALUE) {
|
||||
// We're too far off the main diagonal - a proper edit distance is not viable!
|
||||
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>[] = [];
|
||||
for(let r = lastMergeIndex; r <= row; r++) {
|
||||
ops.push({ input: calc.inputSequence[r], match, op: 'merge' });
|
||||
}
|
||||
pathBuilder.backtracePath(lastMergeIndex - 1, col - 1, ops);
|
||||
}
|
||||
|
||||
if(lastSplitIndex != -1) {
|
||||
const ops: EditTuple<string, ExtendedEditOperation>[] = [];
|
||||
for(let c = lastSplitIndex; c <= col; c++) {
|
||||
ops.push({ input, match: calc.matchSequence[c], op: 'split' });
|
||||
}
|
||||
pathBuilder.backtracePath(row - 1, lastSplitIndex - 1, ops);
|
||||
}
|
||||
}
|
||||
|
|
@ -65,6 +65,18 @@ describe('Split/merge aware edit-distance calculation', () => {
|
|||
['a', 'b', 'c', 'd', 'f', 'gh'].forEach(c => calc = calc.addMatchChar(c));
|
||||
|
||||
assert.equal(calc.getFinalCost(), 4);
|
||||
|
||||
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' }
|
||||
]);
|
||||
});
|
||||
|
||||
it("abc,d,f,g,h -> a,b,c,d,fgh = 2", () => {
|
||||
|
|
@ -75,5 +87,17 @@ describe('Split/merge aware edit-distance calculation', () => {
|
|||
['a', 'b', 'c', 'd', 'fgh'].forEach(c => calc = calc.addMatchChar(c));
|
||||
|
||||
assert.equal(calc.getFinalCost(), 2);
|
||||
|
||||
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' }
|
||||
]);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue