mirror of
https://github.com/keymanapp/keyman.git
synced 2026-11-05 10:34:54 +00:00
Merge pull request #15720 from keymanapp/feat/web/deletion-spurs
feat(web): add dedicated DeletionQuotientSpur type for handling deletion-edit search-graph edges 🚂
This commit is contained in:
commit
7d36c4ac8c
6 changed files with 384 additions and 98 deletions
|
|
@ -0,0 +1,55 @@
|
|||
/**
|
||||
* Keyman is copyright (C) SIL Global. MIT License.
|
||||
*
|
||||
* Created by jahorton on 2026-02-03
|
||||
*
|
||||
* This file adds a SearchQuotientSpur variant modeling deletion of the corresponding
|
||||
* keystroke.
|
||||
*/
|
||||
|
||||
import { LexicalModelTypes } from "@keymanapp/common-types";
|
||||
|
||||
import { SearchNode } from "./distance-modeler.js";
|
||||
import { PathInputProperties, SearchQuotientNode } from "./search-quotient-node.js";
|
||||
import { SearchQuotientSpur } from "./search-quotient-spur.js";
|
||||
import { TokenResultMapping } from "./token-result-mapping.js";
|
||||
|
||||
import Distribution = LexicalModelTypes.Distribution;
|
||||
import ProbabilityMass = LexicalModelTypes.ProbabilityMass;
|
||||
import Transform = LexicalModelTypes.Transform;
|
||||
|
||||
export class DeletionQuotientSpur extends SearchQuotientSpur {
|
||||
public readonly insertLength: number = 0;
|
||||
public readonly leftDeleteLength: number = 0;
|
||||
|
||||
constructor(
|
||||
parentNode: SearchQuotientNode,
|
||||
inputs: Distribution<Readonly<Transform>>,
|
||||
inputSource: PathInputProperties | ProbabilityMass<Transform>
|
||||
) {
|
||||
super(parentNode, inputs, inputSource, parentNode.codepointLength);
|
||||
}
|
||||
|
||||
construct(parentNode: SearchQuotientNode, inputs: ProbabilityMass<Readonly<Transform>>[], inputSource: PathInputProperties): this {
|
||||
return new DeletionQuotientSpur(parentNode, inputs, inputSource) as this;
|
||||
}
|
||||
|
||||
protected buildEdgesFromResults(baseResults: ReadonlyArray<TokenResultMapping>): SearchNode[] {
|
||||
return baseResults
|
||||
// If there are already at least 2 edits for a node, do not add new edits.
|
||||
.filter((n) => n.editCount < 2)
|
||||
.flatMap((n) => n.buildDeletionEdges(this.inputs, this.spaceId));
|
||||
}
|
||||
|
||||
get edgeKey(): string {
|
||||
const baseKey = super.edgeKey;
|
||||
// Be sure to mark the distinction between this node; appending a suffix can
|
||||
// stack for multiple sequential deletions.
|
||||
return `${baseKey}DEL`;
|
||||
}
|
||||
|
||||
get bestExample() {
|
||||
// As deletion spurs add no new input, we can just re-use the parent node's version.
|
||||
return this.parentNode.bestExample;
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ export { ContextTransition } from './correction/context-transition.js';
|
|||
export * from './correction/correction-searchable.js';
|
||||
export * from './correction/correction-result-mapping.js';
|
||||
export * from './correction/distance-modeler.js';
|
||||
export * from './correction/deletion-quotient-spur.js';
|
||||
export * from './correction/insertion-quotient-spur.js';
|
||||
export * from './correction/substitution-quotient-spur.js';
|
||||
export * from './correction/search-quotient-cluster.js';
|
||||
|
|
|
|||
|
|
@ -18,17 +18,17 @@ describe('buildQuotientDocFixture() fixture', () => {
|
|||
[searchRoot, nodes.sc1, nodes.sc2].forEach((n) => {
|
||||
assert.equal(n.inputCount, 0);
|
||||
});
|
||||
[/*nodes.k1c0,*/ nodes.k1c1, nodes.k1c2, nodes.k1c3].forEach((n) => {
|
||||
[nodes.k1c0, nodes.k1c1, nodes.k1c2, nodes.k1c3].forEach((n) => {
|
||||
assert.equal(n.inputCount, 1);
|
||||
});
|
||||
[/*nodes.k2c0, nodes.k2c1,*/ nodes.k2c2, nodes.k2c3].forEach((n) => {
|
||||
[nodes.k2c0, nodes.k2c1, nodes.k2c2, nodes.k2c3].forEach((n) => {
|
||||
assert.equal(n.inputCount, 2);
|
||||
});
|
||||
|
||||
[searchRoot/*, nodes.k1c0, nodes.k2c0*/].forEach((n) => {
|
||||
[searchRoot, nodes.k1c0, nodes.k2c0].forEach((n) => {
|
||||
assert.equal(n.codepointLength, 0);
|
||||
});
|
||||
[nodes.sc1, nodes.k1c1/*, nodes.k2c1*/].forEach((n) => {
|
||||
[nodes.sc1, nodes.k1c1, nodes.k2c1].forEach((n) => {
|
||||
assert.equal(n.codepointLength, 1);
|
||||
});
|
||||
[nodes.sc2, nodes.k1c2, nodes.k2c2].forEach((n) => {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { LexicalModelTypes } from '@keymanapp/common-types';
|
|||
import { jsonFixture } from '@keymanapp/common-test-resources/model-helpers.mjs';
|
||||
|
||||
import {
|
||||
DeletionQuotientSpur,
|
||||
generateSubsetId,
|
||||
InsertionQuotientSpur,
|
||||
models,
|
||||
|
|
@ -52,27 +53,27 @@ export function buildQuotientDocFixture() {
|
|||
const sc1 = new InsertionQuotientSpur(searchRoot);
|
||||
const sc2 = new InsertionQuotientSpur(sc1);
|
||||
|
||||
// // K1C0
|
||||
// const k1c0 = new DeletionQuotientSpur(searchRoot, abDistrib.concat(cdDistrib), {
|
||||
// segment: {
|
||||
// transitionId: key1Id,
|
||||
// start: 0
|
||||
// },
|
||||
// // Deletions always get their own unique subset ID.
|
||||
// subsetId: generateSubsetId(),
|
||||
// bestProbFromSet: abDistrib[0].p
|
||||
// });
|
||||
// K1C0
|
||||
const k1c0 = new DeletionQuotientSpur(searchRoot, abDistrib.concat(cdDistrib), {
|
||||
segment: {
|
||||
transitionId: key1Id,
|
||||
start: 0
|
||||
},
|
||||
// Deletions always get their own unique subset ID.
|
||||
subsetId: generateSubsetId(),
|
||||
bestProbFromSet: abDistrib[0].p
|
||||
});
|
||||
|
||||
// // K1C1
|
||||
// const k1c1_del = new DeletionQuotientSpur(sc1, abDistrib.concat(cdDistrib), {
|
||||
// segment: {
|
||||
// transitionId: key1Id,
|
||||
// start: 0
|
||||
// },
|
||||
// // Deletions always get their own unique subset ID.
|
||||
// subsetId: generateSubsetId(),
|
||||
// bestProbFromSet: abDistrib[0].p
|
||||
// });
|
||||
// K1C1
|
||||
const k1c1_del = new DeletionQuotientSpur(sc1, abDistrib.concat(cdDistrib), {
|
||||
segment: {
|
||||
transitionId: key1Id,
|
||||
start: 0
|
||||
},
|
||||
// Deletions always get their own unique subset ID.
|
||||
subsetId: generateSubsetId(),
|
||||
bestProbFromSet: abDistrib[0].p
|
||||
});
|
||||
const k1c1_ab = new SubstitutionQuotientSpur(searchRoot, abDistrib, {
|
||||
segment: {
|
||||
transitionId: key1Id,
|
||||
|
|
@ -82,18 +83,18 @@ export function buildQuotientDocFixture() {
|
|||
subsetId: generateSubsetId(),
|
||||
bestProbFromSet: abDistrib[0].p
|
||||
});
|
||||
// const k1c1_ins = new InsertionQuotientSpur(k1c0);
|
||||
const k1c1 = new SearchQuotientCluster([/*k1c1_del,*/ k1c1_ab, /*k1c1_ins*/]);
|
||||
const k1c1_ins = new InsertionQuotientSpur(k1c0);
|
||||
const k1c1 = new SearchQuotientCluster([k1c1_del, k1c1_ab, k1c1_ins]);
|
||||
|
||||
// const k1c2_del = new DeletionQuotientSpur(sc2, abDistrib.concat(cdDistrib), {
|
||||
// segment: {
|
||||
// transitionId: key1Id,
|
||||
// start: 0
|
||||
// },
|
||||
// // Deletions always get their own unique subset ID.
|
||||
// subsetId: generateSubsetId(),
|
||||
// bestProbFromSet: abDistrib[0].p
|
||||
// });
|
||||
const k1c2_del = new DeletionQuotientSpur(sc2, abDistrib.concat(cdDistrib), {
|
||||
segment: {
|
||||
transitionId: key1Id,
|
||||
start: 0
|
||||
},
|
||||
// Deletions always get their own unique subset ID.
|
||||
subsetId: generateSubsetId(),
|
||||
bestProbFromSet: abDistrib[0].p
|
||||
});
|
||||
const k1c2_ab = new SubstitutionQuotientSpur(sc1, abDistrib, {
|
||||
segment: {
|
||||
transitionId: key1Id,
|
||||
|
|
@ -113,7 +114,7 @@ export function buildQuotientDocFixture() {
|
|||
bestProbFromSet: abDistrib[0].p
|
||||
});
|
||||
const k1c2_ins = new InsertionQuotientSpur(k1c1);
|
||||
const k1c2 = new SearchQuotientCluster([/*k1c2_del, */ k1c2_ab, k1c2_cd, k1c2_ins]);
|
||||
const k1c2 = new SearchQuotientCluster([k1c2_del, k1c2_ab, k1c2_cd, k1c2_ins]);
|
||||
|
||||
const k1c3_ab = new SubstitutionQuotientSpur(sc2, abDistrib, {
|
||||
segment: {
|
||||
|
|
@ -148,46 +149,46 @@ export function buildQuotientDocFixture() {
|
|||
{ sample: { insert: 'gh', deleteLeft: 0, id: key2Id }, p: .3 }
|
||||
];
|
||||
|
||||
// const k2c0 = new DeletionQuotientSpur(k1c0, efDistrib.concat(ghDistrib), {
|
||||
// segment: {
|
||||
// transitionId: key2Id,
|
||||
// start: 0
|
||||
// },
|
||||
// // Deletions always get their own unique subset ID.
|
||||
// subsetId: generateSubsetId(),
|
||||
// bestProbFromSet: efDistrib[0].p
|
||||
// });
|
||||
const k2c0 = new DeletionQuotientSpur(k1c0, efDistrib.concat(ghDistrib), {
|
||||
segment: {
|
||||
transitionId: key2Id,
|
||||
start: 0
|
||||
},
|
||||
// Deletions always get their own unique subset ID.
|
||||
subsetId: generateSubsetId(),
|
||||
bestProbFromSet: efDistrib[0].p
|
||||
});
|
||||
|
||||
// const k2c1_del = new DeletionQuotientSpur(k1c1, efDistrib.concat(ghDistrib), {
|
||||
// segment: {
|
||||
// transitionId: key2Id,
|
||||
// start: 0
|
||||
// },
|
||||
// // Deletions always get their own unique subset ID.
|
||||
// subsetId: generateSubsetId(),
|
||||
// bestProbFromSet: efDistrib[0].p
|
||||
// });
|
||||
// const k2c1_ef = new SubstitutionQuotientSpur(k1c0, efDistrib, {
|
||||
// segment: {
|
||||
// transitionId: key2Id,
|
||||
// start: 0
|
||||
// },
|
||||
// // Deletions always get their own unique subset ID.
|
||||
// subsetId: generateSubsetId(),
|
||||
// bestProbFromSet: efDistrib[0].p
|
||||
// });
|
||||
// const k2c1_ins = new InsertionQuotientSpur(k2c0);
|
||||
// const k2c1 = new SearchQuotientCluster([k2c1_del, k2c1_ef, k2c1_ins]);
|
||||
const k2c1_del = new DeletionQuotientSpur(k1c1, efDistrib.concat(ghDistrib), {
|
||||
segment: {
|
||||
transitionId: key2Id,
|
||||
start: 0
|
||||
},
|
||||
// Deletions always get their own unique subset ID.
|
||||
subsetId: generateSubsetId(),
|
||||
bestProbFromSet: efDistrib[0].p
|
||||
});
|
||||
const k2c1_ef = new SubstitutionQuotientSpur(k1c0, efDistrib, {
|
||||
segment: {
|
||||
transitionId: key2Id,
|
||||
start: 0
|
||||
},
|
||||
// Deletions always get their own unique subset ID.
|
||||
subsetId: generateSubsetId(),
|
||||
bestProbFromSet: efDistrib[0].p
|
||||
});
|
||||
const k2c1_ins = new InsertionQuotientSpur(k2c0);
|
||||
const k2c1 = new SearchQuotientCluster([k2c1_del, k2c1_ef, k2c1_ins]);
|
||||
|
||||
// const k2c2_del = new DeletionQuotientSpur(k1c2, efDistrib.concat(ghDistrib), {
|
||||
// segment: {
|
||||
// transitionId: key2Id,
|
||||
// start: 0
|
||||
// },
|
||||
// // Deletions always get their own unique subset ID.
|
||||
// subsetId: generateSubsetId(),
|
||||
// bestProbFromSet: efDistrib[0].p
|
||||
// });
|
||||
const k2c2_del = new DeletionQuotientSpur(k1c2, efDistrib.concat(ghDistrib), {
|
||||
segment: {
|
||||
transitionId: key2Id,
|
||||
start: 0
|
||||
},
|
||||
// Deletions always get their own unique subset ID.
|
||||
subsetId: generateSubsetId(),
|
||||
bestProbFromSet: efDistrib[0].p
|
||||
});
|
||||
const k2c2_ef = new SubstitutionQuotientSpur(k1c1, efDistrib, {
|
||||
segment: {
|
||||
transitionId: key2Id,
|
||||
|
|
@ -197,27 +198,27 @@ export function buildQuotientDocFixture() {
|
|||
subsetId: generateSubsetId(),
|
||||
bestProbFromSet: efDistrib[0].p
|
||||
});
|
||||
// const k2c2_gh = new SubstitutionQuotientSpur(k1c0, ghDistrib, {
|
||||
// segment: {
|
||||
// transitionId: key2Id,
|
||||
// start: 0
|
||||
// },
|
||||
// // Deletions always get their own unique subset ID.
|
||||
// subsetId: generateSubsetId(),
|
||||
// bestProbFromSet: efDistrib[0].p
|
||||
// });
|
||||
// const k2c2_ins = new InsertionQuotientSpur(k2c1);
|
||||
const k2c2 = new SearchQuotientCluster([/*k2c2_del,*/ k2c2_ef /*, k2c2_gh, k2c2_ins*/]);
|
||||
const k2c2_gh = new SubstitutionQuotientSpur(k1c0, ghDistrib, {
|
||||
segment: {
|
||||
transitionId: key2Id,
|
||||
start: 0
|
||||
},
|
||||
// Deletions always get their own unique subset ID.
|
||||
subsetId: generateSubsetId(),
|
||||
bestProbFromSet: efDistrib[0].p
|
||||
});
|
||||
const k2c2_ins = new InsertionQuotientSpur(k2c1);
|
||||
const k2c2 = new SearchQuotientCluster([k2c2_del, k2c2_ef, k2c2_gh, k2c2_ins]);
|
||||
|
||||
// const k2c3_del = new DeletionQuotientSpur(k1c3, efDistrib.concat(ghDistrib), {
|
||||
// segment: {
|
||||
// transitionId: key2Id,
|
||||
// start: 0
|
||||
// },
|
||||
// // Deletions always get their own unique subset ID.
|
||||
// subsetId: generateSubsetId(),
|
||||
// bestProbFromSet: efDistrib[0].p
|
||||
// });
|
||||
const k2c3_del = new DeletionQuotientSpur(k1c3, efDistrib.concat(ghDistrib), {
|
||||
segment: {
|
||||
transitionId: key2Id,
|
||||
start: 0
|
||||
},
|
||||
// Deletions always get their own unique subset ID.
|
||||
subsetId: generateSubsetId(),
|
||||
bestProbFromSet: efDistrib[0].p
|
||||
});
|
||||
const k2c3_ef = new SubstitutionQuotientSpur(k1c2, efDistrib, {
|
||||
segment: {
|
||||
transitionId: key2Id,
|
||||
|
|
@ -237,11 +238,11 @@ export function buildQuotientDocFixture() {
|
|||
bestProbFromSet: efDistrib[0].p
|
||||
});
|
||||
const k2c3_ins = new InsertionQuotientSpur(k2c2);
|
||||
const k2c3 = new SearchQuotientCluster([/*k2c3_del, */ k2c3_ef, k2c3_gh, k2c3_ins]);
|
||||
const k2c3 = new SearchQuotientCluster([k2c3_del, k2c3_ef, k2c3_gh, k2c3_ins]);
|
||||
|
||||
return {
|
||||
searchRoot,
|
||||
spurs: {sc1, sc2, k1c1_ab, k1c2_ab, k1c2_cd, k1c2_ins, k1c3_ab, k1c3_cd, k1c3_ins, k2c2_ef, k2c3_ef, k2c3_gh, k2c3_ins},
|
||||
nodes: {sc1, sc2, /* k1c0, */ k1c1, k1c2, k1c3, /* k2c0, k2c1, */ k2c2, k2c3}
|
||||
spurs: {sc1, sc2, k1c1_ab, k1c2_ab, k1c2_cd, k1c2_ins, k1c3_ab, k1c3_cd, k1c3_ins, k2c1_del, k2c2_del, k2c2_ef, k2c3_ef, k2c3_gh, k2c3_ins},
|
||||
nodes: {sc1, sc2, k1c0, k1c1, k1c2, k1c3, k2c0, k2c1, k2c2, k2c3}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,210 @@
|
|||
/*
|
||||
* Keyman is copyright (C) SIL Global. MIT License.
|
||||
*
|
||||
* Created by jahorton on 2026-03-06
|
||||
*
|
||||
* This file defines tests for the DeletionQuotientSpur class of the
|
||||
* predictive-text correction-search engine's search graph.
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { jsonFixture } from '@keymanapp/common-test-resources/model-helpers.mjs';
|
||||
import {
|
||||
DeletionQuotientSpur,
|
||||
models,
|
||||
SearchQuotientRoot,
|
||||
SubstitutionQuotientSpur
|
||||
} from '@keymanapp/lm-worker/test-index';
|
||||
|
||||
import TrieModel = models.TrieModel;
|
||||
|
||||
import { buildCantLinearFixture } from '../../helpers/buildCantLinearFixture.js';
|
||||
import { buildQuotientDocFixture } from '../../helpers/buildQuotientDocFixture.js';
|
||||
|
||||
import { analyzeQuotientNodeResults } from '../../helpers/analyzeQuotientNodeResults.js';
|
||||
|
||||
const testModel = new TrieModel(jsonFixture('models/tries/english-1000'));
|
||||
|
||||
describe('DeletionQuotientSpur', () => {
|
||||
describe('constructor', () => {
|
||||
it('may be extended from root path', () => {
|
||||
const rootPath = new SearchQuotientRoot(testModel);
|
||||
|
||||
const leadEdgeDistribution = [
|
||||
{sample: {insert: 't', deleteLeft: 0, id: 13 }, p: 0.5},
|
||||
{sample: {insert: 'a', deleteLeft: 0, id: 13 }, p: 0.3},
|
||||
{sample: {insert: 'o', deleteLeft: 0, id: 13 }, p: 0.2}
|
||||
];
|
||||
|
||||
const extendedPath = new DeletionQuotientSpur(rootPath, leadEdgeDistribution, leadEdgeDistribution[0]);
|
||||
|
||||
assert.equal(extendedPath.inputCount, 1);
|
||||
assert.equal(extendedPath.codepointLength, 0);
|
||||
assert.isNumber(extendedPath.spaceId);
|
||||
assert.notEqual(extendedPath.spaceId, rootPath.spaceId);
|
||||
assert.deepEqual(extendedPath.bestExample, { text: '', p: 1 } );
|
||||
|
||||
assert.deepEqual(extendedPath.parents, [rootPath]);
|
||||
assert.deepEqual(extendedPath.inputs, leadEdgeDistribution);
|
||||
|
||||
// Assert the root is unchanged.
|
||||
assert.equal(rootPath.inputCount, 0);
|
||||
|
||||
// Should (still) have codepointLength == 0 once it's defined.
|
||||
assert.deepEqual(rootPath.bestExample, {text: '', p: 1});
|
||||
assert.deepEqual(rootPath.parents, []);
|
||||
});
|
||||
|
||||
it('may be built from arbitrary prior SearchQuotientSpur', () => {
|
||||
const rootPath = new SearchQuotientRoot(testModel);
|
||||
|
||||
const leadEdgeDistribution = [
|
||||
{sample: {insert: 't', deleteLeft: 0, id: 13 }, p: 0.5},
|
||||
{sample: {insert: 'a', deleteLeft: 0, id: 13 }, p: 0.3},
|
||||
{sample: {insert: 'o', deleteLeft: 0, id: 13 }, p: 0.2}
|
||||
];
|
||||
const inputClone = leadEdgeDistribution.map(e => ({...e}));
|
||||
|
||||
const length1Path = new SubstitutionQuotientSpur(
|
||||
rootPath,
|
||||
leadEdgeDistribution,
|
||||
leadEdgeDistribution[0]
|
||||
);
|
||||
|
||||
const tailEdgeDistribution = [
|
||||
{sample: {insert: 'r', deleteLeft: 0, id: 17 }, p: 0.6},
|
||||
{sample: {insert: 'e', deleteLeft: 0, id: 17 }, p: 0.25},
|
||||
{sample: {insert: 'h', deleteLeft: 0, id: 17 }, p: 0.15}
|
||||
];
|
||||
|
||||
const length2Path = new DeletionQuotientSpur(
|
||||
length1Path,
|
||||
tailEdgeDistribution,
|
||||
tailEdgeDistribution[0]
|
||||
);
|
||||
|
||||
// Verify that the prior distribution remains fully unaltered.
|
||||
assert.deepEqual(leadEdgeDistribution, inputClone);
|
||||
|
||||
assert.equal(length2Path.inputCount, 2);
|
||||
assert.equal(length2Path.codepointLength, 1);
|
||||
assert.isNumber(length2Path.spaceId);
|
||||
assert.notEqual(length2Path.spaceId, length1Path.spaceId);
|
||||
assert.deepEqual(length2Path.bestExample, length1Path.bestExample);
|
||||
assert.deepEqual(length2Path.parents, [length1Path]);
|
||||
assert.deepEqual(length2Path.inputs, tailEdgeDistribution);
|
||||
assert.deepEqual(length2Path.inputSegments, [
|
||||
{
|
||||
transitionId: leadEdgeDistribution[0].sample.id,
|
||||
start: 0
|
||||
}, {
|
||||
transitionId: tailEdgeDistribution[0].sample.id,
|
||||
start: 0
|
||||
}
|
||||
]);
|
||||
|
||||
assert.equal(length1Path.inputCount, 1);
|
||||
assert.equal(length1Path.codepointLength, 1);
|
||||
assert.isNumber(length1Path.spaceId);
|
||||
assert.notEqual(length1Path.spaceId, rootPath.spaceId);
|
||||
assert.deepEqual(length1Path.bestExample, {text: 't', p: 0.5});
|
||||
assert.deepEqual(length1Path.parents, [rootPath]);
|
||||
assert.deepEqual(length1Path.inputs, leadEdgeDistribution);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.edgeKey', () => {
|
||||
it('is different for different delete locations', () => {
|
||||
const {k1c0, k2c0} = buildQuotientDocFixture().nodes;
|
||||
|
||||
assert.notEqual(k2c0.edgeKey, k1c0.edgeKey);
|
||||
});
|
||||
|
||||
it('is different from the parent node\'s key', () => {
|
||||
const { k1c1_ab, k2c1_del, k1c2_cd, k2c2_del } = buildQuotientDocFixture().spurs;
|
||||
|
||||
assert.notEqual(k2c1_del.edgeKey, k1c1_ab.edgeKey);
|
||||
assert.notEqual(k2c2_del.edgeKey, k1c2_cd.edgeKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleNextNode()', () => {
|
||||
it('does not output results that directly match inputs', () => {
|
||||
const caPath = buildCantLinearFixture().paths[2];
|
||||
const distrib = buildCantLinearFixture().distributions[2]; // for the third entry.
|
||||
const deletionPath = new DeletionQuotientSpur(caPath, distrib, distrib[0]);
|
||||
|
||||
const matchTargets = [
|
||||
'can',
|
||||
'car',
|
||||
'cen', // 'cent' and 'center' are supported in this test model.
|
||||
];
|
||||
const analysis = analyzeQuotientNodeResults(deletionPath, matchTargets);
|
||||
|
||||
assert.sameMembers(analysis.found, []);
|
||||
assert.sameMembers(analysis.missing, matchTargets);
|
||||
assert.isEmpty(analysis.foundWithDuplicates);
|
||||
});
|
||||
|
||||
it('does not output results that substitute inputs', () => {
|
||||
const caPath = buildCantLinearFixture().paths[2];
|
||||
const distrib = buildCantLinearFixture().distributions[2]; // for the third entry.
|
||||
const deletionPath = new DeletionQuotientSpur(caPath, distrib, distrib[0]);
|
||||
|
||||
const matchTargets = [
|
||||
// Replacement of first char
|
||||
'man',
|
||||
'far',
|
||||
// Replacement of second char
|
||||
'con', // 'consider' and variants thereof are also supported.
|
||||
'cor', // 'corner'
|
||||
// Replacement of third char
|
||||
'cal', // 'call'
|
||||
];
|
||||
const analysis = analyzeQuotientNodeResults(deletionPath, matchTargets);
|
||||
|
||||
assert.sameMembers(analysis.found, []);
|
||||
assert.sameMembers(analysis.missing, matchTargets);
|
||||
assert.isEmpty(analysis.foundWithDuplicates);
|
||||
});
|
||||
|
||||
it('does not output results that insert characters as needed', () => {
|
||||
const caPath = buildCantLinearFixture().paths[2];
|
||||
const distrib = buildCantLinearFixture().distributions[2]; // for the third entry.
|
||||
const deletionPath = new DeletionQuotientSpur(caPath, distrib, distrib[0]);
|
||||
|
||||
const matchTargets = [
|
||||
'can', // 'can' + (insert) 'n' (=> 'cannot')
|
||||
'car', // 'car' + (insert) 'e'
|
||||
'ran', // 'ran' + (insert) 'g' (=> 'range')
|
||||
'cann', // 'can' + (insert) 'n' (=> 'cannot')
|
||||
'care', // 'car' + (insert) 'e'
|
||||
'rang', // 'ran' + (insert) 'g' (=> 'range')
|
||||
];
|
||||
const analysis = analyzeQuotientNodeResults(deletionPath, matchTargets);
|
||||
|
||||
assert.sameMembers(analysis.found, []);
|
||||
assert.sameMembers(analysis.missing, matchTargets);
|
||||
assert.isEmpty(analysis.foundWithDuplicates);
|
||||
});
|
||||
|
||||
it('outputs results that delete incoming keystrokes as needed', () => {
|
||||
const caPath = buildCantLinearFixture().paths[2];
|
||||
const distrib = buildCantLinearFixture().distributions[2]; // for the third entry.
|
||||
const deletionPath = new DeletionQuotientSpur(caPath, distrib, distrib[0]);
|
||||
|
||||
const matchTargets = [
|
||||
// Deletes the last char.
|
||||
'ca',
|
||||
'ce',
|
||||
're'
|
||||
];
|
||||
const analysis = analyzeQuotientNodeResults(deletionPath, matchTargets);
|
||||
|
||||
assert.sameMembers(analysis.found, matchTargets);
|
||||
assert.sameMembers(analysis.missing, []);
|
||||
assert.isEmpty(analysis.foundWithDuplicates);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -11,6 +11,7 @@ import { assert } from 'chai';
|
|||
|
||||
import { jsonFixture } from '@keymanapp/common-test-resources/model-helpers.mjs';
|
||||
import {
|
||||
DeletionQuotientSpur,
|
||||
InsertionQuotientSpur,
|
||||
models,
|
||||
SearchQuotientRoot,
|
||||
|
|
@ -166,8 +167,26 @@ describe('InsertionQuotientSpur', () => {
|
|||
assert.isEmpty(analysis.foundWithDuplicates);
|
||||
});
|
||||
|
||||
it.skip('does not output results when immediately following a deletion spur edit', () => {
|
||||
it('does not output results when immediately following a deletion spur edit', () => {
|
||||
const caPath = buildCantLinearFixture().paths[2];
|
||||
const distrib = buildCantLinearFixture().distributions[2]; // for the third entry.
|
||||
const deletionPath = new DeletionQuotientSpur(caPath, distrib, distrib[0]);
|
||||
|
||||
const followingInsert = new InsertionQuotientSpur(deletionPath);
|
||||
|
||||
for(
|
||||
let searchResult = followingInsert.handleNextNode();
|
||||
searchResult.type != 'none';
|
||||
searchResult = followingInsert.handleNextNode()
|
||||
) {
|
||||
// While the insertion quotient node will forward results from the deletion version,
|
||||
// it should never actually build search routes that go delete -> insert directly.
|
||||
if(searchResult.type == 'complete') {
|
||||
assert.notEqual(searchResult.mapping.lastEdgeType, 'insertion');
|
||||
assert.notEqual(searchResult.mapping.spaceId, followingInsert.spaceId);
|
||||
}
|
||||
}
|
||||
// We should reach the end of the loop without once processing an actual 'insert' case.
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue