Transform list into nested map

Hi,

I have a list of key value maps like so:

[
%{k: "vacancy.state.draft", v: "concept"},
%{k: "vacancy.state.expired", v: "gone"},
%{k: "vacancy.state.online", v: "ready"},
%{k: "vacancy.name", v: "some name"},
%{k: "vacancy.title", v: "jadada"}
]

Now I’d like transform this into a map, that should look like this:

%{
  vacancy: %{
    state: %{
      draft: "concept",
      expired: "gone",
      online: "ready"
    },
    name: "some name",
    title: "jadada"
  }  
}

I can use String.split to get an array of keys, but looking them up and creating them when not present in the result map is a bit hard for me to wrap my head around.

I ttried using http://elixir-lang.org/docs/stable/elixir/Kernel.html#put_in/3 but I;m stuck, being very new to Elixir and all

1 Like

Here’s my take with plain recursion:

defmodule Converter do
  def to_map(input) do
    Enum.reduce(input, %{},
      fn(%{k: key, v: value}, intermediate_map) ->
        merge(intermediate_map, String.split(key, "."), value)
      end
    )
  end

  defp merge(map, [leaf], value), do: Map.put(map, leaf, value)
  defp merge(map, [node | remaining_keys], value) do
    inner_map = merge(Map.get(map, node, %{}), remaining_keys, value)
    Map.put(map, node, inner_map)
  end
end

Testing it:

[
  %{k: "vacancy.state.draft", v: "concept"},
  %{k: "vacancy.state.expired", v: "gone"},
  %{k: "vacancy.state.online", v: "ready"},
  %{k: "vacancy.name", v: "some name"},
  %{k: "vacancy.title", v: "jadada"}
]
|> Converter.to_map
|> IO.inspect

produces

%{"vacancy" => %{"name" => "some name",
    "state" => %{"draft" => "concept", "expired" => "gone",
      "online" => "ready"}, "title" => "jadada"}}

Perhaps it can be done with update_in and friends, but I didn’t try it :slight_smile:

7 Likes

This works, thanks Saša!

So Elixir’s pattern matching can enforce a list with a certain number of elements? 1 in this case?

That’s very cool :slight_smile:

1 Like

Yeah, that matches a one-element list. Here are some list patterns I use frequently:

[]     # empty list
[_]    # one-element list
[_|_]  # non-empty list
2 Likes

There is also some sugar for lists of a minimum length: [_, _, _|_]. A list matching this has at least 3 elements. It is equivalent to matching on [_|[_|[_|_]]] but quite a lot more readable.

3 Likes

Here’s a solution that uses Access.key/2 and put_in/3 inside a single Enum.reduce/3:

def to_nested_map(list) do
  Enum.reduce list, %{}, fn %{k: key, v: value}, acc ->
    key_path = key |> String.split(".") |> Enum.map(&Access.key(&1, %{}))
    put_in(acc, key_path, value)
  end
end

The trick to making it work is to use Access.key/2 for each element of the key path, passing %{} as the 2nd arg to Access.key/2 so it is used as the default.

5 Likes