Code Monkey home page Code Monkey logo

match-sorter's Introduction

match-sorter

Simple, expected, and deterministic best-match sorting of an array in JavaScript


Demo

Build Status Code Coverage version downloads MIT License All Contributors PRs Welcome Code of Conduct Examples

The problem

  1. You have a list of dozens, hundreds, or thousands of items
  2. You want to filter and sort those items intelligently (maybe you have a filter input for the user)
  3. You want simple, expected, and deterministic sorting of the items (no fancy math algorithm that fancily changes the sorting as they type)

This solution

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:

  1. CASE SENSITIVE EQUALS: Case-sensitive equality trumps all. These will be first. (ex. France would match France, but not france)
  2. EQUALS: Case-insensitive equality (ex. France would match france)
  3. STARTS WITH: If the item starts with the given value (ex. Sou would match South Korea or South Africa)
  4. WORD STARTS WITH: If the item has multiple words, then if one of those words starts with the given value (ex. Repub would match Dominican Republic)
  5. CONTAINS: If the item contains the given value (ex. ham would match Bahamas)
  6. ACRONYM: If the item's acronym is the given value (ex. us would match United States)
  7. SIMPLE MATCH: If the item has letters in the same order as the letters of the given value (ex. iw would match Zimbabwe, but not Kuwait because it must be in the same order). Furthermore, if the item is a closer match, it will rank higher (ex. ua matches Uruguay more closely than United States of America, therefore Uruguay will be ordered before United States of America)

This ranking seems to make sense in people's minds. At least it does in mine. Feedback welcome!

Installation

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

Usage

import {matchSorter} from 'match-sorter'
// or const {matchSorter} = require('match-sorter')
// or window.matchSorter.matchSorter
const list = ['hi', 'hey', 'hello', 'sup', 'yo']
matchSorter(list, 'h') // ['hello', 'hey', 'hi']
matchSorter(list, 'y') // ['yo', 'hey']
matchSorter(list, 'z') // []

Advanced options

keys: [string]

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

This even works with arrays of multiple nested objects: just specify the key using dot-notation with the * wildcard instead of a numeric index.

