···11export interface StringComparisonOptions {
22+ /**
33+ * An array of transformations to apply to each string before comparing similarity
44+ * */
25 transforms?: StringTransformFunc[]
66+ /**
77+ * An array of strategies used to score similarity. All strategies scores are combined for an average high score.
88+ * */
39 strategies?: ComparisonStrategy<ComparisonStrategyResultValue>[]
1010+ /**
1111+ * Reorder second string so its token match order of first string as closely as possible
1212+ *
1313+ * Useful when only the differences in content are important, but not the order of the content
1414+ * */
1515+ reorder?: boolean
416}
517618export interface StringSamenessResult {
+39-1
src/index.ts
···2525 const {
2626 transforms = strDefaultTransforms,
2727 strategies = defaultStrategies,
2828+ reorder = false,
2829 } = options || {};
29303031 const cleanA = transforms.reduce((acc, curr) => curr(acc), valA);
3131- const cleanB = transforms.reduce((acc, curr) => curr(acc), valB);
3232+ let cleanB = transforms.reduce((acc, curr) => curr(acc), valB);
32333334 const shortest = cleanA.length > cleanB.length ? cleanB : cleanA;
3535+3636+ if (reorder) {
3737+ // we want to ignore order of tokens as much as possible (user does not care about differences in word order, just absolute differences in characters overall)
3838+ // so we will reorder cleanB so its tokens match the order or tokens in cleanA as closely as possible
3939+ // before we run strategies
4040+ cleanB = reorderStr(cleanA, cleanB);
4141+ }
34423543 const stratResults: NamedComparisonStrategyObjectResult[] = [];
3644···6674 highScore,
6775 highScoreWeighted,
6876 }
7777+}
7878+7979+export const reorderStr = (cleanA: string, cleanB: string, options?: StringComparisonOptions): string => {
8080+ // to do the reordering we will use stringSameness with the provided strats to match against each token in cleanA and choose the closest token in cleanB
8181+ // and add the end concat any remaining tokens from cleanB to the reordered string
8282+ const aTokens = cleanA.split(' ');
8383+ const bTokens = cleanB.split(' ');
8484+ const orderedCandidateTokens = aTokens.reduce((acc: { ordered: string[], remaining: string[] }, curr) => {
8585+ let highScore = 0;
8686+ let highIndex: undefined | number = 0;
8787+ let index = 0;
8888+ for (const token of acc.remaining) {
8989+ const result = stringSameness(curr, token, {...options, reorder: false});
9090+ if (result.highScore > highScore) {
9191+ highScore = result.highScore;
9292+ highIndex = index;
9393+ }
9494+ index++;
9595+ }
9696+9797+ const splicedRemaining = [...acc.remaining];
9898+ if(highIndex <= splicedRemaining.length - 1) {
9999+ splicedRemaining.splice(highIndex, 1);
100100+ }
101101+ const ordered = highIndex <= acc.remaining.length - 1 ? acc.ordered.concat(acc.remaining[highIndex]) : acc.ordered;
102102+103103+ return {ordered: ordered, remaining: splicedRemaining};
104104+ }, {ordered: [], remaining: bTokens});
105105+ const allOrderedCandidateTokens = orderedCandidateTokens.ordered.concat(orderedCandidateTokens.remaining);
106106+ return allOrderedCandidateTokens.join(' ');
69107}
7010871109const createStringSameness = (defaults: StringComparisonOptions) => {
+61-1
tests/index.test.ts
···44 stringSameness,
55 defaultStrCompareTransformFuncs as transforms,
66 strategies,
77- defaultStrategies
77+ reorderStr
88} from '../src/index.js';
991010const sameString = 'this string is the same';
···7474 it(`${name}: strings are close but not identical`, function() {
7575 const res = strat.strategy('Another Brick in the Wall, Pt. 1', 'Another Brick in the Wall, Pt. 2');
7676 assert.isAtMost(res.score, 99);
7777+ assert.isAtLeast(res.score, 80);
7778 });
7879 }
7980 });
···9091 });
91929293});
9494+9595+9696+describe('String reordering', () => {
9797+9898+ it('should reorder exact strings', function () {
9999+100100+ const exact = [
101101+ ['this is correct order', 'order correct this is'],
102102+ ['we have a few it he close words', 'close it he a words have we few'],
103103+ ]
104104+105105+ for(const test of exact) {
106106+ assert.equal(reorderStr(test[0], test[1]), test[0])
107107+ }
108108+ });
109109+110110+ it('should reorder strings as closely as possible', function () {
111111+112112+ const a = 'this is correct order';
113113+ const b = 'order correct this is and extra';
114114+ const expected = 'this is correct order and extra'
115115+116116+ assert.equal(reorderStr(a, b), expected);
117117+118118+ const aLong = 'this vorrect order with more tokens';
119119+ const bLong = 'order correct this extra';
120120+ const expectedLong = 'this correct order extra';
121121+122122+ assert.equal(reorderStr(aLong, bLong), expectedLong);
123123+124124+ });
125125+126126+});
127127+128128+describe('Sameness functionality', function() {
129129+130130+ describe('Sanity Checks', function() {
131131+ it(`scores same strings as identical`, function() {
132132+ const res = stringSameness(sameString, sameString);
133133+ assert.equal(res.highScore, 100);
134134+ });
135135+136136+ it(`scores similar strings highly but not as identical`, function() {
137137+ const res = stringSameness('Another Brick in the Wall, Pt. 1', 'Another Brick in the Wall, Pt. 2');
138138+ assert.isAtMost(res.highScore, 99);
139139+ assert.isAtLeast(res.highScore, 95);
140140+ });
141141+142142+ it(`scores completely different strings as near zero`, function() {
143143+ const res = stringSameness(sameString, 'pay bull blood for voice');
144144+ assert.isAtMost(res.highScore, 10);
145145+ });
146146+ });
147147+148148+ it('should score reordered exact strings as identical', function () {
149149+ const res = stringSameness('this is correct order', 'order correct this is', {reorder: true});
150150+ assert.equal(res.highScore, 100);
151151+ });
152152+});