简体   繁体   中英

Filtering array based on value in deeply nested object in javascript

I have array with following structure:

var topics = [
  {
    "id": 1,
    "name": "topic title 1",
    "sub_categories": [
      {
        "id": 1,
        "name": "category title 1",
        "indicators": [
          {
            "id": 1,
            "name": "indicator 1",
            "sub_category_id": 1
          },
          {
            "id": 7,
            "name": "indicator 7 - foo",
            "sub_category_id": 1
          }
        ]
      },
      {
        "id": 6,
        "name": "category title 6",
        "indicators": [
          {
            "id": 8,
            "name": "indicator 8",
            "sub_category_id": 6
          }
        ]
      }
    ]
  },
  {
    "id": 2,
    "name": "topic title 2",
    "sub_categories": [
      {
        "id": 2,
        "name": "category 2",
        "indicators": [
          {
            "id": 2,
            "name": "indicator 2 - foo",
            "sub_category_id": 2
          }
        ]
      },
      {
        "id": 4,
        "name": "category 4",
        "indicators": [
          {
            "id": 5,
            "name": "indicator 5",
            "sub_category_id": 4
          }
        ]
      }
    ]
  }
];

I need to get filtered array based on value of name property in indicators array, removing non-matched indicators and both topic and sub_categories with empty indicators. So for input of foo , result would be:

var topics = [
  {
    "id": 1,
    "name": "topic title 1",
    "sub_categories": [
      {
        "id": 1,
        "name": "category title 1",
        "indicators": [
          {
            "id": 7,
            "name": "indicator 7 - foo",
            "sub_category_id": 1
          }
        ]
      }
    ]
  },
  {
    "id": 2,
    "name": "topic title 2",
    "sub_categories": [
      {
        "id": 2,
        "name": "category 2",
        "indicators": [
          {
            "id": 2,
            "name": "indicator 2 - foo",
            "sub_category_id": 2
          }
        ]
      }
    ]
  }
];

I tried to use lodash methods based on other similar SO question but all examples either have only one level of nesting or same keys on all levels (ie. children). I would be fine with either getting back new array or mutating existing one.

Here is an ES6 solution based on reduce , filter and Object.assign :

 function filterTree(topics, find) { return topics.reduce(function (acc, topic) { const sub_categories = topic.sub_categories.reduce(function (acc, cat) { const indicators = cat.indicators.filter( ind => ind.name.includes(find) ); return !indicators.length ? acc : acc.concat(Object.assign({}, cat, { indicators })); }, []); return !sub_categories.length ? acc : acc.concat(Object.assign({}, topic, { sub_categories })); }, []); } // sample data const topics = [ { "id": 1, "name": "topic title 1", "sub_categories": [ { "id": 1, "name": "category title 1", "indicators": [ { "id": 1, "name": "indicator 1", "sub_category_id": 1 }, { "id": 7, "name": "indicator 7 - foo", "sub_category_id": 1 } ] }, { "id": 6, "name": "category title 6", "indicators": [ { "id": 8, "name": "indicator 8", "sub_category_id": 6 } ] } ] }, { "id": 2, "name": "topic title 2", "sub_categories": [ { "id": 2, "name": "category 2", "indicators": [ { "id": 2, "name": "indicator 2 - foo", "sub_category_id": 2 } ] }, { "id": 4, "name": "category 4", "indicators": [ { "id": 5, "name": "indicator 5", "sub_category_id": 4 } ] } ] } ]; // Call the function var res = filterTree(topics, 'foo'); // Output result console.log(res);
 .as-console-wrapper { max-height: 100% !important; top: 0; }

You could use an iterative and recursive approach for filtering the given array, without hard wired properties.

 const deepFilter = (array, indicator) => { return array.filter(function iter(o) { return Object.keys(o).some(k => { if (typeof o[k] === 'string' && o[k].includes(indicator)) { return true; } if (Array.isArray(o[k])) { o[k] = o[k].filter(iter); return o[k].length; } }); }); } const topics = [{ id: 1, name: "topic title 1", sub_categories: [{ id: 1, name: "category title 1", indicators: [{ id: 1, name: "indicator 1", sub_category_id: 1 }, { id: 7, name: "indicator 7 - foo", sub_category_id: 1 }] }, { id: 6, name: "category title 6", indicators: [{ id: 8, name: "indicator 8", sub_category_id: 6 }] }] }, { id: 2, name: "topic title 2", sub_categories: [{ id: 2, name: "category 2", indicators: [{ id: 2, name: "indicator 2 - foo", sub_category_id: 2 }] }, { id: 4, name: "category 4", indicators: [{ id: 5, name: "indicator 5", sub_category_id: 4 }] }] }]; console.log(deepFilter(topics, 'foo'));
 .as-console-wrapper { max-height: 100% !important; top: 0; }