const nestedObjList = [
  {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'}}]}]
matchSorter(nestedObjList, 'jen', {keys: ['aliases.0.name.first']})
// []

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: matchSorter.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'}]

threshold: number

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
  • 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)

keepDiacritics: boolean

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']

baseSort: function(itemA, itemB): -1 | 0 | 1

Default: (a, b) => String(a.rankedValue).localeCompare(b.rankedValue)

By default, match-sorter uses the String.localeCompare function to tie-break items that have the same ranking. This results in a stable, alphabetic sort.

const list = ['C apple', 'B apple', 'A apple']
matchSorter(list, 'apple')
// ['A apple', 'B apple', 'C apple']

You can customize this behavior by specifying a custom baseSort function:

const list = ['C apple', 'B apple', 'A apple']
// This baseSort function will use the original index of items as the tie breaker
matchSorter(list, 'apple', {baseSort: (a, b) => (a.index < b.index ? -1 : 1)})
// ['C apple', 'B apple', 'A apple']

sorter: function(rankedItems): rankedItems

Default: matchedItems => matchedItems.sort((a, b) => sortRankedValues(a, b, baseSort))

By default, match-sorter uses an internal sortRankedValues function to sort items after matching them.

You can customize the core sorting behavior by specifying a custom sorter function:

Disable sorting entirely:

const list = ['appl', 'C apple', 'B apple', 'A apple', 'app', 'applebutter']
matchSorter(list, 'apple', {sorter: rankedItems => rankedItems})
// ['C apple', 'B apple', 'A apple', 'applebutter']

Return the unsorted rankedItems, but in reverse order:

const list = ['appl', 'C apple', 'B apple', 'A apple', 'app', 'applebutter']
matchSorter(list, 'apple', {sorter: rankedItems => [...rankedItems].reverse()})
// ['applebutter', 'A apple', 'B apple', 'C apple']

Recipes

Match PascalCase, camelCase, snake_case, or kebab-case as words

By default, match-sorter assumes spaces to be the word separator. However, if your data has a different word separator, you can use a property callback to replace your separator with spaces. For example, for snake_case:

const list = [
  {name: 'Janice_Kurtis'},
  {name: 'Fred_Mertz'},
  {name: 'George_Foreman'},
  {name: 'Jen_Smith'},
]
matchSorter(list, 'js', {keys: [item => item.name.replace(/_/g, ' ')]})
// [{name: 'Jen_Smith'}, {name: 'Janice_Kurtis'}]

Match many words across multiple fields (table filtering)

By default, match-sorter will return matches from objects where one of the properties matches the entire search term. For multi-column data sets it can be beneficial to split words in search string and match each word separately. This can be done by chaining match-sorter calls.

The benefit of this is that a filter string of "two words" will match both "two" and "words", but will return rows where the two words are found in different columns as well as when both words match in the same column. For single-column matches it will also return matches out of order (column = "wordstwo" will match just as well as column="twowords", the latter getting a higher score).

function fuzzySearchMultipleWords(
  rows, // array of data [{a: "a", b: "b"}, {a: "c", b: "d"}]
  keys, // keys to search ["a", "b"]
  filterValue: string, // potentially multi-word search string "two words"
) {
  if (!filterValue || !filterValue.length) {
    return rows
  }

  const terms = filterValue.split(' ')
  if (!terms) {
    return rows
  }

  // reduceRight will mean sorting is done by score for the _first_ entered word.
  return terms.reduceRight(
    (results, term) => matchSorter(results, term, {keys}),
    rows,
  )
}

Multi-column code sandbox

Inspiration

Actually, most of this code was extracted from the very first library I ever wrote: genie!

Other Solutions

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.

Issues

Looking to contribute? Look for the Good First Issue label.

🐛 Bugs

Please file an issue for bugs, missing documentation, or unexpected behavior.

See Bugs

💡 Feature Requests

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

Contributors ✨

Thanks goes to these people (emoji key):


Kent C. Dodds

💻 📖 🚇 ⚠️ 👀

Conor Hastings

💻 📖 ⚠️ 👀

Rogelio Guzman

📖

Claudéric Demers

💻 📖 ⚠️

Kevin Davis

💻 ⚠️

Denver Chen

💻 📖 ⚠️

Christian Ruigrok

🐛 💻 📖

Hozefa

🐛 💻 ⚠️ 🤔

pushpinder107

💻

Mordy Tikotzky

💻 📖 ⚠️

Steven Brannum

💻 ⚠️

Christer van der Meeren

🐛

Samuel Petrosyan

💻 🐛

Brandon Kalinowski

🐛

Eric Berry

🔍

Skubie Doo

📖

Michaël De Boey

💻 👀

Tanner Linsley

💻 ⚠️

Victor

📖

Rebecca Stevens

🐛 📖

Marco Moretti

📖

Ricardo Busquet

🤔 👀 💻

Weyert de Boer

🤔 👀

Philipp Garbowsky

💻

Mart

💻 ⚠️ 📖

Aleksey Levenstein

💻

Take Weiland

💻

Amit Abershitz

📖

This project follows the all-contributors specification. Contributions of any kind welcome!

LICENSE

MIT

match-sorter's People

Contributors

allcontributors[bot] avatar amitaber avatar andrewmcodes avatar bernharduw avatar chrisru avatar cliewpaypal avatar conorhastings avatar diesieben07 avatar dobryanskyy avatar glebtv avatar hozefaj avatar kentcdodds avatar kevindavus avatar levenleven avatar luish avatar marcosvega91 avatar mart-jansink avatar michaeldeboey avatar mochaap avatar nfdjps avatar philgarb avatar rbusquet avatar rebeccastevens avatar samyan avatar sdbrannum avatar sjaq avatar skube avatar swevictor avatar tannerlinsley avatar tikotzky avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

match-sorter's Issues

Error while bundling with Rollup

Hi, when I'm trying to use match-sorter in an app that gets bundled with rollup, I get the following error:

[!] Error: 'default' is not exported by node_modules/remove-accents/index.js, imported by node_modules/match-sorter/dist/match-sorter.esm.js
https://rollupjs.org/guide/en/#error-name-is-not-exported-by-module
node_modules/match-sorter/dist/match-sorter.esm.js (2:7)
1: import _extends from '@babel/runtime/helpers/esm/extends';
2: import removeAccents from 'remove-accents';
          ^
3:
4: var rankings = {
Error: 'default' is not exported by node_modules/remove-accents/index.js, imported by node_modules/match-sorter/dist/match-sorter.esm.js

After having a look at remove-accents it seams that this is valid. Do you have any clue on how to fix this?

Edit: here is my rollup config

{
    output: {
        exports: 'named',
        format: 'umd',
        dir: 'dist',
        sourcemap: environment === 'development',
        globals: {
            react: 'React',
            'react-dom': 'ReactDOM',
            'prop-types': 'PropTypes'
        }
    },
    external: ['react', 'react-dom', 'prop-types'],
    plugins: [
        resolve(),
        postcss({
            plugins: [],
            minimize: true,
            sourceMap: false,
            modules: true
        }),
        json(),
        injectProcessEnv({
            NODE_ENV: environment
        }),

        babel({
            include: ['src/**', 'node_modules/@mothership/**'],
            plugins: [
                '@babel/plugin-transform-react-jsx',
                '@babel/plugin-proposal-class-properties'
            ],
            presets: ['@babel/preset-env']
        }),
        commonjs({
            include: 'node_modules/**'
        })
    ]
}

Escape period?

  • match-sorter version: 2.3.0
  • node version:
  • npm (or yarn) version:

Relevant code or config

const placeholders = [{ searchTerms: [project!.slug, 'slug', 'project.slug'] }];

const matches = matchSorter(placeholders, 'project.slug', {
    keys: ['searchTerms'],
});

What you did: Searching for the string "project.slug" returns an empty result.

Problem description: I want to be able to search for the string "project.slug" and match the item in the placeholders array.

Suggested solution: ?

Property callback or nested keys possible?

I'm working with some objects that need to be searched using a sub-property of the object. Most other solutions provide a callback of sorts to locate said properties or support Object.array[] string syntax (similar to lodash's _.get(object, 'path') method).

Thoughts?

More threshold examples

I'm not sure if this is possible with the threshold option but I'm looking to prioritize when l = false as the most important key to sort on. so results with l = false will be at the top of the results

Relevant code or config:

function getSuggestions(value, results) {
  const inputValue = deburr(value.trim()).toLowerCase();
  const inputLength = inputValue.length;
  let count = 0;
  console.log(results);
  const tmp = [];
  for (let j = 0; j < 2; j++) {
    for (let k = 0; k < results.length; k++) {
      if (results[k].l == (j == 0 ? false : true)) {
        tmp.push(results[k]);
      }
    }
  }
  results = tmp;
  const getItems = value =>
    value
      ? matchSorter(results, value, {
          keys: ["i", "n", "nl_n", "s", "nl_s", "c", "nl_c"],
        })
      : results;

  return inputLength === 0
    ? []
    : getItems(inputValue).filter(() => {
        // put the city data here.
        const keep = count < 8;

        if (keep) {
          count += 1;
        }
        // console.log(keep)
        return keep;
      });
}

I can't figure out a way to put l as a key. I tried using different thresholds but I couldn't wrap my head around the threshold options

Filter items in a normalized array

  • match-sorter version: 2.3.0
  • node version: v9.4.0
  • npm (or yarn) version: 6.4.1

Relevant code or config

const adults = [
  { name: "Jim", house: "house-uuid-one", children: ["children-uuid-one", children-uuid-two"], uuid: "adult-uuid" },
  { name: "Yin", house: "house-uuid-two", children: [], uuid: "adult-uuid-two" }
];
const normalizedHouses = { 
  "house-uuid-one": { address: "Some Street", uuid: "house-uuid-one" },
  "hourse-uuid-two": { address: "Simba Ave", uuid: "house-uuid-two"
};
const normalizedChildren = { "children-uuid-one": { age: 2, name: "John", uuid: "children-uuid-one" }, "children-uuid-two": { age: 8, name: "Jenna", uuid: "children-uuid-two" } };

What you did:
I would like to use match sorter with normalized arrays.

What happened:
I couldn't figure out how to use matchSorter with normalized arrays.

Problem description:
I tried it like this:

matchSorter(adults, "o", {
  keys: [
    "name",
    item => normalizedHouses[item.house].name,
    item => item.children.map(child =>  normalizedChildren[child]).name
  ]
})

Suggested solution:
Let the array be transformed:

matchSorter(items, "o", { keys: ["name", "house", "children"], trafo: .map(<someTransformation>) })

So that the initial array stays the same, but the trafo gets applied during the filtering.

threshold NO_MATCH with custom key not returning all results

  • match-sorter version: 4.0.2
  • node version: 10 & 12
  • npm (or yarn) version: yarn v1.21.1

Relevant code or config

const arr = [
  { value: 'Airstream' },
  { value: 'Avatar' },
  { value: 'Foo' },
  { value: 'Cavan' },
];

const works = matchSorter(arr.map(item => item.value), 'Ava', {
  threshold: matchSorter.rankings.NO_MATCH,
}).map(value => ({ value }));
console.log({ works });

const doesNotWork = matchSorter(arr, 'Ava', {
  keys: [{ threshold: matchSorter.rankings.NO_MATCH, key: 'value' }],
});
console.log({ doesNotWork });

Reproduction repository:
https://codesandbox.io/s/matchsorter-threshold-bug-bbu2r

Problem description:
When trying to use matchSorter.rankings.NO_MATCH with a custom key, the threshold is not respected. Instead it returns a subset of results, as opposed to all the results.

If I instead transform the array into a list of string it works as expected.

Suggested solution:
I very well could be using the keys option incorrectly. If this seems like a valid failing case, I'm happy to open a PR.

Limit the results

Hello.

There's a way to limite the results? For example: I just want the 5 first matches, not the all matches.

Thanks

Polyfill for IE11 - String.prototype.includes

match-sorter: 2.2.1
node: v6.11.3
npm: 5.6.0

We've been using this lib and it failed on Internet Explorer 11 because of the following line:

const containsDash = testString.includes('-')

Object doesn’t support property or method ‘includes’

So I'm wondering if I could contribute to this repo by adding a polyfill such as these ones:

Thanks :)

Inconsistent module resolution with typescript under node/web

  • match-sorter version:2.2.1
  • node version:8.2.1
  • npm (or yarn) version:yarn 1.0.2/npm 5.8.0

What you did:
Basically trying to use match-sorter with typescript in browser and node.

What happened:

An error is thrown when trying to test a file that imports match-sorter.

Reproduction repository:

https://github.com/yakirn/matchsorter-ts

Problem description:
I used a very basic setup. create-react-app with typescript as described here.
I added basic match-sorter code to App.tsx.
When running using yarn start a browser pops up and everything works as expected.
But when trying to test the component using yarn test an error is thrown:
Error: Uncaught [TypeError: match_sorter_1.default is not a function]

Suggested solution:
I'm no sure why, but import matchSorter from 'match-sorter'; works in the browser but not in node.
while import * as matchSorter from 'match-sorter'; works in node, but not in the browser.

I would love to help solve this issue, and later add types definition, but I guess there's a problem with the bundler of this project, and I can use some guidance.
Thanks!

Either-Or search

Is there a way to search by a list of strings and return items that match at least one of the search strings?

For example:

const list = ['hi', 'hey', 'hello', 'sup', 'yo']
matchSorter(list, 'hey' | 'up' | 'plox')

In the above example, I have separated the list of strings with a '|'. I want the the output to contain items that match at least one of the items in this list.

Expected output: ['hey', 'sup']

Use without remove-accents

Is there a way to use this library without pulling in the remove-accents library? My strings are all in English so I don't need to handle diacritics, and would rather not bloat out the bundle size with stuff I don't need. I'm using Webpack.

Looking at dist/match-sorter.esm.js (https://unpkg.com/[email protected]/dist/match-sorter.esm.js), it seems like it's compiled in and there's no way to replace it. It also seems like the original source code is not packaged in the npm package, so I can't just do something like import matchSorter from 'match-sorter/src/index' and then shim remove-accents via NormalModuleReplacementPlugin.

Tip: Search multiple terms (AND filter)

Hi,

Great library, simple and easy to use. Using it in conjunction with react-table, specifically to do a global filter (one input box) to filter across multiple columns in the table.

For me the behavior when searching for multiple terms (space-separated words) does not work as I as the user would expect. However, it works great when match-sorter:ing for each search term separately. So, I thought I'd share this as a recipe for anyone looking for the same thing.

Basic implementation for AND-searching for multiple terms. Each term will be fuzzy-matched, gradually reducing the resulting list. First search term is prioritized (its match will sort the results). Written in typescript, just remove the typings to get javascript.

export function fuzzyGlobalTextFilter<T extends object>(
    rows: Array<T>,
    keys: Array<string>,
    filterValue: string,
) {
    const terms = filterValue?.split(" ");
    if (!terms || !(typeof terms === "object")) {
        return rows;
    }

    return terms.reduceRight((results, term) => matchSorter(results, term, {keys}), rows);
}

For the specific use-case for react-table if someone is interested I landed in this. Basic difference is difference of data-structure, strategy is the same.

rows is the dataset (array, where each element contains the actual data inside the values field).
ids is an array of strings containing the keys to search (so it will search row.values.keyName)
filterValue is the space-separated string of search terms (typically the user input)

export function fuzzyGlobalTextFilterForReactTable<T extends object>(
    rows: Array<Row<T>>,
    ids: Array<IdType<T>>,
    filterValue: FilterValue,
) {
    const terms = (filterValue as string)?.split(" ");
    if (!terms || !(typeof terms === "object")) {
        return rows;
    }

    const filterFunction = (data: Array<Row<T>>, filterVal: string) =>
        matchSorter(data, filterVal, {
            keys: ids.map((id) => (row) => row.values[id]),
        });

    return terms.reduceRight((results, term) => filterFunction(results, term), rows);
}

Hope this helps someone!

/Victor

Improve docs

The project's features have grown passed the simple code example and I think we should probably add more complete and organized documentation, at least for the properties in the options object. Anyone wanna take this on? It'd basically amount to taking code examples from here, and putting them in a (new) Options section (probably below Usage) with subsections for each property with some prose explaining what it's for and a small code example (taken from the Usage section).

support regex search?

  • match-sorter version: ^2.2.3

Suggested solution:

Hi, I fount match-sorter is really useful for my large array. But I really need a regex search.
Just wonder if there is a way to put regex instead of string. Or if this can be a future feature?
Thank you.

Add Support for ImmutableJS

Add support for ImmutableJS data structures.

Allow:

  • A shallow Immutable.List to be passed. (Supports native javascript types inside the List)
    eg.
     match(new List(['a', 'b', 'c'], 'a', /*options*/);
     or
     match(new List([
          { name: 'a' },
          { name: 'b' },
     ]), 'a', { key: 'name' });
  • A deep Immutable.List to be passed. (Supports nested Immutable.Maps over native javascript Objects)
    eg
     match(new List([
          new Map({ name: 'a' }),
          new Map({ name: 'b' })
     ], 'a', { key: 'name' });

All the same functionality that exists with native javascript arrays and objects will be available using List and Maps including nested keys and arrays of values.

Add deprioritization of keys

Sometimes you have a UI where you show a list of items, use multiple keys for filtering, but only display one key. In that situation, it would be nice to de-prioritize values for a key. Here's how I think we could do that (from an external API perspective);

matchSorter(objList, 'g', {keys: ['name', {priority: -1, key: 'color'}]})

In this situation, the ranking of a value is added to the priority of that key. The priority will default to 0 for keys which are strings or keys which are objects but omit priority (like {key: 'color'}.

As far as implementation goes. I think we could just change the code right around here to get the priority based on the keyIndex and add it to the newRank variable before doing the check.

We'll want to make sure we add tests for this. Want to stick with 100% code coverage.

Thoughts?

How to ignore caseRank while sorting?

Hello 👋,
I've got a question and could not find the solution on the internet, so I ask here.

  • match-sorter version: 4.2.1
  • node version: 12.18.4
  • npm (or yarn) version: yarn 1.22.4

What you did: https://codesandbox.io/s/react-codesandbox-forked-t5tb4?file=/src/index.js
What happened: Items are sorted the way I do not expect/want.

Reproduction repository: see sandbox please

Problem description:
I want to sort items alphabetically, without additional case ranking. Case ranking causes unexpected (for me) sort.

Suggested solution:
Maybe, there already is an option to avoid this. But i was unable to find it. It would be great, if i could "just" sort by value, without case ranking consideration.

Maybe something like this could be used: https://github.com/mrceperka/match-sorter/blob/match-sorter-possible-fix/src/index.js#L135

Thanks 🙏

Error with number filtering

Hi. I have found possible bug when trying to filter an array with number type fields containing the same values

  • match-sorter version: 3.5.1
  • node version: 10.15.3
  • npm version: 6.9.0

Code Example:

let p = [{ name: 'samuel', likes: 0 }, { name: 'pedro', likes: 0 }];

const result = matchSorter(p, '0', {
	keys: ['name', 'likes'],
	threshold: rankings.WORD_STARTS_WITH
});

console.log(result);

Error:
TypeError aRankedItem.localeCompare is not a function

Live Example

Problem:
If I'm not mistaken, the problem is that the match-sorter try to call the function of the String class from a variable of Number type. My solution was simply, when trying to compare, I cast the variable to String in any case.

Possible Solution:

function sortRankedItems(a, b) {
	var aFirst = -1;
	var bFirst = 1;
	var aRankedItem = a.rankedItem,
		aRank = a.rank,
		aKeyIndex = a.keyIndex;
	var bRankedItem = b.rankedItem,
		bRank = b.rank,
		bKeyIndex = b.keyIndex;

	if (aRank === bRank) {
		if (aKeyIndex === bKeyIndex) {
			return String(aRankedItem).localeCompare(bRankedItem);
		} else {
			return aKeyIndex < bKeyIndex ? aFirst : bFirst;
		}
	} else {
		return aRank > bRank ? aFirst : bFirst;
	}
}

I dont know if it's good solution but works perfect for now.

Nested keys not matching others in the array

  • match-sorter version: 4.0.1
  • node version: v10.15.3
  • yarn version: 1.19.0

The example provided in the docs is as follows (and only has one item in each name array):

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'}}]

I have a similar structure, however mine only seems to match the first entry in the array.

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, 'j', {keys: ['name.0.first', 'name.0.last]})
// [{name: {first: 'Janice'}}]
// Where's Jenny?! 🤔

Am I doing something wrong?

Case-sensitive exact match should trump all

Currently, because of the way the values are toLowerCase-ed before comparison (https://github.com/kentcdodds/match-sorter/blob/master/src/index.js#L203), equal lowercased values (bob, Bob, boB) will be returned in their original order, with no priority given to a case-sensitive exact match. Here's an example to illustrate

const list = ['bob', 'Bob', 'Bobby', 'Janice'];
matchSorter(list, 'Bob') // will return ['bob', 'Bob', 'Bobby']

Presumably the first result returned should instead be the exact case-sensitive match (Bob).

One way to solve this would be to add a new CASE_SENSITIVE_EQUAL threshold. Thoughts?

Filtering on keys which hold the value of numerical zero gives inaccurate results

  • match-sorter version: 2.2.1
  • node version: 8.9.4
  • npm (or yarn) version: 5.6.0

Relevant code or config

return value ? [].concat(value) : null

What you did:
Use data which contains the number 0 as a property value as below:

const objList = [
  {name: 'Janice', color: 'Green', rank:0},
  {name: 'Fred', color: 'Orange', rank:100},
  {name: 'George', color: 'Blue', rank:20},
  {name: 'Jen', color: 'Red', rank:0},
]

What happened:

The rows which have the numerical value zero are rejected falsely by the null check done while getting all items to compare.

Problem description:

matchSorter(objList, '0', {keys: ['name', 'rank']})

this returns:

[ {name: 'Fred', color: 'Orange', rank: 100 },
  { name: 'George', color: 'Blue', rank: 20 } ]

but should return:

[ {name: 'Janice', color: 'Green', rank:0},
{name: 'Jen', color: 'Red', rank:0}
{name: 'Fred', color: 'Orange', rank: 100 },
{name: 'George', color: 'Blue', rank: 20 } ]

Suggested solution:

Adding a check for the number zero in the return value of the function getItemValues.

return value || value === 0 ? [].concat(value) : null

add threshold to key

Just like we have a threshold in general options, each key should be able to specify a threshold.

matchSorter(objList, 'g', {
  keys: ['name', {threshold: matchSorter.rankings.ACRONYM, key: 'color'}],
})

This would mean that an item would have to have a ranking of ACRONYM with color or greater to be included in the final results. However, if an item did have a ranking with name that would still be included even if it didn't match well enough with key.

Unable to search the word if there are special characters in it

Hi, I have the following array of objects: I want to search the word Powai (case insensitive). But when I search the word I am not getting the results with the word (Powai) with parenthesis. I have attached the code below the JSON. Please help.

[
{
      "id": 31,
      "name": "Powai",
    },
  {
      "id": 3474,
      "name": "Powai Chowk Mulund",
   },
  {
      "id": 3475,
      "name": "Powai Vihar Complex",
    },
  {
      "id": 2428,
      "name": "Forest Club Powai",
    },
  {
      "id": 2635,
      "name": "Hiranandani Powai Bus Station",
    },
  {
      "id": 3561,
      "name": "Ramda Hotel (Powai)",
    },
  {
      "id": 2244,
      "name": "Crisil House (Powai)",
    },
  {
     "id": 2662,
      "name": "I.R.B.Complex(Powai)",
    },
  {
      "id": 2890,
      "name": "Kingston Sez (Powai)",
  },
  {
      "id": 3972,
      "name": "Tatapower Centre (Powai)",
 },
  {
      "id": 2362,
      "name": "Dr.Ambedkar Udyan (Powai)",
  },
  {
      "id": 2389,
      "name": "E.S.I.S.Local Office Powai",
 },
]
  • match-sorter version: ^4.1.0
  • node version: 12.14.0
  • npm (or yarn) version: 6.14.4

Relevant code or config

matchSorter(options, inputValue, {
	keys: ["name"],
	threshold: rankings.WORD_STARTS_WITH,
	keepDiacritics: true,
});

Benchmark

Looking for someone who knows how to write reliable benchmarks to make sure that we don't regress perf and have a baseline for knowing how we can improve perf. Would be totally awesome if we could get an idea of the perf intensive issues in our code so we know where to focus efforts.

Filter based on a boolean

I'm trying to filter based on an active state.

const objList = [
  {name: 'Janice', color: 'Green', active: false},
  {name: 'Fred', color: 'Orange', active: true},
  {name: 'George', color: 'Blue', active: true},
  {name: 'Jen', color: 'Red', active: false},
]

matchSorter(objList, false, {keys: ['active']})

If I try to filter by false it returns the whole list.

It works correctly if I filter by true.

Should I not be able to filter by using a false value? Thanks for the great package.

Multiple priority sort Issue

matchSorter(arr, "t", {keys: ["Name","Surname"]})

In this case if both Name and Surname has "t", still it returns an empty string.
It works if any one of the 2 cases are passed.
Expected:
The return array should be a sorted one having "t" in any of the 2 cases(or both, but giving sort priority "Name")

Demo is not working

The demo throws the following error, which happens because the imported matchSorter.rankings is undefined.

screen shot 2016-08-31 at 9 42 55 am

The demo is using https://npmcdn.com/match-sorter@^1.1.0 which needs to be bumped to the latest version and moved from npmcdn to unpkg.

Doesn't handle null values

  • match-sorter version: ^4.0.2
  • node version: 10.4.0
  • npm (or yarn) version: 6.6

Relevant code or config

const nestedObjList = [
  {name: {first: 'Janice'}},
  {name: {first: null}},
  {name: {first: undefined}},
  {name: {first: 'Jen'}},
]
matchSorter(nestedObjList, 'j', {keys: ['name.first']})

gives:
Error: items.reduce is not a function
What you did:

I provided null/ undefined values in list

What happened:

Screen Shot 2019-11-23 at 00 04 38

Reproduction repository:

Problem description:

Suggested solution:

maintain order of original data if no search term is provided

  • match-sorter version: 2.2.0
  • node version: v8.9.3
  • npm (or yarn) version: npm v5.7.1

Relevant code or config

segments = matchSorter(this.props.segments.list, searchTerm, {
  keys: ['id', 'code', 'description'],
});

What you did:

  1. perform a search with proper value of searchTerm
  2. empty searchTerm out. matchSorter modifies the order of the response, does not maintain the original order when no search term is entered.

Suggested solution:
I guess checking for value before the actual search and sorting is performed. So if searchTerm is empty just return the input as is.

Combine value selector function with min/maxRanking?

I'm not a JS dev, I'm just trying to create bindings for this library for use in F# with Fable. Looking at the docs and the current master branch, it doesn't seem possible to use the value selector function along with minRanking and maxRanking. Is that intended?

With the value selector function, I am talking about this:

matchSorter(list, 'j', {keys: [item => item.name]})

It would be great if this could be combined with setting minRanking and maxRanking.

Feature request: typo tolerance

Are there any plans of adding typo tolerance to the results? For instance, searching "appel" or "applr" in [apple, banana] should return [apple].

Add stripDiacritics capabilities

Basically open source this (cc @JedWatson) and add it as a dependency to match-sorter.

I personally think that this should be enabled by default. Thoughts?

Also, I'm going to work on making it possible to have multiple files right now with our build so we can have dependencies.

Add CLOSE_MATCH threshold

In my app, if the user types "India" they will see "Indonesia" because our threshold is set to MATCH. But this is confusing. I'd like to add a CLOSE_MATCH threshold which will function similar to MATCH but only count if there are only two characters between the previous letter and the next (so "Inda" would match "India" but not "Indonesia").

This threshold would probably go best between MATCHES and ACRONYM.

Match multiple fields

  • match-sorter version: 2.3.0

I love this package, but I usually want to filter through multiple fields.

const objList = [
  {name: 'Janice', color: 'Green'},
  {name: 'Fred', color: 'Orange'},
  {name: 'George', color: 'Blue'},
  {name: 'Jen', color: 'Red'},
]

matchSorter(objList, 'ge bl', {keys: ['name', 'color']})

This example results in [], but I would expect the result to be [{name: 'George', color: 'Blue'}].
So it only tries to match one field, and ignores the other fields.

match keys with array within list of objects

  • match-sorter version: 2.2.0
  • node version: 6.10.2
  • npm (or yarn) version: npm 3.10.10

I am looking to do some search from a list of objects. However, the objects themselves contain couple properties that are arrays.

this.state = {
    searchedOffers: matchSorter(this.props.offers, this.props.searchTerm, {
    keys: ['content[0].merchant_name'],
    });
};

// offers: PropTypes.array,
// searchTerm: PropTypes.string

However this does not work. I am wondering how I can match against an array within the object.

Search through array of objects within list of objects?

  • match-sorter version: 2.0.2
  • node version: 8.5.0
  • npm (or yarn) version: 1.1.0
const list = [
  { data: [ { a: '1' }, {a: '2'} ] }
  { data: [ { a: '2' }, {a: '4'} ] }
]

matchSorter(list, {keys: 'data.a'});

Is there a way to search an array of objects where what I'm actually interested in is the array of objects inside? In the example above, I'm interested in searching through the a property of the data list.

How to ignore Spaces?

  • match-sorter version:
  • node version:
  • npm (or yarn) version:

Relevant code or config

What you did:

What happened:

Reproduction repository:

Problem description:

Suggested solution:

Unlisted dependency

From what I can tell, @babel/runtime/helpers/esm/extends is a dependency but is not listed as a dependency or peer dependency in the package.json.

opt out of certain checking

  • match-sorter: 2.2.0
  • node: v8.9.3
  • npm (or yarn) version: 5.7.1

Problem description:
Is there a way to opt out of certain checking. Like the use case I have, I do not want CASE ACRONYM, ACRONYM & SIMPLE MATCH being used for comparison. So any items that match these 3 things are omitted from final result.

Null Checks for resolving nested key

  • match-sorter version: 2.0.2
  • node version: v4.7.3
  • npm (or yarn) version: 3.10.10

Relevant code or config

const matchSorterFilterMethodFlattened = (filter, rows, column) => {
  return matchSorter(rows, filter.value, { keys: 'thing.nestedThing' });
};

Sample Table data:

[
  {
    "name": "row1",
    "thing": {
      "nestedThing:": "nestedValue"
    }
  },
  {
    "name": "row2",
    "thing": null
  }
]

What you did:

What happened:

Reproduction repository:

Problem description:

value = key.split('.').reduce(function (itemObj, nestedKey) {
return itemObj[nestedKey];

Suggested solution:
Do a null check here (and maybe resolve search result as false for empty nested objects?)

value = key.split('.').reduce(function (itemObj, nestedKey) {
return itemObj[nestedKey];

Add maxRanking and minRanking to keys

Similar use case as in #23, just a different implementation to make it easier for people to fine-tune the experience they're looking for. Here's the API I'm thinking:

matchSorter(objList, 'g', {
  keys: [
    'name',
    {maxRanking: matchSorter.rankings.STARTS_WITH, key: 'color'},
    {minRanking: matchSorter.rankings.ACRONYM, key: 'flavor'},
  ],
})

In this scenario, the ranking for color can only be <= STARTS_WITH and the ranking of flavor can only be >= ACRONYM. This should give a pretty good deal of flexibility.

Option to return matched string

  • match-sorter version: 4.0.1
  • node version: 12.14.1
  • npm version: 6.11.3

Relevant code or config

type Category = {
    text: string;
    synonyms: string[];
}

 const sorted = matchSorter(
            categories,
            term,
            { keys: ['text', 'synonyms'], threshold: matchSorter.rankings.CONTAINS }
        );

What you did:
Search in multiple properties

What happened:
Returned whole matching objects

Problem description:
It's currenty not possible to know which string matched (either text, or one of the synonyms) and one would have to search each object again to find the one that matched.

Suggested solution:
Provide an option to also return the specific matching string. Not sure how this would be possible, as it uses the existing objects type as return type

Result limiting with early exit

Hi,

I am trying to have a local version of a search, which uses a big list of preloaded results (1200 items).

When the user starts typing, the result set for 1-3 letters is very long. I would still like to fastly return a response of e.g. 5 items. With my current data this filtering takes already 50-90ms.

I know the question was already raised here: #52

But the proposed solution does not help the performance hit. Would it be possible to define a number of items that should be matched with certain threshold and then let the match sort return early?

Base sort should still be applied if the search string is empty

  • match-sorter version: 4.2.0
  • node version: 14.8.0
  • npm (or yarn) version: 6.14.7 (or 1.21.1 😛)

Relevant code or config

import matchSorter from 'match-sorter'

const list = ['hi', 'hey', 'hello', 'sup', 'yo']

const sorted = matchSorter(list, '')

What you did:

console.log(sorted);

What happened:

The list was returned unsorted.

Problem description:

As no search string was supplied, all items should have the same ranking, therefore the base sort should be applied as the tie-breaker. i.e. The list should be sorted but it's not.

Suggested solution:

If the search string is empty, apply the base sort instead of just returning the list unsorted.

Additional Note:

I stole the "Relevant code" from the Usage section of the Read Me. When I did, I noticed that it's slightly out of date now as the base sort is not be applied to the output.
matchSorter(list, 'h') // ['hi', 'hey', 'hello'] should be matchSorter(list, 'h') // [ 'hello', 'hey', 'hi' ]

Objects are always sorted by the last key instead of the matching key only (if ranks are same)

  • match-sorter version: v4.0.1
  • node version: v10.15.0
  • npm (or yarn) version: yarn v1.17.3

Relevant code or config

console.log(matchSorter([
        {country: 'Italy', counter: 3},
        {country: 'Italy', counter: 2},
        {country: 'Italy', counter: 1},
      ],
      'Italy',
      {keys: ['country', 'counter']}));
//  =>    {country: 'Italy', counter: 1}, {country: 'Italy', counter: 2}, {country: 'Italy', counter: 3}

What you did:
We're using match-sorter for searching through lists of real estate objects (pre-sorted with most recent ones first).

What happened:
When several items match and have the same rank, they are resorted in an unexpected order – the value of the last key.

Reproduction repository:
See code example above. I'll also create a test showing that behavior in the PR.

Problem description:
In v4.0, a change was introduced to sort results alphabetically. When using multiple keys for matching, the last key in the list is used for sorting equally ranked items, which leads to unexpected results.

Suggested solution:
Sort by the value of the key that actually matched instead of the last one.

Default match broken for cyrillic/russian because of incorrect diactrics removal

I opened an issue with node-diactrics but it was not updated for 4 years, so opening an issue here too.
nijikokun/Diacritics.js#6

  • match-sorter version: 2.3.0
  • node version: v11.13.0
  • npm (or yarn) version: 1.15.2

Relevant code or config

console.log(diacritics.clean("л"))

л Л is a lower/upercase case cyrillic/russian L and should not be replaced with latin n when matching.

This breaks case-insensitive search in match-sorter with default settings for any cyrillic text which has this letter.

node-diacritics also has this problem:
andrewrk/node-diacritics#32

but remove-accents does not: https://github.com/tyxla/remove-accents

Example word which does not match: "Лед"

What you did:

const list = ["Лед", "лед"];
const filtered = matchSorter(list, "л");

What happened:

["лед"], both items should match

Reproduction repository:

https://codesandbox.io/s/r76w8oqo8o

Problem description:

Suggested solution:

  • Default keepDiacritics to true
  • Replace used library
  • allow user to bring his own diactrics library

callback to allow for transformation of matched items whilst keeping original list

It would be nice to be able to return all items in the array with a transform function to allow for adding properties to the matched items but keeping the original list.

For example: If I wanted to return a list of the matched items with a visible key attached so allow for animation on the nodes that are not matched eg collapsing them from view.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.