Search code examples
javascriptarraysjsontypescriptfilter

Filter object from nested json array at any level retaining the parent array hierarchy


I have a multi-level nested array similar to the below structure. What I'm trying to do is to filter for list of cases whose has a 'flag' value of 'Gold'. Only nodes with type = 'Model' or type = 'Project' will have direct children cases with type = 'Case' having flag value.

const models = [
    {
        "name": "Group1",
        "type": "Group",
        "cases": [
            {
                "name": "Model1",
                "type": "Model",
                "cases": [
                    {
                        "name": "Case1",
                        "type": "Case",
                        "flag": "Gold",
                        "cases": []
                    }
                ]
            },
            {
                "name": "Model2",
                "type": "Model",
                "cases": [
                    {
                        "name": "Project1",
                        "type": "Project",
                        "cases": [
                            {
                                "name": "Project2",
                                "type": "Project",
                                "cases": [
                                    {
                                        "name": "Case2",
                                        "type": "Case",
                                        "flag": "Gold",
                                        "cases": []
                                    },
                                    {
                                        "name": "Case3",
                                        "type": "Case",
                                        "flag": "Silver",
                                        "cases": []
                                    }
                                ]
                            }
                        ]
                    },
                    {
                        "name": "Case4",
                        "type": "Case",
                        "flag": "Gold",
                        "cases": []
                    }
                ]
            },
        ]
    },
    {
        "name": "Group2",
        "type": "Group",
        "cases": [
            {
                "name": "Model3",
                "type": "Model",
                "cases": [
                    {
                        "name": "Case5",
                        "type": "Case",
                        "flag": "Gold",
                        "cases": []
                    }
                ]
            }
        ]
    }
]

For example, if I want to filter only for list of cases which flag = 'Gold' the result would like the below.

Group1
    ...Model1
         { Case1 }
    ...Model2
        ...Project1
             ...Project2
                 { Case2 }
        { Case4 }
Group2
    ...Model3
        { Case5 }

I tried the below two approaches but not getting the expected result. Using filter option.

 private getFilteredList(models: any[], term: string): any[] {
        const filterList = (data, term) => {
            data.filter((e) => {
                if (e.type === "Model" || e.type === "Project") {
                    e.cases = e.cases.filter(c => c.flag === term);
                }
                else if (e.cases) {
                    filterList(e.cases, term);
                }
            });
        }
        filterList(models, term);

        return models;
    }

Using the reduce function. This piece of code results in infinite loop.

private getFilteredList(models: any[], term: string): any[] {
        return models.reduce((list, item) => {
            if (item.flag === term) {
                list.push(item);
            } else if (item.cases && item.cases.length > 0) {
                const caseList = this.getFilteredList(item.cases, term);

                if (caseList.length > 0) {
                    list.push({ name: item.name, cases: caseList });
                }
            }

            return list;
        }, []);
    }

Any help appreciated.


Solution

  • Basically, this will traverse the tree, looking for items where flag === term. Indeed it returns 4 items for Gold.

    Now comes function filterTreeNodes. We will take the list of nodes from previous step, and trim the original tree, iterating again this time keeping only the nodes that have one of their children to be one of the list.

    const models=[{name:"Group1",type:"Group",cases:[{name:"Model1",type:"Model",cases:[{name:"Case1",type:"Case",flag:"Gold",cases:[]}]},{name:"Model2",type:"Model",cases:[{name:"Project1",type:"Project",cases:[{name:"Project2",type:"Project",cases:[{name:"Case2",type:"Case",flag:"Gold",cases:[]},{name:"Case3",type:"Case",flag:"Silver",cases:[]}]}]},{name:"Case4",type:"Case",flag:"Gold",cases:[]}]},]},{name:"Group2",type:"Group",cases:[{name:"Model3",type:"Model",cases:[{name:"Case5",type:"Case",flag:"Gold",cases:[]}]}]}];
    
    // returns list of nodes that match "term"
    function getFilteredList(models, term) {
      var result = [];
    
      models.forEach(function(model) {
        if (model.type === 'Case' && model.flag === term) {
          result.push(model)
        }
        if (model.cases) {
          var children = getFilteredList(model.cases, term)
          children.forEach(function(child) {
            result.push(child)
          })
        }
      })
    
      return result;
    }
    
    // returns tree having children from the list
    function filterTreeNodes(tree, list) {
      return tree.filter(item => {
        if (list.some(node => node === item)) {
          return true;
        }
        if (item.cases) {
          item.cases = filterTreeNodes(item.cases, list);
          return item.cases.length > 0;
        }
        return false;
      });
    }
    
    var nodes = getFilteredList(models, "Gold")
    var tree = filterTreeNodes(models, nodes)
    console.log(tree)
    .as-console-wrapper {
      min-height: 100%;
    }

    As a final version, we'll combine it into a generic function that filters a tree with a condition for keeping each node.

    const models=[{name:"Group1",type:"Group",cases:[{name:"Model1",type:"Model",cases:[{name:"Case1",type:"Case",flag:"Gold",cases:[]}]},{name:"Model2",type:"Model",cases:[{name:"Project1",type:"Project",cases:[{name:"Project2",type:"Project",cases:[{name:"Case2",type:"Case",flag:"Gold",cases:[]},{name:"Case3",type:"Case",flag:"Silver",cases:[]}]}]},{name:"Case4",type:"Case",flag:"Gold",cases:[]}]},]},{name:"Group2",type:"Group",cases:[{name:"Model3",type:"Model",cases:[{name:"Case5",type:"Case",flag:"Gold",cases:[]}]}]}];
    
    
    function filterTree(tree, foo_condition) {
    
      function getNodes(models, foo_condition) {
        var result = [];
        models.forEach(function(model) {
          if (foo_condition(model)) {
            result.push(model)
          }
          if (model.cases) {
            var children = getNodes(model.cases, foo_condition)
            children.forEach(function(child) {
              result.push(child)
            })
          }
        })
        return result;
      }
    
      function filterTreeNodes(tree, list) {
        return tree.filter(item => {
          if (list.some(node => node === item)) {
            return true;
          }
          if (item.cases) {
            item.cases = filterTreeNodes(item.cases, list);
            return item.cases.length > 0;
          }
          return false;
        });
      }
    
      var list = getNodes(tree, foo_condition)
      var resultTree = filterTreeNodes(tree, list)
    
      return resultTree;
    }
    
    
    function filterTreeByTerm(tree, term) {
      return filterTree(models, function(node) {
        return node.type === 'Case' && node.flag === term
      })
    }
    
    // usage: 
    console.log(filterTreeByTerm(models, "Gold"))
    .as-console-wrapper {
      min-height: 100%;
    }