- You have a list of dozens, hundreds, or thousands of items
- You want to filter and sort those items intelligently (maybe you have a filter input for the user)
- You want simple, expected, and deterministic sorting of the items (no fancy math algorithm that fancily changes the sorting as they type)
This follows a simple and sensible (user friendly) algorithm that makes it easy for you to filter and sort a list of items based on given input. Items are ranked based on sensible criteria that result in a better user experience.
To explain the ranking system, I'll use countries as an example:
- CASE SENSITIVE EQUALS: Case-sensitive equality trumps all. These will be
first. (ex.
France
would matchFrance
, but notfrance
) - EQUALS: Case-insensitive equality (ex.
France
would matchfrance
) - STARTS WITH: If the item starts with the given value (ex.
Sou
would matchSouth Korea
orSouth Africa
) - WORD STARTS WITH: If the item has multiple words, then if one of those
words starts with the given value (ex.
Repub
would matchDominican Republic
) - CASE STARTS WITH: If the item has a defined case (
camelCase
,PascalCase
,snake_case
orkebab-case
), then if one of the parts starts with the given value (ex.kingdom
would matchunitedKingdom
orunited_kingdom
) - CASE ACRONYM If the item's case matches the synonym (ex.
uk
would matchunited-kingdom
orUnitedKingdom
) - CONTAINS: If the item contains the given value (ex.
ham
would matchBahamas
) - ACRONYM: If the item's acronym is the given value (ex.
us
would matchUnited States
) - SIMPLE MATCH: If the item has letters in the same order as the letters
of the given value (ex.
iw
would matchZimbabwe
, but notKuwait
because it must be in the same order). Furthermore, if the item is a closer match, it will rank higher (ex.ua
matchesUruguay
more closely thanUnited States of America
, thereforeUruguay
will be ordered beforeUnited States of America
)
This ranking seems to make sense in people's minds. At least it does in mine. Feedback welcome!
- Installation
- Usage
- Advanced options
- Using ES6?
- Inspiration
- Other Solutions
- Issues
- Contributors ✨
- LICENSE
This module is distributed via npm which is bundled with node and
should be installed as one of your project's dependencies
:
npm install match-sorter
import matchSorter from 'match-sorter'
// or const matchSorter = require('match-sorter').default
// or window.matchSorter
const list = ['hi', 'hey', 'hello', 'sup', 'yo']
matchSorter(list, 'h') // ['hi', 'hey', 'hello']
matchSorter(list, 'y') // ['yo', 'hey']
matchSorter(list, 'z') // []
Default: undefined
By default it just uses the value itself as above. Passing an array tells match-sorter which keys to use for the ranking.
const objList = [
{name: 'Janice', color: 'Green'},
{name: 'Fred', color: 'Orange'},
{name: 'George', color: 'Blue'},
{name: 'Jen', color: 'Red'},
]
matchSorter(objList, 'g', {keys: ['name', 'color']})
// [{name: 'George', color: 'Blue'}, {name: 'Janice', color: 'Green'}, {name: 'Fred', color: 'Orange'}]
matchSorter(objList, 're', {keys: ['color', 'name']})
// [{name: 'Jen', color: 'Red'}, {name: 'Janice', color: 'Green'}, {name: 'Fred', color: 'Orange'}, {name: 'George', color: 'Blue'}]
Array of values: When the specified key matches an array of values, the best match from the values of in the array is going to be used for the ranking.
const iceCreamYum = [
{favoriteIceCream: ['mint', 'chocolate']},
{favoriteIceCream: ['candy cane', 'brownie']},
{favoriteIceCream: ['birthday cake', 'rocky road', 'strawberry']},
]
matchSorter(iceCreamYum, 'cc', {keys: ['favoriteIceCream']})
// [{favoriteIceCream: ['candy cane', 'brownie']}, {favoriteIceCream: ['mint', 'chocolate']}]
Nested Keys: You can specify nested keys using dot-notation.
const nestedObjList = [
{name: {first: 'Janice'}},
{name: {first: 'Fred'}},
{name: {first: 'George'}},
{name: {first: 'Jen'}},
]
matchSorter(nestedObjList, 'j', {keys: ['name.first']})
// [{name: {first: 'Janice'}}, {name: {first: 'Jen'}}]
const nestedObjList = [
{name: [{first: 'Janice'}]},
{name: [{first: 'Fred'}]},
{name: [{first: 'George'}]},
{name: [{first: 'Jen'}]},
]
matchSorter(nestedObjList, 'j', {keys: ['name.0.first']})
// [{name: {first: 'Janice'}}, {name: {first: 'Jen'}}]
// matchSorter(nestedObjList, 'j', {keys: ['name[0].first']}) does not work
Property Callbacks: Alternatively, you may also pass in a callback function that resolves the value of the key(s) you wish to match on. This is especially useful when interfacing with libraries such as Immutable.js
const list = [{name: 'Janice'}, {name: 'Fred'}, {name: 'George'}, {name: 'Jen'}]
matchSorter(list, 'j', {keys: [item => item.name]})
// [{name: 'Janice'}, {name: 'Jen'}]
For more complex structures, expanding on the nestedObjList
example above, you
can use map
:
const nestedObjList = [
{
name: [
{first: 'Janice', last: 'Smith'},
{first: 'Jon', last: 'Doe'},
],
},
{
name: [
{first: 'Fred', last: 'Astaire'},
{first: 'Jenny', last: 'Doe'},
{first: 'Wilma', last: 'Flintstone'},
],
},
]
matchSorter(nestedObjList, 'doe', {
keys: [
item => item.name.map(i => i.first),
item => item.name.map(i => i.last),
],
})
// [name: [{ first: 'Janice', last: 'Smith' },{ first: 'Jon', last: 'Doe' }], name: [{ first: 'Fred', last: 'Astaire' },{ first: 'Jenny', last: 'Doe' },{ first: 'Wilma', last: 'Flintstone' }]]
Threshold: You may specify an individual threshold for specific keys. A key will only match if it meets the specified threshold. For more information regarding thresholds see below
const list = [
{name: 'Fred', color: 'Orange'},
{name: 'Jen', color: 'Red'},
]
matchSorter(list, 'ed', {
keys: [{threshold: rankings.STARTS_WITH, key: 'name'}, 'color'],
})
//[{name: 'Jen', color: 'Red'}]
Min and Max Ranking: You may restrict specific keys to a minimum or maximum ranking by passing in an object. A key with a minimum rank will only get promoted if there is at least a simple match.
const tea = [
{tea: 'Earl Grey', alias: 'A'},
{tea: 'Assam', alias: 'B'},
{tea: 'Black', alias: 'C'},
]
matchSorter(tea, 'A', {
keys: ['tea', {maxRanking: matchSorter.rankings.STARTS_WITH, key: 'alias'}],
})
// without maxRanking, Earl Grey would come first because the alias "A" would be CASE_SENSITIVE_EQUAL
// `tea` key comes before `alias` key, so Assam comes first even though both match as STARTS_WITH
// [{tea: 'Assam', alias: 'B'}, {tea: 'Earl Grey', alias: 'A'},{tea: 'Black', alias: 'C'}]
const tea = [
{tea: 'Milk', alias: 'moo'},
{tea: 'Oolong', alias: 'B'},
{tea: 'Green', alias: 'C'},
]
matchSorter(tea, 'oo', {
keys: ['tea', {minRanking: matchSorter.rankings.EQUAL, key: 'alias'}],
})
// minRanking bumps Milk up to EQUAL from CONTAINS (alias)
// Oolong matches as STARTS_WITH
// Green is missing due to no match
// [{tea: 'Milk', alias: 'moo'}, {tea: 'Oolong', alias: 'B'}]
Default: MATCHES
Thresholds can be used to specify the criteria used to rank the results. Available thresholds (from top to bottom) are:
- CASE_SENSITIVE_EQUAL
- EQUAL
- STARTS_WITH
- WORD_STARTS_WITH
- STRING_CASE
- STRING_CASE_ACRONYM
- CONTAINS
- ACRONYM
- MATCHES (default value)
- NO_MATCH
const fruit = ['orange', 'apple', 'grape', 'banana']
matchSorter(fruit, 'ap', {threshold: matchSorter.rankings.NO_MATCH})
// ['apple', 'grape', 'orange', 'banana'] (returns all items, just sorted by best match)
const things = ['google', 'airbnb', 'apple', 'apply', 'app'],
matchSorter(things, 'app', {threshold: matchSorter.rankings.EQUAL})
// ['app'] (only items that are equal)
const otherThings = ['fiji apple', 'google', 'app', 'crabapple', 'apple', 'apply']
matchSorter(otherThings, 'app', {threshold: matchSorter.rankings.WORD_STARTS_WITH})
// ['app', 'apple', 'apply', 'fiji apple'] (everything that matches with "word starts with" or better)
Default: false
By default, match-sorter will strip diacritics before doing any comparisons. This is the default because it makes the most sense from a UX perspective.
You can disable this behavior by specifying keepDiacritics: true
const thingsWithDiacritics = [
'jalapeño',
'à la carte',
'café',
'papier-mâché',
'à la mode',
]
matchSorter(thingsWithDiacritics, 'aa')
// ['jalapeño', 'à la carte', 'à la mode', 'papier-mâché']
matchSorter(thingsWithDiacritics, 'aa', {keepDiacritics: true})
// ['jalapeño', 'à la carte']
matchSorter(thingsWithDiacritics, 'à', {keepDiacritics: true})
// ['à la carte', 'à la mode']
In the examples above, we're using CommonJS. If you're using ES6 modules, then you can do:
import matchSorter, {rankings, caseRankings} from 'match-sorter'
Actually, most of this code was extracted from the very first library I ever wrote: genie!
You might try Fuse.js. It uses advanced math fanciness to get the closest match. Unfortunately what's "closest" doesn't always really make sense. So I extracted this from genie.
Looking to contribute? Look for the [Good First Issue][good-first-issue] label.
Please file an issue for bugs, missing documentation, or unexpected behavior.
[See Bugs][bugs]
Please file an issue to suggest new features. Vote on feature requests by adding a 👍. This helps maintainers prioritize what to work on.
[See Feature Requests][requests]
Thanks goes to these people (emoji key):
This project follows the all-contributors specification. Contributions of any kind welcome!
MIT