Teo Dragovic
Teo Dragovic

Reputation: 3518

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.

Upvotes: 2

Views: 4563

Answers (6)

Nina Scholz
Nina Scholz

Reputation: 386550

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

Upvotes: 7

Yuri Gor
Yuri Gor

Reputation: 1463

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

Upvotes: 0

Abhinav Galodha
Abhinav Galodha

Reputation: 9878

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

Upvotes: 0

Ja9ad335h
Ja9ad335h

Reputation: 5075

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

Upvotes: 2

Jared Smith
Jared Smith

Reputation: 21926

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

Upvotes: 4

trincot
trincot

Reputation: 350127

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

Upvotes: 7

Related Questions