UNPKG

@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
/** * @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 }