UNPKG

@storm-stack/utilities

Version:

This package includes various base utility class and various functions to assist in the development process.

248 lines (247 loc) 7.36 kB
import { removeAccents } from "./remove-accents.mjs"; const rankings = { CASE_SENSITIVE_EQUAL: 7, EQUAL: 6, STARTS_WITH: 5, WORD_STARTS_WITH: 4, CONTAINS: 3, ACRONYM: 2, MATCHES: 1, NO_MATCH: 0 }; const defaultBaseSortFn = (a, b) => String(a.rankedValue).localeCompare(String(b.rankedValue)); function matchSorter(items, value, options = {}) { const { keys, threshold = rankings.MATCHES, baseSort = defaultBaseSortFn, sorter = (matchedItems2) => matchedItems2.sort((a, b) => sortRankedValues(a, b, baseSort)) } = options; const matchedItems = items.reduce( (ret, item, index) => reduceItemsToRanked(ret, item, index), [] ); return sorter(matchedItems).map(({ item }) => item); function reduceItemsToRanked(matches, item, index) { const rankingInfo = getHighestRanking(item, keys, value, options); const { rank, keyThreshold = threshold } = rankingInfo; if (rank >= keyThreshold) { matches.push({ ...rankingInfo, item, index }); } return matches; } } matchSorter.rankings = rankings; function getHighestRanking(item, keys, value, options) { if (!keys) { const stringItem = item; return { // ends up being duplicate of 'item' in matches but consistent rankedValue: stringItem, rank: getMatchRanking(stringItem, value, options), keyIndex: -1, keyThreshold: options.threshold }; } const valuesToRank = getAllValuesToRank(item, keys); return valuesToRank.reduce( ({ rank, rankedValue, keyIndex, keyThreshold }, { itemValue, attributes }, i) => { let newRank = getMatchRanking(itemValue, value, options); let newRankedValue = rankedValue; const { minRanking, maxRanking, threshold } = attributes; if (newRank < minRanking && newRank >= rankings.MATCHES) { newRank = minRanking; } else if (newRank > maxRanking) { newRank = maxRanking; } if (newRank > rank) { rank = newRank; keyIndex = i; keyThreshold = threshold; newRankedValue = itemValue; } return { rankedValue: newRankedValue, rank, keyIndex, keyThreshold }; }, { rankedValue: item, rank: rankings.NO_MATCH, keyIndex: -1, keyThreshold: options.threshold } ); } function getMatchRanking(testString, stringToRank, options) { testString = prepareValueForComparison(testString, options); stringToRank = prepareValueForComparison(stringToRank, options); if (stringToRank.length > testString.length) { return rankings.NO_MATCH; } if (testString === stringToRank) { return rankings.CASE_SENSITIVE_EQUAL; } testString = testString.toLowerCase(); stringToRank = stringToRank.toLowerCase(); if (testString === stringToRank) { return rankings.EQUAL; } if (testString.startsWith(stringToRank)) { return rankings.STARTS_WITH; } if (testString.includes(` ${stringToRank}`)) { return rankings.WORD_STARTS_WITH; } if (testString.includes(stringToRank)) { return rankings.CONTAINS; } else if (stringToRank.length === 1) { return rankings.NO_MATCH; } if (getAcronym(testString).includes(stringToRank)) { return rankings.ACRONYM; } return getClosenessRanking(testString, stringToRank); } function getAcronym(string) { let acronym = ""; const wordsInString = string.split(" "); for (const wordInString of wordsInString) { const splitByHyphenWords = wordInString.split("-"); for (const splitByHyphenWord of splitByHyphenWords) { acronym += splitByHyphenWord.slice(0, 1); } } return acronym; } function getClosenessRanking(testString, stringToRank) { let matchingInOrderCharCount = 0; let charNumber = 0; function findMatchingCharacter(matchChar, string, index) { for (let j = index, J = string.length; j < J; j++) { const stringChar = string[j]; if (stringChar === matchChar) { matchingInOrderCharCount += 1; return j + 1; } } return -1; } function getRanking(spread2) { const spreadPercentage = 1 / spread2; const inOrderPercentage = matchingInOrderCharCount / stringToRank.length; const ranking = rankings.MATCHES + inOrderPercentage * spreadPercentage; return ranking; } const firstIndex = findMatchingCharacter(stringToRank[0], testString, 0); if (firstIndex < 0) { return rankings.NO_MATCH; } charNumber = firstIndex; for (let i = 1, I = stringToRank.length; i < I; i++) { const matchChar = stringToRank[i]; charNumber = findMatchingCharacter(matchChar, testString, charNumber); const found = charNumber > -1; if (!found) { return rankings.NO_MATCH; } } const spread = charNumber - firstIndex; return getRanking(spread); } function sortRankedValues(a, b, baseSort) { const aFirst = -1; const bFirst = 1; const { rank: aRank, keyIndex: aKeyIndex } = a; const { rank: bRank, keyIndex: bKeyIndex } = b; const same = aRank === bRank; if (same) { if (aKeyIndex === bKeyIndex) { return baseSort(a, b); } return aKeyIndex < bKeyIndex ? aFirst : bFirst; } return aRank > bRank ? aFirst : bFirst; } function prepareValueForComparison(value, { keepDiacritics }) { value = `${value}`; if (!keepDiacritics) { value = removeAccents(value); } return value; } function getItemValues(item, key) { if (typeof key === "object") { key = key.key; } let value; if (typeof key === "function") { value = key(item); } else if (item === null) { value = null; } else if (Object.hasOwnProperty.call(item, key)) { value = item[key]; } else if (key.includes(".")) { return getNestedValues(key, item); } else { value = null; } if (value === null) { return []; } if (Array.isArray(value)) { return value; } return [String(value)]; } function getNestedValues(path, item) { const keys = path.split("."); let values = [item]; for (let i = 0, I = keys.length; i < I; i++) { const nestedKey = keys[i]; let nestedValues = []; for (let j = 0, J = values.length; j < J; j++) { const nestedItem = values[j]; if (nestedItem === null) { continue; } if (nestedKey && Object.hasOwnProperty.call(nestedItem, nestedKey)) { const nestedValue = nestedItem[nestedKey]; if (nestedValue !== null) { nestedValues.push(nestedValue); } } else if (nestedItem && nestedKey === "*") { nestedValues = [...nestedValues, nestedItem]; } } values = nestedValues; } if (Array.isArray(values[0])) { const result = []; return result.concat(...values); } return values; } function getAllValuesToRank(item, keys) { const allValues = []; for (let j = 0, J = keys.length; j < J; j++) { const key = keys[j]; const attributes = getKeyAttributes(key); const itemValues = getItemValues(item, key); for (let i = 0, I = itemValues.length; i < I; i++) { allValues.push({ itemValue: itemValues[i], attributes }); } } return allValues; } const defaultKeyAttributes = { maxRanking: Infinity, minRanking: -Infinity }; function getKeyAttributes(key) { if (typeof key === "string") { return defaultKeyAttributes; } return { ...defaultKeyAttributes, ...key }; } export { defaultBaseSortFn, matchSorter, rankings };