n4thyra

n4thyra

Build a tree from a flat structure recursively

There are similar questions such as Build a Tree of Structs but I haven’t found any that would deal with exactly the same issue that I need to solve.

Let’s say we have an (unsorted) set of data that we load from DB

[
  %{id: 2, name: "Child 2", parent_id: nil},
  %{id: 1 ,name: "Child 1", parent_id: nil},
  %{id: 3, name: "GrandChild 1", parent_id: 1},
  %{id: 5, name: "Child 3", parent_id: nil},
  %{id: 6, name: "GrandGrandChild 1", parent_id: 3},
]

Desired result

[
  %{
    id: 2, 
    name: "Child 2",
    children: [],
  }, 
 %{
    id: 1, 
    name: "Child 1",
    children: [
       %{
           id: 3, 
           name: "GrandChild 1",
           children: [
             %{
                 id: 6, 
                 name: "GrandGrandChild 1",
                 children: []
              }, 
           ]
        }, 
    ],
  }, 
 %{
    id: 5, 
    name: "Child 3",
    children: [],
  }, 
]

There can be N levels, not just 3

Most Liked

stefanchrobot

stefanchrobot

You could use digraph and digraph_utils to do this.

kokolegorille

kokolegorille

I would start by selecting root nodes, where parent_id is nil. Then use a recursive constructor to build children’s field.

iex> list = [
  %{id: 2, name: "Child 2", parent_id: nil},
  %{id: 1, name: "Child 1", parent_id: nil},
  %{id: 3, name: "GrandChild 1", parent_id: 1},
  %{id: 5, name: "Child 3", parent_id: nil},
  %{id: 6, name: "GrandGrandChild 1", parent_id: 3}
]
iex> new_node = fn node -> %{id: node.id, name: node.name, children: Enum.filter(list, & &1.parent_id == node.id) |> Enum.map(& new_node.(&1))} end
#Function<44.65746770/1 in :erl_eval.expr/5>
iex> Enum.filter(list, & is_nil(&1.parent_id)) |> Enum.map(& new_node.(&1))                                                                        
[
  %{children: [], id: 2, name: "Child 2"},
  %{
    children: [
      %{
        children: [%{children: [], id: 6, name: "GrandGrandChild 1"}],
        id: 3,
        name: "GrandChild 1"
      }
    ],
    id: 1,
    name: "Child 1"
  },
  %{children: [], id: 5, name: "Child 3"}
]
stefanchrobot

stefanchrobot

Here’s how you can do it with :digraph:

input = [
  %{id: 2, name: "Child 2", parent_id: nil},
  %{id: 1 ,name: "Child 1", parent_id: nil},
  %{id: 3, name: "GrandChild 1", parent_id: 1},
  %{id: 5, name: "Child 3", parent_id: nil},
  %{id: 6, name: "GrandGrandChild 1", parent_id: 3},
]

graph = :digraph.new()

fake_root = %{id: 0, name: "Root", parent_id: nil}

# Add vertices + the fake root vertex.
for %{id: id} = node <- [fake_root | input] do
  :digraph.add_vertex(graph, id, Map.delete(node, :parent_id))
end

# Add all edges; if no parent set, add an edge to the fake root.
for %{id: id, parent_id: parent_id} <- input do
  :digraph.add_edge(graph, id, parent_id || fake_root.id)
end

# I needed recursion, so I introduced a module.
defmodule TreeBuilder do
  def build(graph, vertex) do
    children =
      for child <- :digraph.in_neighbours(graph, vertex) do
        build(graph, child)
      end

    {^vertex, label} = :digraph.vertex(graph, vertex)
    
    Map.put(label, :children, children)
  end
end

%{children: children} = TreeBuilder.build(graph, fake_root.id)
# Get rid of the fake root.
children

The idea here is that the input represents the vertices (id and name) and the edges (parent_id). I introduced a fake root node since working with a tree is easier. I first build the graph/tree and then traverse it. Since :digraph is based on ETS, the code does not need to carry the state around.

al2o3cr

al2o3cr

Enum.group_by is a little more efficient than repeatedly scanning the whole list with Enum.filter, and (IMO) is easier to understand:

defmodule GroupThings do
  def run(data) do
    groups = Enum.group_by(data, & &1.parent_id)

    Enum.map(groups[nil], &associate_children(&1, groups))
  end

  defp associate_children(node, groups) do
    children =
      Enum.map(groups[node.id] || [], &associate_children(&1, groups))

    Map.put(node, :children, children)
  end
end

The key here is transforming the input into a map that can answer the question "what are the nodes that have a given parent_id":

%{
  1 => [%{id: 3, name: "GrandChild 1", parent_id: 1}],
  3 => [%{id: 6, name: "GrandGrandChild 1", parent_id: 3}],
  nil => [
    %{id: 2, name: "Child 2", parent_id: nil},
    %{id: 1, name: "Child 1", parent_id: nil},
    %{id: 5, name: "Child 3", parent_id: nil}
  ]
}

Where Next?

Popular in Questions Top

dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
LegitStack
I’m hoping you guys can give me some general advice and perhaps code examples if you’re feeling up to it. I’m very interested in Elixir,...
New
pgiesin
This should be a simple problem but I just can’t seem to figure it out. I have a standalone Elixir app that won’t find the database. Dep...
New
logicmason
Hi there, I'm working through my first release with elixir/phoenix. I've built a release with distillery and found that it crashes when I...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list....
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
Codball
Mix format works fine if run from the cmd. I’ve followed this to facilitate the implementation into VSC which involves downloading an ext...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New

Other popular topics Top

srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
New
Tee
can someone please explain to me how Enum.reduce works with maps
New
sergio
I couldn’t find any guides that worked well with Phoenix 1.6.0 and esbuild. I hope this helps people test the waters and eases you into t...
New
polypush135
As many of you may have realized by now (sorry for all the posts here) I’ve been working on a db problem where I’m trying to aggregate a ...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New
Nvim
Elixir appears to be a superior language to Python. I don’t see any advantage of Python over Elixir. Are there any?
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New

We're in Beta

About us Mission Statement