diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-cluster.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-cluster.ts index 88202f5dbd4..54b285f69cd 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-cluster.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-cluster.ts @@ -182,4 +182,25 @@ export class SearchQuotientCluster implements SearchQuotientNode { split(charIndex: number): [SearchQuotientNode, SearchQuotientNode] { throw new Error('Method not implemented.'); } + + isSameNode(space: SearchQuotientNode): boolean { + // Easiest cases: when the instances or their ' `spaceId` matches, we have + // a perfect match. + if(this == space || this.spaceId == space.spaceId) { + return true; + } + + // If it's falsy or a different SearchSpace type, that's an easy filter. + if(!space || !(space instanceof SearchQuotientCluster)) { + return false; + } + + // We need to check if the parents match. Done naively in the manner below, this is O(N^2). + // Granted, we shouldn't have _that_ many incoming paths. + if(this.parents.find((path) => !space.parents.find((path2) => path.isSameNode(path2)))) { + return false; + } + + return true; + } } \ No newline at end of file diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-node.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-node.ts index c1859b11287..c88f2f33301 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-node.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-node.ts @@ -207,4 +207,12 @@ export interface SearchQuotientNode { * @param charIndex */ split(charIndex: number): [SearchQuotientNode, SearchQuotientNode]; + + /** + * Determines if the SearchQuotientNode is a duplicate of another instance. + * For such cases, the total search space covered by the quotient-graph + * path(s) taken to reach each must be 100% identical. + * @param node + */ + isSameNode(node: SearchQuotientNode): boolean; } \ No newline at end of file diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-root.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-root.ts index 2af16de8c8e..61399815609 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-root.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-root.ts @@ -98,6 +98,25 @@ export class SearchQuotientRoot implements SearchQuotientNode { return ''; } + isSameNode(space: SearchQuotientNode): boolean { + // Easiest cases: when the instances or their ' `spaceId` matches, we have + // a perfect match. + if(this == space || this.spaceId == space.spaceId) { + return true; + } + + // If it's falsy or a different SearchQuotientNode type, that's an easy filter. + if(!space || !(space instanceof SearchQuotientRoot)) { + return false; + } + + // The two should be based upon the same LexicalModel. + if(this.model != space.model) { + return false; + } + return true; + } + split(charIndex: number): [SearchQuotientNode, SearchQuotientNode] { return [this, new SearchQuotientRoot(this.model)]; } diff --git a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-spur.ts b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-spur.ts index d32cfa8becb..034aab1ddd6 100644 --- a/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-spur.ts +++ b/web/src/engine/predictive-text/worker-thread/src/main/correction/search-quotient-spur.ts @@ -495,4 +495,26 @@ export abstract class SearchQuotientSpur implements SearchQuotientNode { const segment = inputSrc.segment; return `E${inputSrc.subsetId}:${segment.start}${segment.end !== undefined ? `-${segment.end}` : ''}`; } + + isSameNode(space: SearchQuotientNode): boolean { + // Easiest cases: when the instances or their ' `spaceId` matches, we have + // a perfect match. + if(this == space || this.spaceId == space.spaceId) { + return true; + } + + // If it's falsy or a different SearchSpace type, that's an easy filter. + if(!space || !(space instanceof SearchQuotientSpur)) { + return false; + } + + // If this spur does not match the exact same edge conditions as the other spur, + // they are not duplicates. + if(this.edgeKey != space.edgeKey) { + return false; + } + + // Finally, we recursively verify that the parent matches. + return this.parentNode.isSameNode(space.parentNode); + } } diff --git a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-spur.tests.ts b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-spur.tests.ts index a8a32fa8b4f..4238ca0594e 100644 --- a/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-spur.tests.ts +++ b/web/src/test/auto/headless/engine/predictive-text/worker-thread/correction-search/search-quotient-spur.tests.ts @@ -1850,6 +1850,7 @@ describe('SearchQuotientSpur', () => { assert.equal(remergedPath.codepointLength, mergeTarget.codepointLength); assert.sameDeepOrderedMembers(remergedPath.inputSegments, mergeTarget.inputSegments); assert.isTrue(quotientPathHasInputs(remergedPath, trueDistributions)); + assert.isTrue(remergedPath.isSameNode(mergeTarget)); } it('setup: constructs path properly', () => { @@ -1960,6 +1961,7 @@ describe('SearchQuotientSpur', () => { assert.isTrue(remerged instanceof SearchQuotientSpur); assert.deepEqual((remerged as SearchQuotientSpur).inputs, inputDistribution); assert.isTrue(quotientPathHasInputs(remerged, [[startSample], inputDistribution])); + assert.isTrue(remerged.isSameNode(mergeTarget)); }); }); }); \ No newline at end of file