This can pretty much all be done with ES 5 array methods (no library or polyfill needed for IE 9+):

var passed = topics.filter(function(x) {
  return x.subcategories.some(function(y) {
    return y.indicators.some(function(z) {
      return Boolean(z.name.match(/foo/));
    });
  });
});

While this is total one-off code, the situation is perhaps too complicated for an easily digestible general-purpose solution (although I'd love to see someone prove me wrong).

UPDATE

After taking a closer look at the output you will need to use reduce instead of filter:

var passed = topics.reduce((acc, x) => {
  var hasfoo = x.subcategories.reduce((accum, y) => {
    var ls = y.indicators.filter(z => z.name.match(/foo/));
    if (ls.length) {
      accum.push(Object.assign({}, y, {indicators: ls}));
    }
    return accum;
  }, []);

  if (hasfoo.length) {
    acc.push(Object.assign({}, x, {subcategories: hasfoo}));
  }

  return acc;
}, []);

Astute readers will note the recursive pattern here. Abstracting that out is left as an exercise, I'm tapped out. Object.assign will need to be polyfilled for old browsers (trivial though).

this will also modify existing topics

var result = topics.filter(top => 
    (top.sub_categories = top.sub_categories.filter(cat => 
        (cat.indicators = cat.indicators.filter(i => i.name.match(/foo/))).length)
    ).length
);

Example

 var topics = [{ "id": 1, "name": "topic title 1", "sub_categories": [{ "id": 1, "name": "category title 1", "indicators": [{ "id": 1, "name": "indicator 1", "sub_category_id": 1 }, { "id": 7, "name": "indicator 7 - foo", "sub_category_id": 1 }] }, { "id": 6, "name": "category title 6", "indicators": [{ "id": 8, "name": "indicator 8", "sub_category_id": 6 }] }] }, { "id": 2, "name": "topic title 2", "sub_categories": [{ "id": 2, "name": "category 2", "indicators": [{ "id": 2, "name": "indicator 2 - foo", "sub_category_id": 2 }] }, { "id": 4, "name": "category 4", "indicators": [{ "id": 5, "name": "indicator 5", "sub_category_id": 4 }] }] }]; var result = topics.filter(top => (top.sub_categories = top.sub_categories.filter(cat => (cat.indicators = cat.indicators.filter(i => i.name.match(/foo/))).length)).length); console.log(result);

One more implementation.

        topics.forEach(function(topic, indexTopic, indexTopicArray) { 
                topic.sub_categories.forEach(function(subCat, indexsubCat, arraysubCat) { 
                         subCat.indicators = subCat.indicators.filter(indic => indic.name.includes("foo"));    
                         if(subCat.indicators.length === 0) { 
                                indexTopicArray[indexTopic].sub_categories.splice(indexsubCat, 1); 
    }})});
    console.log(topics);

Complete Code.

 var topics = [ { "id": 1, "name": "topic title 1", "sub_categories": [ { "id": 1, "name": "category title 1", "indicators": [ { "id": 1, "name": "indicator 1", "sub_category_id": 1 }, { "id": 7, "name": "indicator 7 - foo", "sub_category_id": 1 } ] }, { "id": 6, "name": "category title 6", "indicators": [ { "id": 8, "name": "indicator 8", "sub_category_id": 6 } ] } ] }, { "id": 2, "name": "topic title 2", "sub_categories": [ { "id": 2, "name": "category 2", "indicators": [ { "id": 2, "name": "indicator 2 - foo", "sub_category_id": 2 } ] }, { "id": 4, "name": "category 4", "indicators": [ { "id": 5, "name": "indicator 5", "sub_category_id": 4 } ] } ] } ]; topics.forEach(function(topic, indexTopic, indexTopicArray) { topic.sub_categories.forEach(function(subCat, indexsubCat, arraysubCat) { subCat.indicators = subCat.indicators.filter(indic => indic.name.includes("foo")); if(subCat.indicators.length === 0) { indexTopicArray[indexTopic].sub_categories.splice(indexsubCat, 1); }})}); console.log(topics);

You can do it using _.filterDeep from deepdash extension for lodash :

var endsWith = 'foo';
var foundFoo = _.filterDeep(
  obj,
  function(value, key) {
    return _.endsWith(value.name, endsWith);
  },
  { tree: { children: ['sub_categories', 'indicators'] } }
);

Here is a full test for your case

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM