Reputation: 17132
I have this sample code here, and I am trying to filter matching objects without exploding the code complexity or performance:
This code here filters matches based on one explicitly defined key and it's not case insensitive.
const people = [
{ firstName: 'Bob', lastName: 'Smith', status: 'single' },
{ firstName: 'bobby', lastName: 'Suxatcapitalizing', status: 'single' },
{ firstName: 'Jim', lastName: 'Johnson', status: 'complicated' },
{ firstName: 'Sally', lastName: 'Fields', status: 'relationship' },
{ firstName: 'Robert', lastName: 'Bobler', status: 'single' },
{ firstName: 'Johnny', lastName: 'Johannson', status: 'complicated' },
{ firstName: 'Whaley', lastName: 'McWhalerson', status: 'relationship'
rogueBonusKey: 'bob likes salmon' },
]
const searchString = 'Bob'
const found = people.filter((person) => {
if (person.firstName === searchString) return true
})
console.log(found)
THE GOAL:
contains
not exact matchSomething like this:
// const people = [
// { firstName: 'Bob', lastName: 'Smith', status: 'single' },
// { firstName: 'bobby', lastName: 'Suxatcapitalizing', status: 'single' },
// { firstName: 'Jim', lastName: 'Johnson', status: 'complicated' },
// { firstName: 'Sally', lastName: 'Fields', status: 'relationship' },
// { firstName: 'Robert', lastName: 'Bobler', status: 'single' },
// { firstName: 'Johnny', lastName: 'Johannson', status: 'complicated' },
// { firstName: 'Whaley', lastName: 'McWhalerson', status: 'relationship'
// rogueBonusKey: 'bob likes salmon' },
// ]
// const searchString = 'bob'
// ... magic
// console.log(found)
// { firstName: 'Bob', lastName: 'Smith', status: 'single' },
// { firstName: 'bobby', lastName: 'Suxatcapitalizing', status: 'single' },
// { firstName: 'Robert', lastName: 'Bobler', status: 'single' },
// { firstName: 'Whaley', lastName: 'McWhalerson', status: 'relationship'
// rogueBonusKey: 'bob likes salmon' },
I have scoured the documentations related to Array.filter()
and I can definitely make solutions that involve Array.reduce()
and looping over stuff with Object.keys(obj).forEach()
, but I want to know if there is a concise, performant way to handle this kind of fuzzy search.
Something like this:
const people = [
{ firstName: 'Bob', lastName: 'Smith', status: 'single' },
{ firstName: 'bobby', lastName: 'Suxatcapitalizing', status: 'single' },
{ firstName: 'Jim', lastName: 'Johnson', status: 'complicated' },
{ firstName: 'Sally', lastName: 'Fields', status: 'relationship' },
{ firstName: 'Robert', lastName: 'Bobler', status: 'single' },
{ firstName: 'Johnny', lastName: 'Johannson', status: 'complicated' },
{ firstName: 'Whaley', lastName: 'McWhalerson', status: 'relationship' },
rogueBonusKey: 'bob likes salmon' },
]
const searchString = 'Bob'
const found = people.filter((person) => {
if (person.toString().indexOf(searchString).toLowerCase !== -1) return true
})
console.log(found)
[edit] This definitely works, but is it acceptable?
const people = [
{ firstName: 'Bob', lastName: 'Smith', status: 'single' },
{ firstName: 'bobby', lastName: 'Suxatcapitalizing', status: 'single' },
{ firstName: 'Jim', lastName: 'Johnson', status: 'complicated' },
{ firstName: 'Sally', lastName: 'Fields', status: 'relationship' },
{ firstName: 'Robert', lastName: 'Bobler', status: 'single' },
{ firstName: 'Johnny', lastName: 'Johannson', status: 'complicated' },
{ firstName: 'Whaley', lastName: 'McWhalerson', status: 'relationship',
rogueBonusKey: 'bob likes salmon' },
]
const searchString = 'Bob'
const found = people.filter((person) => {
const savageMatch = JSON.stringify(person)
.toLowerCase()
.indexOf(searchString.toLowerCase()) !== -1
console.log(savageMatch)
if (savageMatch) return true
})
console.log(found)
Memory footprint optimized:
const found = people.filter((person) => JSON.stringify(person)
.toLowerCase()
.indexOf(searchString.toLowerCase()) !== -1
)
Converted to a function:
const fuzzyMatch = (collection, searchTerm) =>
collection.filter((obj) => JSON.stringify(obj)
.toLowerCase()
.indexOf(searchTerm.toLowerCase()) !== -1
)
console.log(fuzzyMatch(people, 'bob'))
There are some great answers in here; so far, I have selected this for my filtering needs:
const people = [
{ imageURL: 'http://www.alice.com/goat.jpeg', firstName: 'Bob', lastName: 'Smith', status: 'single' },
{ firstName: 'bobby', lastName: 'Suxatcapitalizing', status: 'single' },
{ firstName: 'Jim', lastName: 'Johnson', status: 'complicated' },
{ firstName: 'Sally', lastName: 'Fields', status: 'relationship' },
{ firstName: 'Robert', lastName: 'Bobler', status: 'single' },
{ firstName: 'Johnny', lastName: 'Johannson', status: 'complicated' },
{
firstName: 'Ronald', lastName: 'McDonlad', status: 'relationship',
rogueBonusKey: 'bob likes salmon'
},
{
imageURL: 'http://www.bob.com/cats.jpeg', firstName: 'Whaley', lastName: 'McWhalerson', status: 'relationship',
rogueBonusKey: 'bob hates salmon'
},
]
const searchString = 'bob'
const options = {
caseSensitive: false,
excludedKeys: ['imageURL', 'firstName'],
}
const customFind = (collection, term, opts) => {
const filterBy = () => {
const searchTerms = (!opts.caseSensitive) ? new RegExp(term, 'i') : new RegExp(term)
return (obj) => {
for (const key of Object.keys(obj)) {
if (searchTerms.test(obj[key]) &&
!opts.excludedKeys.includes(key)) return true
}
return false
}
}
return collection.filter(filterBy(term))
}
const found = customFind(people, searchString, options)
console.log(found)
I made it able to support case sensitivity and to exclude specific keys.
Upvotes: 12
Views: 15908
Reputation: 311
Array.prototype.filter() also works as well:
const includesQuery = (value) => {
return value['SOME_KEY'].toLowerCase().includes(query.toLowerCase());
}
const filtered = this.myArray.filter(includesQuery);
console.log(filtered);
Upvotes: 0
Reputation: 19070
You can also use Regular expressions with i
modifier to perform case-insensitive matching and the method RegExp.prototype.test()
It is very convenient when you want to evaluate multiple object properties like:
new RegExp(searchString, 'i').test(
person.email || person.firstName || person.lastName
)
Code:
const people = [{ firstName: 'Bob', lastName: 'Smith', status: 'single' }, { firstName: 'bobby', lastName: 'Suxatcapitalizing', status: 'single' }, { firstName: 'Jim', lastName: 'Johnson', status: 'complicated' }, { firstName: 'Sally', lastName: 'Fields', status: 'relationship' }, { firstName: 'Robert', lastName: 'Bobler', status: 'single' }, { firstName: 'Johnny', lastName: 'Johannson', status: 'complicated' }, { firstName: 'Whaley', lastName: 'McWhalerson', status: 'relationship', rogueBonusKey: 'bob likes salmon' }]
const searchString = 'Bob'
const found = people.filter(({ firstName }) =>
new RegExp(searchString, 'i').test(firstName))
console.log(found)
Upvotes: 2
Reputation: 3820
You can use Array.prototype.find()
.
const people = [
{ firstName: 'Bob', lastName: 'Smith', status: 'single' },
{ firstName: 'bobby', lastName: 'Suxatcapitalizing', status: 'single' },
{ firstName: 'Jim', lastName: 'Johnson', status: 'complicated' },
{ firstName: 'Sally', lastName: 'Fields', status: 'relationship' },
{ firstName: 'Robert', lastName: 'Bobler', status: 'single' },
{ firstName: 'Johnny', lastName: 'Johannson', status: 'complicated' },
{ firstName: 'Whaley', lastName: 'McWhalerson', status: 'relationship',
'rogueBonusKey': 'bob likes salmon' },
]
const searchString = 'Bob';
const person = people.find(({ firstName }) => firstName.toLowerCase() === searchString.toLowerCase());
console.log(person);
Upvotes: 1
Reputation: 6282
you need to filter the array, then filter each key in the objects to match a regular expression. This example breaks down the problem into single responsibility funcitons and connects them with functional concepts eg.
Performance tests are included, in chrome this is consistently faster than Dmitry's example. I have not tested any other browsers. This may be because of optimisations that chrome takes to allow the jit to process the script faster when the code is expressed as small single responsibility functions that only take one type of data as an input and one type of data as an output.
Due to the tests this takes around 4 seconds to load.
const people = [
{ firstName: 'Bob', lastName: 'Smith', status: 'single' },
{ firstName: 'bobby', lastName: 'Suxatcapitalizing', status: 'single' },
{ firstName: 'Jim', lastName: 'Johnson', status: 'complicated' },
{ firstName: 'Sally', lastName: 'Fields', status: 'relationship' },
{ firstName: 'Robert', lastName: 'Bobler', status: 'single' },
{ firstName: 'Johnny', lastName: 'Johannson', status: 'complicated' },
{ firstName: 'Whaley', lastName: 'McWhalerson', status: 'relationship', rogueBonusKey: 'bob likes salmon' },
]
// run a predicate function over each key of an object
// const hasValue = f => o =>
// Object.keys(o).some(x => f(o[x]))
const hasValue = f => o => {
let key
for (key in o) {
if (f(o[key])) return true
}
return false
}
// convert string to regular expression
const toReg = str =>
new RegExp(str.replace(/\//g, '//'), 'gi')
// test a string with a regular expression
const match = reg => x =>
reg.test(x)
// filter an array by a predicate
// const filter = f => a => a.filter(a)
const filter = f => a => {
const ret = []
let ii = 0
let ll = a.length
for (;ii < ll; ii++) {
if (f(a[ii])) ret.push(a[ii])
}
return ret
}
// **magic**
const filterArrByValue = value => {
// create a regular expression based on your search value
// cache it for all filter iterations
const reg = toReg(value)
// filter your array of people
return filter(
// only return the results that match the regex
hasValue(match(reg))
)
}
// create a function to filter by the value 'bob'
const filterBob = filterArrByValue('Bob')
// ########################## UNIT TESTS ########################## //
console.assert('hasValue finds a matching value', !!hasValue(x => x === 'one')({ one: 'one' }))
console.assert('toReg is a regular expression', toReg('reg') instanceof RegExp)
console.assert('match finds a regular expression in a string', !!match(/test/)('this is a test'))
console.assert('filter filters an array', filter(x => x === true)([true, false]).length === 1)
// ########################## RESULTS ########################## //
console.log(
// run your function passing in your people array
'find bob',
filterBob(people)
)
console.log(
// or you could call both of them at the same time
'find salmon',
filterArrByValue('salmon')(people)
)
// ########################## BENCHMARKS ########################## //
// dmitry's first function
const filterBy = (term) => {
const termLowerCase = term.toLowerCase()
return (person) =>
Object.keys(person)
.some(prop => person[prop].toLowerCase().indexOf(termLowerCase) !== -1)
}
// dmitry's updated function
const escapeRegExp = (str) => // or better use 'escape-string-regexp' package
str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&")
const filterBy2 = (term) => {
const re = new RegExp(escapeRegExp(term), 'i')
return person => {
for (let prop in person) {
if (!person.hasOwnProperty(prop)) {
continue;
}
if (re.test(person[prop])) {
return true;
}
}
return false;
}
}
// test stringify - incredibly slow
const fuzzyMatch = (collection, searchTerm) =>
collection.filter((obj) => JSON.stringify(obj)
.toLowerCase()
.indexOf(searchTerm.toLowerCase()) !== -1
)
new Benchmark({ iterations: 1000000 })
// test my function - fastest
.add('synthet1c', function() {
filterBob(people)
})
.add('dmitry', function() {
people.filter(filterBy('Bob'))
})
.add('dmitry2', function() {
people.filter(filterBy2('Bob'))
})
.add('guest', function() {
fuzzyMatch(people, 'Bob')
})
.run()
<link rel="stylesheet" type="text/css" href="https://codepen.io/synthet1c/pen/WrQapG.css">
<script src="https://codepen.io/synthet1c/pen/WrQapG.js"></script>
Upvotes: 2
Reputation: 2188
You should give fusejs a shot. http://fusejs.io/
It has some interesting settings such as threshold, which allow some typo error (0.0 = perfect, 1.0 = match anything) and keys
to specify any keys you want to search in.
const people = [
{ firstName: 'Bob', lastName: 'Smith', status: 'single' },
{ firstName: 'bobby', lastName: 'Suxatcapitalizing', status: 'single' },
{ firstName: 'Jim', lastName: 'Johnson', status: 'complicated' },
{ firstName: 'Sally', lastName: 'Fields', status: 'relationship' },
{ firstName: 'Robert', lastName: 'Bobler', status: 'single' },
{ firstName: 'Johnny', lastName: 'Johannson', status: 'complicated' },
{ firstName: 'Whaley', lastName: 'McWhalerson', status: 'relationship'
rogueBonusKey: 'bob likes salmon' },
]
const fuseOptions = {
caseSensitive: false,
shouldSort: true,
threshold: 0.2,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [
"firstName",
"lastName",
"rogueBonusKey",
]
};
const search = (txt) => {
const fuse = new Fuse(people, fuseOptions);
const result = fuse.search(txt);
return result;
}
Upvotes: 2
Reputation: 2348
If we assume that all properties are strings, then you might do in the following way
const people = [
// ...
]
const searchString = 'Bob'
const filterBy = (term) => {
const termLowerCase = term.toLowerCase()
return (person) =>
Object.keys(person)
.some(prop => person[prop].toLowerCase().indexOf(termLowerCase) !== -1)
}
const found = people.filter(filterBy(searchString))
console.log(found)
Update: alternative solution with RegExp and more old-school :) but 2x faster
const people = [
// ...
]
const searchString = 'Bob'
const escapeRegExp = (str) => // or better use 'escape-string-regexp' package
str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&")
const filterBy = (term) => {
const re = new RegExp(escapeRegExp(term), 'i')
return person => {
for (let prop in person) {
if (!person.hasOwnProperty(prop)) {
continue;
}
if (re.test(person[prop])) {
return true;
}
}
return false;
}
}
const found = people.filter(filterBy(searchString))
Upvotes: 7
Reputation: 1
If the entire matched object is expected result you can use for..of
loop, Object.values()
, Array.prototype.find()
consy searchString = "Bob";
const re = new RegExp(searchString, "i");
let res = [];
for (const props of Object.values(people))
Object.values(props).find(prop => re.test(prop)) && res.push(props);
Upvotes: 1