Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to filter an array of objects for case insensitive matches from any object key

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:

  1. I want it to match case-insensitive
  2. I want it to return matches from any key
  3. I want it to find using contains not exact match

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'

// ... 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.

like image 279
agm1984 Avatar asked Nov 05 '17 03:11

agm1984


People also ask

How do you filter objects with keys?

JavaScript objects don't have a filter() method, you must first turn the object into an array to use array's filter() method. You can use the Object. keys() function to convert the object's keys into an array, and accumulate the filtered keys into a new object using the reduce() function as shown below.

Can you filter an array of objects?

One can use filter() function in JavaScript to filter the object array based on attributes. The filter() function will return a new array containing all the array elements that pass the given condition. If no elements pass the condition it returns an empty array.


2 Answers

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);
like image 149
xinthose Avatar answered Sep 30 '22 13:09

xinthose


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))
like image 40
Dmitry Druganov Avatar answered Sep 30 '22 13:09

Dmitry Druganov