1

Given a search string:

Jane

and this array of objects:

[
  {
    name: 'Jane Smith',
    address: '123 Main St, Boston, MA 01234',
    telephone: {
      primary: '1234567890',
      secondary: '1112223333'
    }
  },
  {
    name: 'John Smith',
    address: '333 Main St, New York, NY 56789',
    telephone: {
      primary: '2223334444',
      secondary: '3334445555'
    }
  },
  ...
]

we can filter the array by name:

arr.filter(person => person.name.includes(search))

Great. That works well if all we are only searching by each object's name property.

What is the best practice for filtering across all properties of an object?

Do we have to do something like:

arr.filter(person => 
  person.name.includes(search) ||
  person.address.includes(search) ||
  person.telephone.primary.includes(search) ||
  person.telephone.secondary.includes(search)
)

This becomes tedious and error prone if there are more than a couple properties.

Is there a way to filter an array if any property's value matches a search string?

Update:

This works nicely for top level properties on the person object.

.filter(person => {
  for (let property in person) {
    return String(person[property]).includes(search)
  }
})

Working on trying to find a nice solution for recursively searching through properties that may themselves be objects.

Raphael Rafatpanah
  • 19,082
  • 25
  • 92
  • 158
  • 1
    Would you want to do this recursively or for only the top level, you can iterate through properties of object literals using [`for...in`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in). – Spencer Wieczorek Mar 05 '17 at 01:41
  • @SpencerWieczorek, That works well for top level objects. How would you then recursively test if `obj[prop]` is an object itself? – Raphael Rafatpanah Mar 05 '17 at 02:01
  • 1
    Same idea but you would call a recursive function, [take a look at this question](http://stackoverflow.com/questions/2549320/looping-through-an-object-tree-recursively). – Spencer Wieczorek Mar 05 '17 at 02:04

3 Answers3

1

Use Object.keys and Array#some in a recursive function (I have called it deepIncludes) to check if any property (or sub-property) includes the search string. Here I have used this to store the search string, because that allows you to filter your array of people with this syntax:

let result = array.filter(deepIncludes, 'Search String')

function deepIncludes (object) {
  return Object.keys(object).some(k => {
    let v = object[k]
    return (v && typeof v == 'object') ? deepIncludes.call(this, v) : String(v).includes(this)
  })
}

let array = [
  {
    name: 'Jane Smith',
    address: '123 Main St, Boston, MA 01234',
    telephone: {
      primary: '1234567890',
      secondary: '1112223333'
    }
  },
  {
    name: 'John Smith',
    address: '333 Main St, New York, NY 56789',
    telephone: {
      primary: '2223334444',
      secondary: '3334445555'
    }
  }
]

// Usage:
console.log(
  array.filter(deepIncludes, '3334445555') //=> [ { name: 'John Smith', ... } ]
)
gyre
  • 16,369
  • 3
  • 37
  • 47
1

You can use JSON.stringify with the replacer parameter as a way to traverse the object.

function search(obj, str) {
  var found = false;

  JSON.stringify(obj, (key, value) => {
    if (!found && typeof value === 'string' && value.includes(str)) found = true;
    else return value;
  });

  return found;
}
  • Does this improve or diminish performance when compared to recursive `for...in`? – Raphael Rafatpanah Mar 05 '17 at 03:52
  • Without having done a benchmark, I would say that probably `for...in` is faster, since this code builds a JSON string (only to then throw it away), and will also continue doing a bit of work after the match is found. –  Mar 05 '17 at 04:36
0

In the end I chose something close to this:

function search (obj, text) {
  for (let prop in obj) {
    if (String(obj[prop]).includes(text)) {
      return true
    } else if (typeof obj[prop] === 'object') {
      return search(obj[prop], text)
    }
  }
}

Usage:

.filter(person => search(person, text))

Update:

The for...in method does not work well for my use case. I'm not sure why, but some keys are not searched.

Using the Object.keys method works perfectly.

function search (obj, text) {
  return Object.keys(obj).some(k => 
    typeof obj[k] === 'object'
      ? search(obj[k], text)
      : String(obj[k]).includes(text)
  )
}

(same usage)

Raphael Rafatpanah
  • 19,082
  • 25
  • 92
  • 158
  • This code is a bit broken. For instance, it will return true when searching `search({a:{b: 42}}, 'b')`, since `b` (a property) occurs in the stringified object. Perhaps you should reverse the order of the two `if` statements. –  Mar 05 '17 at 04:55
  • @torazaburo, Yes, I'm noticing that now. Will update if I figure it out. Also, for some reason, some (top level) properties are not being searched at all. – Raphael Rafatpanah Mar 05 '17 at 04:58
  • @torazaburo, For some reason, using `Object.keys` works perfectly. But `for....in` does not. – Raphael Rafatpanah Mar 05 '17 at 05:10