0

How could I extract a list of values out of a nested array; matching a condition ?

const tree = {
   "menu_id":"root",
   "open":true,
   "items":[
      {
        "menu_id":"Brussels",
        "open":false,
         "items":[
            {
               "menu_id":"Brussels/CBD",
               "open":true,
               "items":[
                  {
                     "menu_id":"Brussels/CBD/Centre",
                     "open":false
                  },
                  {
                     "menu_id":"Brussels/CBD/Louise",
                     "open":true
                  },
                  {
                     "menu_id":"Brussels/CBD/Léopold",
                     "open":false
                  },
                  {
                     "menu_id":"Brussels/CBD/Midi",
                     "open":true
                  },
                  {
                     "menu_id":"Brussels/CBD/North",
                     "open":false
                  }
               ]
            }
        ]
      }
    ]
}

I would like to get a list of menu_id values, where the open attribute is false, that would thus give:

var closed = ['Brussels','Brussels/CBD/Centre','Brussels/CBD/Léopold','Brussels/CBD/North']

How could I do that ?

Thanks !

gordie
  • 1,637
  • 3
  • 21
  • 41
  • 1
    Please visit the [help], take the [tour] to see what and [ask]. Do some research, search for related topics on SO; if you get stuck, post a [mcve] of your attempt, noting input and expected output using the `[<>]` snippet editor. – mplungjan Jan 27 '21 at 11:20
  • so ignore that `"menu_id":"root", "open":true,` – mplungjan Jan 27 '21 at 11:22
  • @mplungjan: While this question perhaps doesn't show enough effort, your suggested duplicate is quite different. This one is looking for a way to collect all matching nodes into an array, regardless of depth. That one keeps the tree structure intact, keeping only the matching leaf nodes. – Scott Sauyet Jan 27 '21 at 13:50

1 Answers1

1

One useful way to write this is to build a generic function that handles trees of all sorts and arbitrary predicates, and then build your function atop that.

This version does that and then partially applies a function describing the tree structure to get a new function, partially applies a predicate to that to get another function which accepts a tree and returns all nodes that match the predicate. Finally, our main function accepts a tree, calls the previous function and extracts the id node from each result.

const filterDeep = (getKids) => (pred) => (tree) => [
  ... (pred (tree) ? [tree] : []),
  ... (getKids (tree)) .flatMap (filterDeep (getKids) (pred))
]

const filterItems = 
  filterDeep (({items = []}) => items)

const closed =
  filterItems (({open}) => !open)

const closedIds = (tree) =>
  closed (tree) .map (({menu_id}) => menu_id)

const tree = {menu_id: "root", open: true, items: [{menu_id: "Brussels", open: false, items: [{menu_id: "Brussels/CBD", open: true, items: [{menu_id: "Brussels/CBD/Centre", open: false}, {menu_id: "Brussels/CBD/Louise", open: true}, {menu_id: "Brussels/CBD/Léopold", open: false}, {menu_id: "Brussels/CBD/Midi", open: true}, {menu_id: "Brussels/CBD/North", open: false}]}]}]}

console .log (closedIds (tree))

We could obviously write closedIds directly atop filterDeep, with something like this:

const closedIds = (tree) =>
  filterDeep (({items}) => items) (({open}) => open == false) (tree) 
    .map (({menu_id}) => menu_id)

and, while there's nothing precisely wrong with that, those intermediate functions are possibly reusable, and I think help make the problem more clear. For instance, filterItems is a function that takes a predicate and a tree where the children of a node are in an array called items and returns all the nodes (at whatever level of nesting) matching the predicate. It seems pretty useful.


However, if you just want a dedicated function, we can merge all that down into something fairly simple:

const closedIds = (tree) => [
  ... (tree.open ? [] : [tree.menu_id]),
  ... (tree.items || []) .flatMap (closedIds)
]

const tree = {menu_id: "root", open: true, items: [{menu_id: "Brussels", open: false, items: [{menu_id: "Brussels/CBD", open: true, items: [{menu_id: "Brussels/CBD/Centre", open: false}, {menu_id: "Brussels/CBD/Louise", open: true}, {menu_id: "Brussels/CBD/Léopold", open: false}, {menu_id: "Brussels/CBD/Midi", open: true}, {menu_id: "Brussels/CBD/North", open: false}]}]}]}

console .log (closedIds (tree))
Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • 1
    another use case for `filterDeep` :D – Mulan Jan 27 '21 at 16:46
  • @Thankyou: I'm still not sure about the name. There are two different styles of deep filtering I use. One creates an array of matches; the other keeps the tree structure but reduces the number of nodes. Neither seems more deserving than the other to get the name `filterDeep`, but calling one `deepFilter` and the other `filterDeep` is too confusing. – Scott Sauyet Jan 27 '21 at 16:53
  • for pruning trees but keeping shape i have used names `prune` or `shake` – Mulan Jan 27 '21 at 17:01
  • 1
    @Thankyou: `prune` sounds good to me. I'll keep `deepFilter` for this. – Scott Sauyet Jan 27 '21 at 17:07
  • i should clarify, `prune` returns the tree without the "filtered" bits, whereas `shake` returns the "filtered" bits. nothing wrong with `deepFilter` though ^^ – Mulan Jan 27 '21 at 17:19
  • @Thankyou: Yes, I don't particularly like `shake` as a name, but `prune` is crystal-clear. When we wanted an opposite-sense of `filter` in Ramda, we were somewhat happy with `reject`. But for a tree, `prune` seems clear enough, and I don't (yet) have anything better than `shake` for its complement, but will keep looking. – Scott Sauyet Jan 27 '21 at 17:25