Nathan Power
Nathan Power

Reputation: 650

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:

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.

Upvotes: 54

Views: 47715

Answers (9)

bstiffler582
bstiffler582

Reputation: 112

Was looking for another way to solve this problem without directly mutating the children array and came up with 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' } ] }
    ]
  },
];

const filtered = input.reduce(function fr(acc, curr) {
  
  if (curr.children) {
    const children = curr.children.reduce(fr, []);
    if (children.length) {
        return acc.concat({ ...curr, children: children });
    }
  }
  if (curr.value.includes('Hit')) {
    return acc.concat(curr);
  } else {
    return acc;
  }
}, []);

console.log(filtered);

Upvotes: 0

vincent
vincent

Reputation: 2181

Here is a different type of solution using object-scan.

This solution is iterative instead of recursive. It works because object-scan iterates in delete safe order. Basically we traverse into the tree and break on any match. Then we keep track of matches at the different depth (ensuring that we reset that information appropriately as we traverse into a neighbouring branches).

It's mostly an academic exercise as the recursive approach is cleaner and faster. However this answer might be interesting if there is additional processing that needs to be done or stack depth errors are a problem.

// const objectScan = require('object-scan');

const myInput = [{ 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' }] }] }];
const myFilterFn = ({ value }) => value.includes('Hit');

const rewrite = (input, filterFn) => objectScan(['**(^children$)'], {
  breakFn: ({ isMatch, value }) => isMatch && filterFn(value),
  filterFn: ({
    parent, property, key, value, context
  }) => {
    const len = (key.length - 1) / 2;
    if (context.prevLen <= len) {
      context.matches.length = context.prevLen + 1;
    }
    context.prevLen = len;

    if (context.matches[len + 1] === true || filterFn(value)) {
      context.matches[len] = true;
      return false;
    }
    parent.splice(property, 1);
    return true;
  },
  useArraySelector: false,
  rtn: 'count'
})(input, { prevLen: 0, matches: [] });

console.log(rewrite(myInput, myFilterFn)); // returns number of deletions
// => 8

console.log(myInput);
// => [ { 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' } ] } ]
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/[email protected]"></script>

Disclaimer: I'm the author of object-scan

Upvotes: 0

Mulan
Mulan

Reputation: 135357

Array.prototype.flatMap is a good fit for recursive filtering. Similar to map, filter and reduce, using flatMap does not modify the original input -

const findHits = (t = []) =>
  t.flatMap(({ value, children }) => {
    if (value.startsWith("Hit"))
      return [{ value, children }]
    else {
      const r = findHits(children)
      return r.length ? [{ value, children: r }] : []
    }
  })

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'}]}]}]

const result =
  findHits(input)

console.log(JSON.stringify(result, null, 2))

[
  {
    "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"
      }
    ]
  }
]

Upvotes: 1

Malik Bagwala
Malik Bagwala

Reputation: 3019

Here is a good solution which utilizes the array reduce function which results in more readable code then the other solutions. Also it is more readable in my opinion. We are calling the filter function recursively to filter an array along with its children

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" }] },
    ],
  },
];

function recursiveFilter(arr) {
  return arr.reduce(function filter(prev, item) {
    if (item.value.includes("Hit")) {
      if (item.children && item.children.length > 0) {
        return prev.concat({
          ...item,
          children: item.children.reduce(filter, []),
        });
      } else {
        return item;
      }
    }
    return prev;
  }, []);
}

console.log(recursiveFilter(input));

Upvotes: 1

sudheer nunna
sudheer nunna

Reputation: 1719

const input = [
  {
    value: 'Miss1',
    children: [
      { value: 'Miss1' },
      { 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: 'Miss14asds',
    children: [
      { value: 'Hit4sdas' },
      { value: 'Miss15', children: [ { value: 'Miss16' } ] }
    ]
  },
];

function filter(arr, term) {
    var matches = [];
    
    if (!Array.isArray(arr)) return matches;

    arr.forEach(function(i) {
     
        if (i.value === term) {
    
         const filterData =  (i.children && Array.isArray(i.children))? i.children.filter(values => values.value ===term):[];
         console.log(filterData)
         i.children =filterData;
            matches.push(i);
          
        } else {
       // console.log(i.children)
            let childResults = filter(i.children, term);
            if (childResults.length)
     matches.push(Object.assign({}, i, { children: childResults }));
        }
    })

    return matches;
}


const filterData= filter(input,'Miss1');
console.log(filterData)

Below code for filter the parent and child array data using recursive function

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))

Upvotes: 1

Yuri Gor
Yuri Gor

Reputation: 1463

Alternatively you can use _.filterDeep method from deepdash extension for lodash:

var keyword = 'Hit';
var foundHit = _.filterDeep(
  input,
  function(value) {
    return value.value.includes(keyword);
  },
  {
    tree: true,
    onTrue: { skipChildren: true },
  }
);

Here is a full test for your case

Upvotes: 0

user1106925
user1106925

Reputation:

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.

Upvotes: 69

Zohaib Ijaz
Zohaib Ijaz

Reputation: 22885

I think it will be a recursive solution. Here is one that I tried.

function find(obj, key) {
  if (obj.value && obj.value.indexOf(key) > -1){
    return true;
  }
  if (obj.children && obj.children.length > 0){
    return obj.children.reduce(function(obj1, obj2){
      return find(obj1, key) || find(obj2, key);
    }, {}); 
  } 
  return false;
}

var output = input.filter(function(obj){
     return find(obj, 'Hit');
 });
console.log('Result', output);

Upvotes: 2

jj689
jj689

Reputation: 456

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;
}

Upvotes: 15

Related Questions