UNPKG

13.1 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, '__esModule', { value: true });
4
5var _extends = require('@babel/runtime/helpers/extends');
6var removeAccents = require('remove-accents');
7
8function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
9
10var _extends__default = /*#__PURE__*/_interopDefaultLegacy(_extends);
11var removeAccents__default = /*#__PURE__*/_interopDefaultLegacy(removeAccents);
12
13var rankings = {
14 CASE_SENSITIVE_EQUAL: 7,
15 EQUAL: 6,
16 STARTS_WITH: 5,
17 WORD_STARTS_WITH: 4,
18 CONTAINS: 3,
19 ACRONYM: 2,
20 MATCHES: 1,
21 NO_MATCH: 0
22};
23matchSorter.rankings = rankings;
24
25var defaultBaseSortFn = function defaultBaseSortFn(a, b) {
26 return String(a.rankedValue).localeCompare(String(b.rankedValue));
27};
28/**
29 * Takes an array of items and a value and returns a new array with the items that match the given value
30 * @param {Array} items - the items to sort
31 * @param {String} value - the value to use for ranking
32 * @param {Object} options - Some options to configure the sorter
33 * @return {Array} - the new sorted array
34 */
35
36
37function matchSorter(items, value, options) {
38 if (options === void 0) {
39 options = {};
40 }
41
42 var _options = options,
43 keys = _options.keys,
44 _options$threshold = _options.threshold,
45 threshold = _options$threshold === void 0 ? rankings.MATCHES : _options$threshold,
46 _options$baseSort = _options.baseSort,
47 baseSort = _options$baseSort === void 0 ? defaultBaseSortFn : _options$baseSort,
48 _options$sorter = _options.sorter,
49 sorter = _options$sorter === void 0 ? function (matchedItems) {
50 return matchedItems.sort(function (a, b) {
51 return sortRankedValues(a, b, baseSort);
52 });
53 } : _options$sorter;
54 var matchedItems = items.reduce(reduceItemsToRanked, []);
55 return sorter(matchedItems).map(function (_ref) {
56 var item = _ref.item;
57 return item;
58 });
59
60 function reduceItemsToRanked(matches, item, index) {
61 var rankingInfo = getHighestRanking(item, keys, value, options);
62 var rank = rankingInfo.rank,
63 _rankingInfo$keyThres = rankingInfo.keyThreshold,
64 keyThreshold = _rankingInfo$keyThres === void 0 ? threshold : _rankingInfo$keyThres;
65
66 if (rank >= keyThreshold) {
67 matches.push(_extends__default['default']({}, rankingInfo, {
68 item: item,
69 index: index
70 }));
71 }
72
73 return matches;
74 }
75}
76/**
77 * Gets the highest ranking for value for the given item based on its values for the given keys
78 * @param {*} item - the item to rank
79 * @param {Array} keys - the keys to get values from the item for the ranking
80 * @param {String} value - the value to rank against
81 * @param {Object} options - options to control the ranking
82 * @return {{rank: Number, keyIndex: Number, keyThreshold: Number}} - the highest ranking
83 */
84
85
86function getHighestRanking(item, keys, value, options) {
87 if (!keys) {
88 // if keys is not specified, then we assume the item given is ready to be matched
89 var stringItem = item;
90 return {
91 // ends up being duplicate of 'item' in matches but consistent
92 rankedValue: stringItem,
93 rank: getMatchRanking(stringItem, value, options),
94 keyIndex: -1,
95 keyThreshold: options.threshold
96 };
97 }
98
99 var valuesToRank = getAllValuesToRank(item, keys);
100 return valuesToRank.reduce(function (_ref2, _ref3, i) {
101 var rank = _ref2.rank,
102 rankedValue = _ref2.rankedValue,
103 keyIndex = _ref2.keyIndex,
104 keyThreshold = _ref2.keyThreshold;
105 var itemValue = _ref3.itemValue,
106 attributes = _ref3.attributes;
107 var newRank = getMatchRanking(itemValue, value, options);
108 var newRankedValue = rankedValue;
109 var minRanking = attributes.minRanking,
110 maxRanking = attributes.maxRanking,
111 threshold = attributes.threshold;
112
113 if (newRank < minRanking && newRank >= rankings.MATCHES) {
114 newRank = minRanking;
115 } else if (newRank > maxRanking) {
116 newRank = maxRanking;
117 }
118
119 if (newRank > rank) {
120 rank = newRank;
121 keyIndex = i;
122 keyThreshold = threshold;
123 newRankedValue = itemValue;
124 }
125
126 return {
127 rankedValue: newRankedValue,
128 rank: rank,
129 keyIndex: keyIndex,
130 keyThreshold: keyThreshold
131 };
132 }, {
133 rankedValue: item,
134 rank: rankings.NO_MATCH,
135 keyIndex: -1,
136 keyThreshold: options.threshold
137 });
138}
139/**
140 * Gives a rankings score based on how well the two strings match.
141 * @param {String} testString - the string to test against
142 * @param {String} stringToRank - the string to rank
143 * @param {Object} options - options for the match (like keepDiacritics for comparison)
144 * @returns {Number} the ranking for how well stringToRank matches testString
145 */
146
147
148function getMatchRanking(testString, stringToRank, options) {
149 testString = prepareValueForComparison(testString, options);
150 stringToRank = prepareValueForComparison(stringToRank, options); // too long
151
152 if (stringToRank.length > testString.length) {
153 return rankings.NO_MATCH;
154 } // case sensitive equals
155
156
157 if (testString === stringToRank) {
158 return rankings.CASE_SENSITIVE_EQUAL;
159 } // Lower casing before further comparison
160
161
162 testString = testString.toLowerCase();
163 stringToRank = stringToRank.toLowerCase(); // case insensitive equals
164
165 if (testString === stringToRank) {
166 return rankings.EQUAL;
167 } // starts with
168
169
170 if (testString.startsWith(stringToRank)) {
171 return rankings.STARTS_WITH;
172 } // word starts with
173
174
175 if (testString.includes(" " + stringToRank)) {
176 return rankings.WORD_STARTS_WITH;
177 } // contains
178
179
180 if (testString.includes(stringToRank)) {
181 return rankings.CONTAINS;
182 } else if (stringToRank.length === 1) {
183 // If the only character in the given stringToRank
184 // isn't even contained in the testString, then
185 // it's definitely not a match.
186 return rankings.NO_MATCH;
187 } // acronym
188
189
190 if (getAcronym(testString).includes(stringToRank)) {
191 return rankings.ACRONYM;
192 } // will return a number between rankings.MATCHES and
193 // rankings.MATCHES + 1 depending on how close of a match it is.
194
195
196 return getClosenessRanking(testString, stringToRank);
197}
198/**
199 * Generates an acronym for a string.
200 *
201 * @param {String} string the string for which to produce the acronym
202 * @returns {String} the acronym
203 */
204
205
206function getAcronym(string) {
207 var acronym = '';
208 var wordsInString = string.split(' ');
209 wordsInString.forEach(function (wordInString) {
210 var splitByHyphenWords = wordInString.split('-');
211 splitByHyphenWords.forEach(function (splitByHyphenWord) {
212 acronym += splitByHyphenWord.substr(0, 1);
213 });
214 });
215 return acronym;
216}
217/**
218 * Returns a score based on how spread apart the
219 * characters from the stringToRank are within the testString.
220 * A number close to rankings.MATCHES represents a loose match. A number close
221 * to rankings.MATCHES + 1 represents a tighter match.
222 * @param {String} testString - the string to test against
223 * @param {String} stringToRank - the string to rank
224 * @returns {Number} the number between rankings.MATCHES and
225 * rankings.MATCHES + 1 for how well stringToRank matches testString
226 */
227
228
229function getClosenessRanking(testString, stringToRank) {
230 var matchingInOrderCharCount = 0;
231 var charNumber = 0;
232
233 function findMatchingCharacter(matchChar, string, index) {
234 for (var j = index, J = string.length; j < J; j++) {
235 var stringChar = string[j];
236
237 if (stringChar === matchChar) {
238 matchingInOrderCharCount += 1;
239 return j + 1;
240 }
241 }
242
243 return -1;
244 }
245
246 function getRanking(spread) {
247 var spreadPercentage = 1 / spread;
248 var inOrderPercentage = matchingInOrderCharCount / stringToRank.length;
249 var ranking = rankings.MATCHES + inOrderPercentage * spreadPercentage;
250 return ranking;
251 }
252
253 var firstIndex = findMatchingCharacter(stringToRank[0], testString, 0);
254
255 if (firstIndex < 0) {
256 return rankings.NO_MATCH;
257 }
258
259 charNumber = firstIndex;
260
261 for (var i = 1, I = stringToRank.length; i < I; i++) {
262 var matchChar = stringToRank[i];
263 charNumber = findMatchingCharacter(matchChar, testString, charNumber);
264 var found = charNumber > -1;
265
266 if (!found) {
267 return rankings.NO_MATCH;
268 }
269 }
270
271 var spread = charNumber - firstIndex;
272 return getRanking(spread);
273}
274/**
275 * Sorts items that have a rank, index, and keyIndex
276 * @param {Object} a - the first item to sort
277 * @param {Object} b - the second item to sort
278 * @return {Number} -1 if a should come first, 1 if b should come first, 0 if equal
279 */
280
281
282function sortRankedValues(a, b, baseSort) {
283 var aFirst = -1;
284 var bFirst = 1;
285 var aRank = a.rank,
286 aKeyIndex = a.keyIndex;
287 var bRank = b.rank,
288 bKeyIndex = b.keyIndex;
289 var same = aRank === bRank;
290
291 if (same) {
292 if (aKeyIndex === bKeyIndex) {
293 // use the base sort function as a tie-breaker
294 return baseSort(a, b);
295 } else {
296 return aKeyIndex < bKeyIndex ? aFirst : bFirst;
297 }
298 } else {
299 return aRank > bRank ? aFirst : bFirst;
300 }
301}
302/**
303 * Prepares value for comparison by stringifying it, removing diacritics (if specified)
304 * @param {String} value - the value to clean
305 * @param {Object} options - {keepDiacritics: whether to remove diacritics}
306 * @return {String} the prepared value
307 */
308
309
310function prepareValueForComparison(value, _ref4) {
311 var keepDiacritics = _ref4.keepDiacritics;
312 // value might not actually be a string at this point (we don't get to choose)
313 // so part of preparing the value for comparison is ensure that it is a string
314 value = "" + value; // toString
315
316 if (!keepDiacritics) {
317 value = removeAccents__default['default'](value);
318 }
319
320 return value;
321}
322/**
323 * Gets value for key in item at arbitrarily nested keypath
324 * @param {Object} item - the item
325 * @param {Object|Function} key - the potentially nested keypath or property callback
326 * @return {Array} - an array containing the value(s) at the nested keypath
327 */
328
329
330function getItemValues(item, key) {
331 if (typeof key === 'object') {
332 key = key.key;
333 }
334
335 var value;
336
337 if (typeof key === 'function') {
338 value = key(item);
339 } else if (item == null) {
340 value = null;
341 } else if (Object.hasOwnProperty.call(item, key)) {
342 value = item[key];
343 } else if (key.includes('.')) {
344 // eslint-disable-next-line @typescript-eslint/no-unsafe-call
345 return getNestedValues(key, item);
346 } else {
347 value = null;
348 } // because `value` can also be undefined
349
350
351 if (value == null) {
352 return [];
353 }
354
355 if (Array.isArray(value)) {
356 return value;
357 }
358
359 return [String(value)];
360}
361/**
362 * Given path: "foo.bar.baz"
363 * And item: {foo: {bar: {baz: 'buzz'}}}
364 * -> 'buzz'
365 * @param path a dot-separated set of keys
366 * @param item the item to get the value from
367 */
368
369
370function getNestedValues(path, item) {
371 var keys = path.split('.');
372 var values = [item];
373
374 for (var i = 0, I = keys.length; i < I; i++) {
375 var nestedKey = keys[i];
376 var nestedValues = [];
377
378 for (var j = 0, J = values.length; j < J; j++) {
379 var nestedItem = values[j];
380 if (nestedItem == null) continue;
381
382 if (Object.hasOwnProperty.call(nestedItem, nestedKey)) {
383 var nestedValue = nestedItem[nestedKey];
384
385 if (nestedValue != null) {
386 nestedValues.push(nestedValue);
387 }
388 } else if (nestedKey === '*') {
389 // ensure that values is an array
390 nestedValues = nestedValues.concat(nestedItem);
391 }
392 }
393
394 values = nestedValues;
395 }
396
397 if (Array.isArray(values[0])) {
398 // keep allowing the implicit wildcard for an array of strings at the end of
399 // the path; don't use `.flat()` because that's not available in node.js v10
400 var result = [];
401 return result.concat.apply(result, values);
402 } // Based on our logic it should be an array of strings by now...
403 // assuming the user's path terminated in strings
404
405
406 return values;
407}
408/**
409 * Gets all the values for the given keys in the given item and returns an array of those values
410 * @param item - the item from which the values will be retrieved
411 * @param keys - the keys to use to retrieve the values
412 * @return objects with {itemValue, attributes}
413 */
414
415
416function getAllValuesToRank(item, keys) {
417 var allValues = [];
418
419 for (var j = 0, J = keys.length; j < J; j++) {
420 var key = keys[j];
421 var attributes = getKeyAttributes(key);
422 var itemValues = getItemValues(item, key);
423
424 for (var i = 0, I = itemValues.length; i < I; i++) {
425 allValues.push({
426 itemValue: itemValues[i],
427 attributes: attributes
428 });
429 }
430 }
431
432 return allValues;
433}
434
435var defaultKeyAttributes = {
436 maxRanking: Infinity,
437 minRanking: -Infinity
438};
439/**
440 * Gets all the attributes for the given key
441 * @param key - the key from which the attributes will be retrieved
442 * @return object containing the key's attributes
443 */
444
445function getKeyAttributes(key) {
446 if (typeof key === 'string') {
447 return defaultKeyAttributes;
448 }
449
450 return _extends__default['default']({}, defaultKeyAttributes, key);
451}
452/*
453eslint
454 no-continue: "off",
455*/
456
457exports.defaultBaseSortFn = defaultBaseSortFn;
458exports.matchSorter = matchSorter;
459exports.rankings = rankings;