@acusti/matchmaking
Advanced tools
Comparing version 0.6.1 to 0.7.0
export declare const getMatchScore: (strA: string, strB: string, isSanitized?: boolean) => number; | ||
type Payload = { | ||
excludeMismatches?: boolean; | ||
items: Array<string>; | ||
text: string; | ||
}; | ||
export declare const sortByBestMatch: ({ items, text }: Payload) => string[]; | ||
export declare const sortByBestMatch: ({ excludeMismatches, items, text }: Payload) => string[]; | ||
export declare const getBestMatch: (payload: Payload) => string; | ||
export {}; |
@@ -1,4 +0,4 @@ | ||
const FIRST_NUMBER = '0'.charCodeAt(0); | ||
const LAST_NUMBER = '9'.charCodeAt(0); | ||
const FIRST_LETTER = 'A'.charCodeAt(0); | ||
const FIRST_NUMBER = 48; // '0'.charCodeAt(0); | ||
const LAST_NUMBER = 57; // '9'.charCodeAt(0); | ||
const FIRST_LETTER = 65; // 'A'.charCodeAt(0); | ||
// Consider every distance greater than or equal to 15 to be functionally equivalent | ||
@@ -8,2 +8,3 @@ const MAX_DISTANCE = 15; | ||
const MAX_PARTIAL_EXACT_SCORE = 0.999999; | ||
const MIN_PARTIAL_EXACT_SCORE = 0.81; // cut-off for partial match vs mismatch | ||
export const getMatchScore = (strA, strB, isSanitized) => { | ||
@@ -28,9 +29,9 @@ if (!isSanitized) { | ||
if (matchStart > -1) { | ||
// Maximum penalty for distance from beginning is 0.25 | ||
const penaltyPerStep = 0.25 / (strLonger.length - 2); | ||
const penaltyPerStep = 0.15 / (strLonger.length - 2); | ||
const penalty = penaltyPerStep * matchStart; | ||
return MAX_PARTIAL_EXACT_SCORE - penalty; | ||
// Apply a minimum to partial exact matches | ||
return Math.max(MAX_PARTIAL_EXACT_SCORE - penalty, MIN_PARTIAL_EXACT_SCORE); | ||
} | ||
// To proportionally weight consecutive exact matches, increase bonus relative to total length | ||
const bonusMultiplier = Math.min(0.25, 3 / shortestLength); | ||
const bonusMultiplier = Math.min(0.4, 3 / shortestLength); | ||
let score = 0; | ||
@@ -67,2 +68,3 @@ let worstPossibleScore = 0; | ||
return MAX_INEXACT_SCORE; | ||
// Invert score and normalize into a range based on worst possible score | ||
score = (worstPossibleScore - score) / worstPossibleScore; | ||
@@ -72,3 +74,3 @@ // Don’t allow an inexact match to get a score of 1 (reserved for an exact match) | ||
}; | ||
export const sortByBestMatch = ({ items, text }) => { | ||
export const sortByBestMatch = ({ excludeMismatches, items, text }) => { | ||
text = text.trim().toUpperCase(); | ||
@@ -80,2 +82,5 @@ if (!text || !items.length) | ||
const itemScore = getMatchScore(item.trim().toUpperCase(), text, true); | ||
if (excludeMismatches && itemScore < MIN_PARTIAL_EXACT_SCORE) { | ||
return [sorted, scores]; | ||
} | ||
let index = sorted.length; | ||
@@ -82,0 +87,0 @@ if (itemScore > scores[0]) { |
@@ -120,2 +120,28 @@ import { describe, expect, it } from 'vitest'; | ||
}); | ||
it('excludes non-matches when excludeMismatches is true', () => { | ||
expect(sortByBestMatch({ excludeMismatches: true, items: STATES, text: 'ma' })).toEqual([ | ||
'Maine', // exact match | ||
'Maryland', // exact match | ||
'Massachusetts', // exact match | ||
'Kansas', // partial match | ||
'Hawaii', // partial match | ||
'Michigan', // partial match (further distance on 2nd letter) | ||
'Minnesota', // partial match (further distance on 2nd letter) | ||
'Mississippi', // partial match (further distance on 2nd letter) | ||
'Missouri', // partial match (further distance on 2nd letter) | ||
'Alabama', // end-of-text exact match | ||
'Oklahoma', // end-of-text exact match | ||
'Nebraska', // short distance from match | ||
'Nevada', // short distance from match | ||
'New Hampshire', // short distance from match | ||
'New Jersey', // short distance from match | ||
'New Mexico', // short distance from match | ||
'New York', // short distance from match | ||
]); | ||
expect(sortByBestMatch({ | ||
excludeMismatches: true, | ||
items: CSS_VALUES, | ||
text: '11', | ||
})).toEqual(['11px', '128px', '18px']); | ||
}); | ||
}); | ||
@@ -122,0 +148,0 @@ describe('getBestMatch', () => { |
{ | ||
"name": "@acusti/matchmaking", | ||
"version": "0.6.1", | ||
"version": "0.7.0", | ||
"type": "module", | ||
@@ -5,0 +5,0 @@ "sideEffects": false, |
@@ -34,2 +34,3 @@ # @acusti/matchmaking | ||
type Payload = { | ||
excludeMismatches?: boolean; | ||
items: Array<string>; | ||
@@ -43,2 +44,26 @@ text: string; | ||
`getBestMatch` just returns the text of the single closest match found | ||
(i.e. `sortByBestMatch(payload)[0]`); | ||
(i.e. `sortByBestMatch(payload)[0]`). | ||
The `excludeMismatches` option for `sortByBestMatch` allows that function to also filter the results to only include (partial) matches, where a match include fuzzy matches that are strong enough to qualify as a partial match. For example, considering a list of all states in alphabetical order, searching for the text `"ma"` with `excludeMismatches: true` returns: | ||
```js | ||
[ | ||
'Maine', // exact match | ||
'Maryland', // exact match | ||
'Massachusetts', // exact match | ||
'Kansas', // partial match | ||
'Hawaii', // partial match | ||
'Michigan', // partial match (further distance on 2nd letter) | ||
'Minnesota', // partial match (further distance on 2nd letter) | ||
'Mississippi', // partial match (further distance on 2nd letter) | ||
'Missouri', // partial match (further distance on 2nd letter) | ||
'Alabama', // end-of-text exact match | ||
'Oklahoma', // end-of-text exact match | ||
'Nebraska', // short distance from match | ||
'Nevada', // short distance from match | ||
'New Hampshire', // short distance from match | ||
'New Jersey', // short distance from match | ||
'New Mexico', // short distance from match | ||
'New York', // short distance from match | ||
] | ||
``` |
@@ -137,2 +137,34 @@ import { describe, expect, it } from 'vitest'; | ||
}); | ||
it('excludes non-matches when excludeMismatches is true', () => { | ||
expect( | ||
sortByBestMatch({ excludeMismatches: true, items: STATES, text: 'ma' }), | ||
).toEqual([ | ||
'Maine', // exact match | ||
'Maryland', // exact match | ||
'Massachusetts', // exact match | ||
'Kansas', // partial match | ||
'Hawaii', // partial match | ||
'Michigan', // partial match (further distance on 2nd letter) | ||
'Minnesota', // partial match (further distance on 2nd letter) | ||
'Mississippi', // partial match (further distance on 2nd letter) | ||
'Missouri', // partial match (further distance on 2nd letter) | ||
'Alabama', // end-of-text exact match | ||
'Oklahoma', // end-of-text exact match | ||
'Nebraska', // short distance from match | ||
'Nevada', // short distance from match | ||
'New Hampshire', // short distance from match | ||
'New Jersey', // short distance from match | ||
'New Mexico', // short distance from match | ||
'New York', // short distance from match | ||
]); | ||
expect( | ||
sortByBestMatch({ | ||
excludeMismatches: true, | ||
items: CSS_VALUES, | ||
text: '11', | ||
}), | ||
).toEqual(['11px', '128px', '18px']); | ||
}); | ||
}); | ||
@@ -139,0 +171,0 @@ |
@@ -1,4 +0,4 @@ | ||
const FIRST_NUMBER = '0'.charCodeAt(0); | ||
const LAST_NUMBER = '9'.charCodeAt(0); | ||
const FIRST_LETTER = 'A'.charCodeAt(0); | ||
const FIRST_NUMBER = 48; // '0'.charCodeAt(0); | ||
const LAST_NUMBER = 57; // '9'.charCodeAt(0); | ||
const FIRST_LETTER = 65; // 'A'.charCodeAt(0); | ||
@@ -9,2 +9,3 @@ // Consider every distance greater than or equal to 15 to be functionally equivalent | ||
const MAX_PARTIAL_EXACT_SCORE = 0.999999; | ||
const MIN_PARTIAL_EXACT_SCORE = 0.81; // cut-off for partial match vs mismatch | ||
@@ -31,10 +32,10 @@ export const getMatchScore = (strA: string, strB: string, isSanitized?: boolean) => { | ||
if (matchStart > -1) { | ||
// Maximum penalty for distance from beginning is 0.25 | ||
const penaltyPerStep = 0.25 / (strLonger.length - 2); | ||
const penaltyPerStep = 0.15 / (strLonger.length - 2); | ||
const penalty = penaltyPerStep * matchStart; | ||
return MAX_PARTIAL_EXACT_SCORE - penalty; | ||
// Apply a minimum to partial exact matches | ||
return Math.max(MAX_PARTIAL_EXACT_SCORE - penalty, MIN_PARTIAL_EXACT_SCORE); | ||
} | ||
// To proportionally weight consecutive exact matches, increase bonus relative to total length | ||
const bonusMultiplier = Math.min(0.25, 3 / shortestLength); | ||
const bonusMultiplier = Math.min(0.4, 3 / shortestLength); | ||
let score = 0; | ||
@@ -73,3 +74,3 @@ let worstPossibleScore = 0; | ||
if (score <= 0) return MAX_INEXACT_SCORE; | ||
// Invert score and normalize into a range based on worst possible score | ||
score = (worstPossibleScore - score) / worstPossibleScore; | ||
@@ -81,2 +82,3 @@ // Don’t allow an inexact match to get a score of 1 (reserved for an exact match) | ||
type Payload = { | ||
excludeMismatches?: boolean; | ||
items: Array<string>; | ||
@@ -86,3 +88,3 @@ text: string; | ||
export const sortByBestMatch = ({ items, text }: Payload) => { | ||
export const sortByBestMatch = ({ excludeMismatches, items, text }: Payload) => { | ||
text = text.trim().toUpperCase(); | ||
@@ -94,2 +96,6 @@ if (!text || !items.length) return items; | ||
const itemScore = getMatchScore(item.trim().toUpperCase(), text, true); | ||
if (excludeMismatches && itemScore < MIN_PARTIAL_EXACT_SCORE) { | ||
return [sorted, scores]; | ||
} | ||
let index = sorted.length; | ||
@@ -96,0 +102,0 @@ if (itemScore > scores[0]) { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
33057
529
68