2

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.

Teo Dragovic
  • 3,438
  • 20
  • 34

6 Answers6

7

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; }
trincot
  • 317,000
  • 35
  • 244
  • 286
  • Interesting, after a comment by the op I came up with an almost Identical solution. Nice use of object shorthand properties. – Jared Smith Feb 10 '17 at 20:23
7

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; }
Nic Scozzaro
  • 6,651
  • 3
  • 42
  • 46
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
4

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

Jared Smith
  • 19,721
  • 5
  • 45
  • 83
  • Thank you for quick response but your solution returns everything under topic that contains matched indicator so I would still get back subcategories that contain indicator lists with no matches. – Teo Dragovic Feb 10 '17 at 20:06
  • @TeoDragovic updated answer. Also see trinicot's, almost exact same as my updated. – Jared Smith Feb 10 '17 at 20:22
2

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);
Ja9ad335h
  • 4,995
  • 2
  • 21
  • 29
0

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);
Abhinav Galodha
  • 9,293
  • 2
  • 31
  • 41
0

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

Yuri Gor
  • 1,353
  • 12
  • 26