From 289ffc052f977bbbe17391281bf6924f92229555 Mon Sep 17 00:00:00 2001 From: Justin Waite Date: Tue, 14 Apr 2026 17:54:29 -0600 Subject: [PATCH] feat: add matchSorterWithRankInfo to expose rank info in results --- README.md | 28 +++++++++++++++-- src/__tests__/index.ts | 71 +++++++++++++++++++++++++++++++++++++++++- src/index.ts | 35 +++++++++++++++++++-- 3 files changed, 128 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 26c1753..b903104 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,26 @@ matchSorter(list, 'y') // ['yo', 'hey'] matchSorter(list, 'z') // [] ``` +If you need the ranking metadata that `match-sorter` computes internally, use +`matchSorterWithRankInfo`: + +```javascript +import {matchSorterWithRankInfo} from 'match-sorter' + +const rankedResults = matchSorterWithRankInfo(list, 'h') +// [ +// { +// item: 'hello', +// rankedValue: 'hello', +// rank: 5, +// keyIndex: -1, +// keyThreshold: undefined, +// index: 2, +// }, +// // ... +// ] +``` + ## Advanced options ### keys: `[string]` @@ -168,9 +188,9 @@ using dot-notation with the `*` wildcard instead of a numeric index. ```javascript const nestedObjList = [ - {aliases: [{name: {first: 'Janice'}},{name: {first: 'Jen'}}]}, - {aliases: [{name: {first: 'Fred'}},{name: {first: 'Frederic'}}]}, - {aliases: [{name: {first: 'George'}},{name: {first: 'Georgie'}}]}, + {aliases: [{name: {first: 'Janice'}}, {name: {first: 'Jen'}}]}, + {aliases: [{name: {first: 'Fred'}}, {name: {first: 'Frederic'}}]}, + {aliases: [{name: {first: 'George'}}, {name: {first: 'Georgie'}}]}, ] matchSorter(nestedObjList, 'jen', {keys: ['aliases.*.name.first']}) // [{aliases: [{name: {first: 'Janice'}},{name: {first: 'Jen'}}]}] @@ -355,6 +375,7 @@ _You can customize the core sorting behavior by specifying a custom `sorter` function:_ Disable sorting entirely: + ```javascript const list = ['appl', 'C apple', 'B apple', 'A apple', 'app', 'applebutter'] matchSorter(list, 'apple', {sorter: rankedItems => rankedItems}) @@ -362,6 +383,7 @@ matchSorter(list, 'apple', {sorter: rankedItems => rankedItems}) ``` Return the unsorted rankedItems, but in reverse order: + ```javascript const list = ['appl', 'C apple', 'B apple', 'A apple', 'app', 'applebutter'] matchSorter(list, 'apple', {sorter: rankedItems => [...rankedItems].reverse()}) diff --git a/src/__tests__/index.ts b/src/__tests__/index.ts index b160582..0764ec2 100644 --- a/src/__tests__/index.ts +++ b/src/__tests__/index.ts @@ -1,4 +1,10 @@ -import {matchSorter, rankings, MatchSorterOptions} from '../' +import { + matchSorter, + matchSorterWithRankInfo, + rankings, + MatchSorterOptions, + type RankedItem, +} from '../' type TestCase = { input: [Array, string, MatchSorterOptions?] @@ -663,6 +669,69 @@ for (const [ } } +test('can return ranked items with ranking metadata attached', () => { + const rankedResults: Array> = + matchSorterWithRankInfo( + [ + {tea: 'Earl Grey', alias: 'A'}, + {tea: 'Assam', alias: 'B'}, + {tea: 'Black', alias: 'C'}, + ], + 'A', + { + keys: ['tea', {maxRanking: rankings.STARTS_WITH, key: 'alias'}], + }, + ) + + expect(rankedResults).toEqual([ + { + item: {tea: 'Assam', alias: 'B'}, + rankedValue: 'Assam', + rank: rankings.STARTS_WITH, + keyIndex: 0, + keyThreshold: undefined, + index: 1, + }, + { + item: {tea: 'Earl Grey', alias: 'A'}, + rankedValue: 'A', + rank: rankings.STARTS_WITH, + keyIndex: 1, + keyThreshold: undefined, + index: 0, + }, + { + item: {tea: 'Black', alias: 'C'}, + rankedValue: 'Black', + rank: rankings.CONTAINS, + keyIndex: 0, + keyThreshold: undefined, + index: 2, + }, + ]) +}) + +test('can return ranked items without passing options', () => { + expect(matchSorterWithRankInfo(['hello', 'hey', 'sup'], 'h')).toEqual([ + { + item: 'hello', + rankedValue: 'hello', + rank: rankings.STARTS_WITH, + keyIndex: -1, + keyThreshold: undefined, + index: 0, + }, + { + item: 'hey', + rankedValue: 'hey', + rank: rankings.STARTS_WITH, + keyIndex: -1, + keyThreshold: undefined, + index: 1, + }, + ]) +}) + /* eslint jest/prefer-each: "off", diff --git a/src/index.ts b/src/index.ts index 4dcd7fd..99b9362 100644 --- a/src/index.ts +++ b/src/index.ts @@ -84,6 +84,29 @@ function matchSorter( value: string, options: MatchSorterOptions = {}, ): Array { + return getRankedItems(items, value, options).map(({item}) => item) +} + +/** + * Takes an array of items and a value and returns ranked items with metadata attached + * @param {Array} items - the items to sort + * @param {String} value - the value to use for ranking + * @param {Object} options - Some options to configure the sorter + * @return {Array} - the new ranked array + */ +function matchSorterWithRankInfo( + items: ReadonlyArray, + value: string, + options: MatchSorterOptions = {}, +): Array> { + return getRankedItems(items, value, options) +} + +function getRankedItems( + items: ReadonlyArray, + value: string, + options: MatchSorterOptions, +): Array> { const { keys, threshold = rankings.MATCHES, @@ -92,7 +115,7 @@ function matchSorter( matchedItems.sort((a, b) => sortRankedValues(a, b, baseSort)), } = options const matchedItems = items.reduce(reduceItemsToRanked, []) - return sorter(matchedItems).map(({item}) => item) + return sorter(matchedItems) function reduceItemsToRanked( matches: Array>, @@ -109,6 +132,7 @@ function matchSorter( } matchSorter.rankings = rankings +matchSorterWithRankInfo.rankings = rankings /** * Gets the highest ranking for value for the given item based on its values for the given keys @@ -520,7 +544,13 @@ function getKeyAttributes(key: KeyOption): KeyAttributes { return {...defaultKeyAttributes, ...key} } -export {matchSorter, rankings, defaultBaseSortFn, getItemValues} +export { + matchSorter, + matchSorterWithRankInfo, + rankings, + defaultBaseSortFn, + getItemValues, +} export type { MatchSorterOptions, @@ -528,6 +558,7 @@ export type { KeyOption, KeyAttributes, RankingInfo, + RankedItem, ValueGetterKey, }