@samual/match
Version:
A fork of a fork of match-sorter with separated filtering, sorting phases, and match locations.
147 lines (146 loc) • 6.55 kB
JavaScript
/**
* @name match-sorter
* @license MIT license.
* @copyright (c) 2099 Kent C. Dodds
* @author Kent C. Dodds <me@kentcdodds.com> (https://kentcdodds.com)
*/
const rankings = {
CASE_SENSITIVE_EQUAL: 7,
EQUAL: 6,
STARTS_WITH: 5,
WORD_STARTS_WITH: 4,
CONTAINS: 3,
ACRONYM: 2,
MATCHES: 1,
NO_MATCH: 0
}
function rankItem(item, value, options) {
;(options = options || {}).threshold = options.threshold ?? 1
if (!options.accessors) {
const matchRanking = getMatchRanking(item, value, options)
return {
rankedValue: item,
...matchRanking,
accessorIndex: -1,
accessorThreshold: options.threshold,
passed: matchRanking.rank >= options.threshold
}
}
const valuesToRank = (function (item, accessors) {
const allValues = []
for (let j = 0, J = accessors.length; j < J; j++) {
const accessor = accessors[j],
attributes = getAccessorAttributes(accessor),
itemValues = getItemValues(item, accessor)
for (let i = 0, I = itemValues.length; i < I; i++) allValues.push({ itemValue: itemValues[i], attributes })
}
return allValues
})(item, options.accessors)
let bestMatchRanking = { rank: 0 },
passed = !1,
accessorIndex = -1,
accessorThreshold = options.threshold,
rankedValue = item
for (let i = 0; i < valuesToRank.length; i++) {
const rankValue = valuesToRank[i]
let matchRanking = getMatchRanking(rankValue.itemValue, value, options),
newRank = matchRanking.rank
const { minRanking, maxRanking, threshold = options.threshold } = rankValue.attributes
newRank < minRanking && newRank >= 1 ? (newRank = minRanking) : newRank > maxRanking && (newRank = maxRanking)
newRank = Math.min(newRank, maxRanking)
if (
newRank >= threshold &&
(newRank > bestMatchRanking.rank ||
(1 == matchRanking.rank &&
1 == bestMatchRanking.rank &&
matchRanking.closeness > bestMatchRanking.closeness))
) {
bestMatchRanking = matchRanking
passed = !0
accessorIndex = i
accessorThreshold = threshold
rankedValue = rankValue.itemValue
}
}
return { rankedValue, accessorIndex, accessorThreshold, passed, ...bestMatchRanking }
}
const similarCharacters =
"\\s\n(?:AE|Æ|Ǽ)\n(?:ae|æ|ǽ)\n(?:IJ|IJ)\n(?:ij|ij)\n(?:OE|Œ)\n(?:oe|œ)\n(?:TH|Þ)\n(?:th|þ)\n(?:A|À|Á|Â|Ã|Ä|Å|Ấ|Ắ|Ẳ|Ẵ|Ặ|Ầ|Ằ|Ȃ|Ā|Ă|Ą|Ǎ|Ǻ|A|Ȁ|A)\n(?:C|Ç|Ḉ|Ć|Ĉ|Ċ|Č|C|Č)\n(?:E|È|É|Ê|Ë|Ế|Ḗ|Ề|Ḕ|Ḝ|Ȇ|Ē|Ĕ|Ė|Ę|Ě|E|Ȅ|Ê|Ȩ|Ɛ)\n(?:I|Ì|Í|Î|Ï|Ḯ|Ȋ|Ĩ|Ī|Ĭ|Į|İ|Ǐ|I|Ȉ|I|Ɨ)\n(?:D|Ð|Ď|Đ|Ḑ)\n(?:N|Ñ|Ń|Ņ|Ň|N|Ǹ)\n(?:O|Ò|Ó|Ô|Õ|Ö|Ø|Ố|Ṍ|Ṓ|Ȏ|Ō|Ŏ|Ő|Ơ|Ǒ|Ǿ|Ồ|Ṑ|Ȍ|O)\n(?:U|Ù|Ú|Û|Ü|Ũ|Ū|Ŭ|Ů|Ű|Ų|Ȗ|Ư|Ǔ|Ǖ|Ǘ|Ǚ|Ǜ|Ứ|Ṹ|Ừ|Ȕ|U)\n(?:Y|Ý|Ŷ|Ÿ|Y|Ỳ|Y)\n(?:a|à|á|â|ã|ä|å|ấ|ắ|ẳ|ẵ|ặ|ầ|ằ|ȃ|ā|ă|ą|ǎ|ǻ|a|ȁ|a)\n(?:c|ç|ḉ|ć|ĉ|ċ|č|c|č)\n(?:e|è|é|ê|ë|ế|ḗ|ề|ḕ|ḝ|ȇ|ē|ĕ|ė|ę|ě|e|ȅ|ê|ȩ|ɛ)\n(?:i|ì|í|î|ï|ḯ|ȋ|ĩ|ī|ĭ|į|ı|ǐ|i|ȉ|i|ɨ)\n(?:d|ð|ď|đ|ḑ)\n(?:n|ñ|ń|ņ|ň|ʼn|n|ǹ)\n(?:o|ò|ó|ô|õ|ö|ø|ố|ṍ|ṓ|ȏ|ō|ŏ|ő|ơ|ǒ|ǿ|ồ|ṑ|ȍ|o)\n(?:u|ù|ú|û|ü|ũ|ū|ŭ|ů|ű|ų|ȗ|ư|ǔ|ǖ|ǘ|ǚ|ǜ|ứ|ṹ|ừ|ȕ|u)\n(?:y|ý|ÿ|ŷ|y|ỳ|y)\n(?:G|Ĝ|Ǵ|Ğ|Ġ|Ģ|Ǧ)\n(?:g|ĝ|ǵ|ğ|ġ|ģ|ǧ)\n(?:H|Ĥ|Ħ|Ḫ|Ȟ|Ḩ)\n(?:h|ĥ|ħ|ḫ|ȟ|ḩ)\n(?:J|Ĵ|J)\n(?:j|ĵ|ǰ)\n(?:K|Ķ|Ḱ|K|Ǩ)\n(?:k|ķ|ḱ|k|ǩ)\n(?:L|Ĺ|Ļ|Ľ|Ŀ)\n(?:l|ĺ|ļ|ľ|ŀ|Ł|ł)\n(?:M|Ḿ|M|M|M)\n(?:m|ḿ|m|m|m)\n(?:P|P|Ṕ|P)\n(?:p|p|ṕ|p)\n(?:R|Ŕ|Ŗ|Ř|R|Ȓ|Ȑ|Ř)\n(?:r|ŕ|ŗ|ř|r|ȓ|ȑ|ř)\n(?:S|Ś|Ŝ|Ş|Ș|Š|Ṥ|Ṧ)\n(?:s|ś|ŝ|ș|ş|š|ſ|ṥ|ṧ)\n(?:T|Ţ|Ț|Ť|Ŧ|T)\n(?:t|ţ|ț|ť|ŧ|t)\n(?:V|V|V)\n(?:v|v|v)\n(?:W|Ŵ|Ẃ|Ẁ|W)\n(?:w|ŵ|ẃ|ẁ|w)\n(?:X|X|X|X|X)\n(?:x|x|x|x|x)\n(?:Z|Ź|Ż|Ž|Z)\n(?:z|ź|ż|ž|z)\n(?:f|ƒ|f)\n(?:Г|Ѓ)\n(?:г|ѓ)\n(?:К|Ќ)\n(?:к|ќ)\n(?:B|B|B)\n(?:b|b|b)\n(?:F|F)\n(?:Q|Q|Q)\n(?:q|q|q)".split(
"\n"
)
function searchRegex(search) {
for (const group of similarCharacters) search = search.replace(RegExp(group, "g"), group)
return search
}
function getMatchRanking(testString, stringToRank, options) {
let match,
searchString = stringToRank
options.keepDiacritics || (searchString = searchRegex(searchString))
if (RegExp(`^${searchString}$`).test(testString)) return { rank: 7 }
if (RegExp(`^${searchString}$`, "i").test(testString)) return { rank: 6 }
if ((match = RegExp("^" + searchString, "i").exec(testString))) return { rank: 5, length: match[0].length }
if ((match = RegExp("\\s" + searchString, "i").exec(testString)))
return { rank: 4, index: match.index + 1, length: match[0].length - 1 }
if ((match = RegExp(searchString, "i").exec(testString)))
return { rank: 3, index: match.index, length: match[0].length }
if (
(match = RegExp(
stringToRank
.split("")
.map(character => `(^|\\s|-)(${searchRegex(character)})(.*)`)
.join("")
).exec(testString))
) {
const indexes = []
let index = match.index
for (const [arrayIndex, value] of match.slice(1).entries()) {
;(arrayIndex + 2) % 3 || indexes.push(index)
index += value.length
}
return { rank: 2, indexes }
}
return (function (testString, stringToRank) {
let matchingInOrderCharCount = 0,
charNumber = 0
function findMatchingCharacter(matchChar, string, index) {
for (let j = index, J = string.length; j < J; j++)
if (string[j] === matchChar) {
matchingInOrderCharCount += 1
return j + 1
}
return -1
}
const firstIndex = findMatchingCharacter(stringToRank[0], testString, 0)
if (firstIndex < 0) return { rank: 0 }
charNumber = firstIndex
for (let i = 1, I = stringToRank.length; i < I; i++) {
charNumber = findMatchingCharacter(stringToRank[i], testString, charNumber)
if (!(charNumber > -1)) return { rank: 0 }
}
const length = charNumber - firstIndex
return {
rank: 1,
index: length,
length: matchingInOrderCharCount,
closeness: (matchingInOrderCharCount / stringToRank.length) * (1 / length)
}
})(testString, stringToRank)
}
function compareItems(a, b) {
return 1 == a.rank && 1 == b.rank ? b.closeness - a.closeness : b.rank - a.rank
}
function getItemValues(item, accessor) {
let accessorFn = accessor
"object" == typeof accessor && (accessorFn = accessor.accessor)
const value = accessorFn(item)
return (
null == value ? []
: Array.isArray(value) ? value
: [value + ""]
)
}
const defaultKeyAttributes = { maxRanking: 1 / 0, minRanking: -1 / 0 }
function getAccessorAttributes(accessor) {
return "function" == typeof accessor ? defaultKeyAttributes : { ...defaultKeyAttributes, ...accessor }
}
export { compareItems, rankItem, rankings }