1

I am working on a filtering feature that filters through a rendered list of data. My goal is to be able to filter data objects with user selected filters and return any data object that matches all of the selected filter options.

So for example, here is a simulated array of selected options:

//This simulates a current filter options array
const filterOptions = ['Black', 'Green', '10.00', 'Wacky', 'false'];

Here is an example of the data array and object properties I would be comparing these values against:

const data = {
    games: [
        {
            name: 'Game1',
            color: 'Black',
            price: '1.00',
            theme: 'Lucky',
            licensed: true
        },
        {
            name: 'Game2',
            color: 'Pink',
            price: '1.00',
            theme: 'Multiple',
            licensed: true
        },
                {
            name: 'Game3',
            color: 'Black',
            price: '5.00',
            theme: 'Crossword',
            licensed: false
        },
                {
            name: 'Game4',
            color: 'Green',
            price: '5.00',
            theme: 'Lucky',
            licensed: true
        },
                {
            name: 'Game5',
            color: 'Black',
            price: '10.00',
            theme: 'Wacky',
            licensed: false
        },
                {
            name: 'Game6',
            color: 'Green',
            price: '10.00',
            theme: 'Wacky',
            licensed: false
        },
    ]
}

Considering the above mentioned simulated filter options, I would expect to see Game5 & Game6. Both are 10.00, are either Black or Green, have the wacky theme and have a false value for license.

My thinking is that it would be easier to drop the use of the 'loose values' array, and possibly create an object with arrays of the selected filter options, with the property of those options arrays being the name of the property I intend to compare it against in the data object.

For example:

`const filterOptions = 
  {
    color: ['Black', 'Green'],
    price: ['10.00'],
    theme: ['Wacky'],
    licensed: [false]
  }`

And then, I could use something like Object.keys(filterOptions) to make it possible to loop the selected filter options object. My only thing here is that it seems for every game, I would need to loop the filterOptions object like so:

const filterOptionsKeys = Object.keys(filterOptions);
const { games } = data;

games.forEach(game => {
//Game loop
  console.log(game)
    //Loop through filterOptions key array
    filterOptionsKeys.forEach(key => {
      console.log(key);
         //Loop through each index of the filterOptions option arrays 
         filterOptions[key].forEach(option => {
           console.log(option); //Assuming this is where I would make the comparisons
         })
    })
})

In total, it seems like I would need 2 internal forEach() loops for every iteration of games.forEach(). The data object I am working with returns just over 5000 game objects, and I feel like this approach would be extremely time consuming with a data array that large.

I am pretty stumped on my approach to this, and I am worried I will get to entrenched in doing this a certain way that in the end, will not be sufficient. Is there maybe something I could make use of in lodash that would aid in doing something like this? I have used the lodash library on occasion, but I am not sure how I could apply it to something with this many matching conditions. So my question to sum this all up, would be: what is a smart way to approach this?

maison.m
  • 813
  • 2
  • 19
  • 34

1 Answers1

0

While iterating over the objects, check that .every one of the object's values (except the name) (cast to strings, if needed) are included in the filterOptions array:

const filterOptions = ['Black', 'Green', '10.00', 'Wacky', 'false'];

const data = {
  games: [{
      name: 'Game1',
      color: 'Black',
      price: '1.00',
      theme: 'Lucky',
      licensed: true
    },
    {
      name: 'Game2',
      color: 'Pink',
      price: '1.00',
      theme: 'Multiple',
      licensed: true
    },
    {
      name: 'Game3',
      color: 'Black',
      price: '5.00',
      theme: 'Crossword',
      licensed: false
    },
    {
      name: 'Game4',
      color: 'Green',
      price: '5.00',
      theme: 'Lucky',
      licensed: true
    },
    {
      name: 'Game5',
      color: 'Black',
      price: '10.00',
      theme: 'Wacky',
      licensed: false
    },
    {
      name: 'Game6',
      color: 'Green',
      price: '10.00',
      theme: 'Wacky',
      licensed: false
    },
  ]
}

const matchingObjects = data.games.filter(
  ({ name, ...game }) => Object.values(game).every(
    value => filterOptions.includes(String(value))
  )
);
console.log(matchingObjects);

The "loose" values aren't a problem as long as you can come up with consistent rules for them, the code is still quite concise.

If you're worried about performance (though it probably doesn't matter), if the filterOptions can have a reasonable number of items, you can use a Set instead of an array. This is helpful because Set.has has a computational complexity of O(1), compared to Array.includes, which is O(n):

const filterOptions = new Set(['Black', 'Green', '10.00', 'Wacky', 'false']);

const data = {
  games: [{
      name: 'Game1',
      color: 'Black',
      price: '1.00',
      theme: 'Lucky',
      licensed: true
    },
    {
      name: 'Game2',
      color: 'Pink',
      price: '1.00',
      theme: 'Multiple',
      licensed: true
    },
    {
      name: 'Game3',
      color: 'Black',
      price: '5.00',
      theme: 'Crossword',
      licensed: false
    },
    {
      name: 'Game4',
      color: 'Green',
      price: '5.00',
      theme: 'Lucky',
      licensed: true
    },
    {
      name: 'Game5',
      color: 'Black',
      price: '10.00',
      theme: 'Wacky',
      licensed: false
    },
    {
      name: 'Game6',
      color: 'Green',
      price: '10.00',
      theme: 'Wacky',
      licensed: false
    },
  ]
}

const matchingObjects = data.games.filter(
  ({ name, ...game }) => Object.values(game).every(
    value => filterOptions.has(String(value))
  )
);
console.log(matchingObjects);
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • Excellent thank you so much for this, I definitely learned something new tonight. Would you mind explaining how `name` is omitted? Does it have something to do with adding `name` here in `({ name, ...game })`? Could I add more properties here if I want to also omit them from the check? – maison.m Dec 30 '19 at 02:12
  • That's right. It destructures the `name` property into a standalone variable, while collecting the rest of the properties into a separate object named `game`. See https://stackoverflow.com/a/54605288 – CertainPerformance Dec 30 '19 at 02:14
  • Wow, that is perfect. Thank you again for the information tonight, this has not only helped me further my progress in my project but I've also added something to my toolbelt! – maison.m Dec 30 '19 at 02:16
  • The issue I am running into here is that in the case of a search like so: `const filterOptions = ['Black']` this does not return anything because it's checking a match over every property. I need to be able to return any game that is `Black` in this particular case. The user could select any number of filter options. From 1 option up to 20 options. For instance I might only want to show `Black` games as in the example above, and this function is checking every game property for a match. Since `Black` is not found in any other game property other than `color`, it just returns false. – maison.m Dec 30 '19 at 16:24
  • 1
    That's at odds with the logic of the original example, for which you wanted to *return any data object that matches all of the selected filter options.*. If you want something that returns an object if it matches *at least one* option instead, that'd be different: change `Object.values(game).every(` to `Object.values(game).some(` – CertainPerformance Dec 31 '19 at 02:04