Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Recursively filter array of objects

Hitting a wall with this one, thought I would post it here in case some kind soul has come across a similar one. I have some data that looks something like this:

const input = [   {     value: 'Miss1',     children: [       { value: 'Miss2' },       { value: 'Hit1', children: [ { value: 'Miss3' } ] }     ]   },   {     value: 'Miss4',     children: [       { value: 'Miss5' },       { value: 'Miss6', children: [ { value: 'Hit2' } ] }     ]   },   {     value: 'Miss7',     children: [       { value: 'Miss8' },       { value: 'Miss9', children: [ { value: 'Miss10' } ] }     ]   },   {     value: 'Hit3',     children: [       { value: 'Miss11' },       { value: 'Miss12', children: [ { value: 'Miss13' } ] }     ]   },   {     value: 'Miss14',     children: [       { value: 'Hit4' },       { value: 'Miss15', children: [ { value: 'Miss16' } ] }     ]   }, ]; 

I don't know at run time how deep the hierarchy will be, i.e. how many levels of objects will have a children array. I have simplified the example somewhat, I will actually need to match the value properties against an array of search terms. Let's for the moment assume that I am matching where value.includes('Hit').

I need a function that returns a new array, such that:

  • Every non-matching object with no children, or no matches in children hierarchy, should not exist in output object

  • Every object with a descendant that contains a matching object, should remain

  • All descendants of matching objects should remain

I am considering a 'matching object' to be one with a value property that contains the string Hit in this case, and vice versa.

The output should look something like the following:

const expected = [   {     value: 'Miss1',     children: [       { value: 'Hit1', children: [ { value: 'Miss3' } ] }     ]   },   {     value: 'Miss4',     children: [       { value: 'Miss6', children: [ { value: 'Hit2' } ] }     ]   },   {     value: 'Hit3',     children: [       { value: 'Miss11' },       { value: 'Miss12', children: [ { value: 'Miss13' } ] }     ]   },   {     value: 'Miss14',     children: [       { value: 'Hit4' },     ]   } ]; 

Many thanks to anyone who took the time to read this far, will post my solution if I get there first.

like image 784
Nathan Power Avatar asked Jun 30 '16 19:06

Nathan Power


People also ask

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.

Does filter manipulate the array?

The filter() method creates a new array filled with elements that pass a test provided by a function. The filter() method does not execute the function for empty elements. The filter() method does not change the original array.


2 Answers

Using .filter() and making a recursive call as I described in the comment above is basically what you need. You just need to update each .children property with the result of the recursive call before returning.

The return value is just the .length of the resulting .children collection, so if there's at least one, the object is kept.

var res = input.filter(function f(o) {   if (o.value.includes("Hit")) return true    if (o.children) {     return (o.children = o.children.filter(f)).length   } }) 

const input = [    {      value: 'Miss1',      children: [        { value: 'Miss2' },        { value: 'Hit1', children: [ { value: 'Miss3' } ] }      ]    },    {      value: 'Miss4',      children: [        { value: 'Miss5' },        { value: 'Miss6', children: [ { value: 'Hit2' } ] }      ]    },    {      value: 'Miss7',      children: [        { value: 'Miss8' },        { value: 'Miss9', children: [ { value: 'Miss10' } ] }      ]    },    {      value: 'Hit3',      children: [        { value: 'Miss11' },        { value: 'Miss12', children: [ { value: 'Miss13' } ] }      ]    },    {      value: 'Miss14',      children: [        { value: 'Hit4' },        { value: 'Miss15', children: [ { value: 'Miss16' } ] }      ]    },  ];    var res = input.filter(function f(o) {    if (o.value.includes("Hit")) return true      if (o.children) {      return (o.children = o.children.filter(f)).length    }  })  console.log(JSON.stringify(res, null, 2))

Note that .includes() on a String is ES7, so may need to be patched for legacy browsers. You can use the traditional .indexOf("Hit") != -1 in its place.


To not mutate the original, create a map function that copies an object and use that before the filter.

function copy(o) {   return Object.assign({}, o) }  var res = input.map(copy).filter(function f(o) {   if (o.value.includes("Hit")) return true    if (o.children) {     return (o.children = o.children.map(copy).filter(f)).length   } }) 

To really squeeze the code down, you could do this:

var res = input.filter(function f(o) {   return o.value.includes("Hit") ||          o.children && (o.children = o.children.filter(f)).length }) 

Though it gets a little hard to read.

like image 111
4 revs, 2 users 99%user1106925 Avatar answered Sep 23 '22 09:09

4 revs, 2 users 99%user1106925


Here's a function that'll do what you're looking for. Essentially it will test every item in arr for a match, then recursively call filter on its children. Also Object.assign is used so that the underlying object isn't changed.

function filter(arr, term) {     var matches = [];     if (!Array.isArray(arr)) return matches;      arr.forEach(function(i) {         if (i.value.includes(term)) {             matches.push(i);         } else {             let childResults = filter(i.children, term);             if (childResults.length)                 matches.push(Object.assign({}, i, { children: childResults }));         }     })      return matches; } 
like image 25
jj689 Avatar answered Sep 23 '22 09:09

jj689