@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
JavaScript
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 };