Matchsort is a simple library NPM package that provides a fast, precise and customisable way to sort and filter strings by a given search term.
Matchsort can be installed using NPM:
npm install match-sort
Matchsort provides sorting functionality only - how to implement it graphically on a website is up to you. Most likely you will want to use it together with autocomplete tools from some UI library, like Floating UI. Here are some examples on Codesandbox:
The fastest way to get started is by importing the ready-to-use generalTextMatcher
object:
import {generalTextMatcher} from 'match-sort';
const planets = [
'Mercury',
'Venus',
'Earth',
'Mars',
'Jupiter',
'Saturn',
'Uranus',
'Neptune',
];
const searchTerm = 'sa';
const sortedData = generalTextMatcher.sort(searchTerm, planets);
/**
* Expected result:
* sortedData = [
* 'Saturn',
* 'Mars',
* 'Uranus',
* ];
*/
This example will sort "Saturn" first because it is the only planet that starts with the provided search term, "sa". Then, Mars and Uranus are both included in the result because they both contain the letters "s" and "a". Mars is sorted before Uranus because the letters of the search term constitute a larger part of the word "Mars" (1/2) than they do of the word "Uranus" (1/3), hence "Mars" is considered a better match. Words that do not contain all the characters of the search term are not included in the result.
The generalTextMatcher
object is built using the StringMatchSort
class, which can be used to create any kind of functions for sorting and filtering string arrays.
Let's say we want to create a matcher that simply filters out all strings that do not contain the search term.
This can be done using the StringMatchSort
class with a StringMatchPredicates
filter as follows:
import {StringMatchSort, StringMatchPredicates} from 'match-sort';
const filterMatcher = new MatchSort().setFilter(StringMatchPredicates.contains);
However, when we now call filterMatcher.sort('sa', planets)
on the list in the first example, we will get an empty result.
This is because the function is case sensitive. To fix that, it is necessary to provide a transformation function.
That is a function that transforms the search term and the values to be matched to a common format.
In fact, this package provides a ready-to-use transformation functions through the StringTransform
class, including one for converting to lower case.
The StringMatchSort
class accepts an array of such functions in its constructor, so we can apply it like this:
import {StringMatchSort, StringMatchPredicates, StringTransform} from 'match-sort';
const transformFunctionList = [StringTransform.lowercase];
const filterMatcher = new StringMatchSort(transformFunctionList).setFilter(StringMatchPredicates.contains);
Now, when we call filterMatcher.sort('sa', planets)
on the list in the first example we will get an an array containing "Saturn" as the only result.
If the list had contained "Saturn" and "saturn", both would have been included in the result, but "saturn" would have been sorted before "Saturn" because it matched the search term before the transformation was applied.
This sorter might need some more specific functionality, though. Results are only filtered, but not sorted.
If a user searches for "ur", "Mercury", "Saturn" and "Uranus" will be included in the result, but "Mercury" and "Saturn" will be sorted before "Uranus", although the user is probably looking for "Uranus".
To fix this, we can provide a sorting function that checks if the value starts with the search term.
This is done using the MatchSort.chain()
function:
import {StringMatchSort, StringMatchPredicates, StringTransform} from 'match-sort';
import {startsWith} from './startsWith';
const startsWith = (search: string) => (value: string) => value.startsWith(search);
const transformFunctionList = [StringTransform.lowercase];
const filterMatcher = new StringMatchSort(transformFunctionList)
.chain(StringMatchPredicates.startsWith)
.setFilter(StringMatchPredicates.contains);
Any number of chain functions can be applied to the StringMatchSort
object, and they will be applied in the order they were added.
This means that values that match according to the first function will be sorted first, then values that match according to the second function, and so on.
If several values rank equally according to this strategy, they will be compared using the transform functions.
Sometimes, it is necessary to do a more complex comparison, so a function returning a boolean is not enough.
Therefore, StringMatchSort.chain
also supports functions that return a number. The lower the number, the better the rank.
Let's say we want to sort the items by how well they match the search term, given that they don't start with it.
Since all values that do not contain the search string are filtered out, we can do that with a function that simply returns the length of the value:
import {StringMatchSort, StringMatchPredicates, StringTransform} from 'match-sort';
const rank = () => (value: string) => value.length;
const filterMatcher = new StringMatchSort(transformFunctionList)
.chain(StringMatchPredicates.startsWith)
.chain(rank)
.setFilter(StringMatchPredicates.contains);
Now, when we run filterMatcher.sort('ur', planets)
on the list in the first example, "Uranus" will still be sorted first since the startsWith
function is applied first,
but then "Saturn" will be sorted before "Mercury" because "ur" constitutes a larger part of the value.
StringMatchSort
is actually just an extension of the MatchSort
class, which can be used with lists of any type.
If the list of things to sort is a list of objects, and the words to sort by addressed by a certain property of those objects,
then the onProperty
function of the StringMatchSort
class may be used to return a MatchSort
object that sorts the objects accordingly.
Here is an example of how this can be done using the generalTextMatcher
object, which, as already mentioned, is an instance of the StringMatchSort
class:
import {generalTextMatcher} from 'match-sort';
import {data} from './data';
const planets = [
{ name: 'Mercury', distance: 0.39 },
{ name: 'Venus', distance: 0.72 },
{ name: 'Earth', distance: 1 },
{ name: 'Mars', distance: 1.52 },
{ name: 'Jupiter', distance: 5.20 },
{ name: 'Saturn', distance: 9.58 },
{ name: 'Uranus', distance: 19.20 },
{ name: 'Neptune', distance: 30.05 },
];
const searchTerm = 'sa';
const sortedData = generalTextMatcher.onProperty('name').sort(searchTerm, planets);
Sometimes, it is necessary to sort by a list of keywords, rather than a single search term.
Therefore, the sort objects also have an onList
method that returns a MatchSort
object that sorts by the given list of keywords.
The ranking will then be made by the best matching keyword of each item, and only items where none of the keywords match the filter will be filtered out.
Here is an example of how this can be done using the generalTextMatcher
object:
import {generalTextMatcher} from 'match-sort';
const planets = [
['Mercury', 'Mercurius'],
['Venus'],
['Earth', 'Terra', 'Tellus', 'Gaia'],
['Mars'],
['Jupiter'],
['Saturn'],
['Uranus'],
['Neptune', 'Neptun'],
];
const searchTerm = 'sa';
const sortedData = generalTextMatcher.onList().sort(searchTerm, planets);
Both of the previous examples may be combined to a MatchSort object that sorts objects like this:
const earth = {
name: 'Earth',
distance: 1,
keywords: ['Earth', 'Terra', 'Tellus', 'Gaia'],
}
This can simply be accomplished by chaining onList
and onProperty
together:
import {generalTextMatcher} from 'match-sort';
const planets = [
{ name: 'Mercury', distance: 0.39, keywords: ['Mercury', 'Mercurius'] },
{ name: 'Venus', distance: 0.72, keywords: ['Venus'] },
{ name: 'Earth', distance: 1, keywords: ['Earth', 'Terra', 'Tellus', 'Gaia'] },
{ name: 'Mars', distance: 1.52, keywords: ['Mars'] },
{ name: 'Jupiter', distance: 5.20, keywords: ['Jupiter'] },
{ name: 'Saturn', distance: 9.58, keywords: ['Saturn'] },
{ name: 'Uranus', distance: 19.20, keywords: ['Uranus'] },
{ name: 'Neptune', distance: 30.05, keywords: ['Neptune', 'Neptun'] },
];
const searchTerm = 'sa';
const sortedData = generalTextMatcher
.onList()
.onProperty('keywords')
.sort(searchTerm, planets);
This package is built for search interfaces. It is optimized for updating the result list as the user types. Therefore, the last search result is cached, and as long as the search term starts with the previous search term, the searching will only be performed on the previous result list. In consequence, items that are filtered out, will not be included in the next search, unless the search term is shortened or completely changed.
Although StringMatchSort
is simply an extension of MatchSort<string>
,
the already mentioned string transformation features are only available on StringMatchSort
.
When using the StringMatchSort.onProperty
function, these features are preserved, but they are not available on MatchSort
by default.
In conclusion, when creating a function that sorts strings, StringMatchSort
is probably a better choice than MatchSort<string>
,
unless only exact character matches are relevant.