amedeo

amedeo

Build a Tree of Structs

Hi,
I’m working on a Elixir/Phoenix small project in my spare time and I found myself writing code to build a tree structure starting from a list of nodes.

Here is the input:

[
%{id: 1, children: [], parent_id: nil},
%{id: 2, children: [3], parent_id: nil},
%{id: 3, children: [], parent_id: 2}
]

and the expected output:

%TreeNode{
   id: "ROOT",
   children: [
      %TreeNode{
           id: 1,
           parent_id: "ROOT"
           children: []
      },
      %TreeNode{
           id: 2,
           parent_id: "ROOT"
           children: [
               %TreeNode{
                   id: 3,
                   parent_id: 2
                   children: []
                }
           ]
      }
  ],
  parent_id: nil
}

I’ve come to the following implementation but it is quite long and I’ve the feeling that it could be written in a different way.
Do you have some idea or suggestions on how to write it?

defmodule TestTree do

  defmodule TreeNode do
    defstruct id: nil, children: [], parent_id: nil
  end

  @_ROOT_NODE "ROOT"

  def build_tree(list) do
    # use the build_hierarchy to get two Maps as support data for the next step
    {id_to_nodes, id_to_children} = build_hierarchy(list, %{@_ROOT_NODE=>%TreeNode{id: @_ROOT_NODE}}, %{@_ROOT_NODE=>[]})
    # build the tree
    build_tree(Map.keys(id_to_nodes), id_to_nodes, id_to_children)
  end

  # Creates two Maps:
  # one Map keeps the information on Id -> %TreeNode
  # the other Map is an Id -> [ /child id 1/, /child id 2/, ...]
  defp build_hierarchy([], a, b), do: {a, b}
  defp build_hierarchy([m | nodes], id_to_nodes, id_to_children) do
    parent_key = m.parent_id || @_ROOT_NODE

    new_node = %TreeNode{ id: m.id, parent_id: parent_key}

    id_to_nodes = Map.put_new(id_to_nodes, m.id, new_node)

    # update the parent children
    id_to_children = Map.put_new_lazy(id_to_children, parent_key, fn -> [] end)
    children = id_to_children[parent_key]
    id_to_children = %{id_to_children | parent_key => [new_node.id | children]}

    build_hierarchy(nodes, id_to_nodes, id_to_children)
  end


  # the function will build the tree (tail) recursively examining each node_id
  # if the current node has no children then it is added to the parent node
  # if the node has children then its processing is postponed, we need first to take care of the ones without children
  defp build_tree([@_ROOT_NODE], id_to_nodes, _), do: id_to_nodes[@_ROOT_NODE]
  defp build_tree([node_id | node_ids], id_to_nodes, id_to_children) do
    node = id_to_nodes[node_id]
    parent_id = node.parent_id
    children = Map.get(id_to_children, node_id, [])
    if length(children) > 0 do
      # node not ready yet, it has some children
      build_tree(node_ids ++ [node_id], id_to_nodes, id_to_children)
    else
      # the node has no children let's add it to its parent children and remove it from the id_to_children
      parent_node = id_to_nodes[parent_id]
      id_to_nodes = %{id_to_nodes | parent_id => %{parent_node | children: [node | parent_node.children]}}
      id_to_children = %{id_to_children | parent_id => List.delete(id_to_children[parent_id], node_id)}
      build_tree(node_ids, id_to_nodes, id_to_children)
    end
  end

end

Most Liked

michalmuskala

michalmuskala

That depends largely on the data. If the data is sorted (as in the sample you provided), it can be transformed using the following:

def build_tree(elements) do
  {tree, []} = build_tree(elements, %TreeNode{id: nil, children: []})
  tree
end

defp build_tree([%{parent_id: parent, id: id, children: _} | rest], 
                %{id: parent, children: children} = tree) do
  {node, rest} = build_tree(rest, %TreeNode{id: id, children: []})
  build_tree(rest, %{tree | children: [node | children]})
end
defp build_tree(rest, tree), do: {tree, rest}
amedeo

amedeo

:open_mouth: awesome! and beautiful.

Sry, I forgot about the ordering, I have it on the parent_id, nil firsts, but I’ll do a pass of sorting before calling the new build_tree :smiley:

Thanks!

Where Next?

Popular in Questions Top

shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
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
Werner
Hi, I’m using Ubuntu 18.04 and after updating to OTP-24.0 yesterday i have this warning when I run “mix local.hex”: 14:57:30.512 [warn] ...
New
quazar
How to set Jason to encode all fields in ecto schema, I don’t care about security and implementing only is taking long list of attributes...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
Kagamiiiii
Student & New to elixir. Nice language. I want to convert a english character, e.g. “a”, which is stored in a variable, to it’s asci...
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
mathew4509
I have a list say x = ["23gh", "56kh", "97mh"] I would like to pass each element to Val in each iteration. Say, in iteration 1 -------...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New

Other popular topics Top

Qqwy
Update: How to use the Blogs & Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3268 119930 1237
New
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
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 27727 240
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New

We're in Beta

About us Mission Statement