Merging each item in a map with each item in a list

I have a list of structs from the return of Repo.preload. And then I have a map that has a list with items in it.

structs = [%Struct{...}, %Struct{...}]

{:ok, states} = %{states: [ %{ "pos_x" => 1.0, "pos_y" => 2.0, "pos_z" => 1.022, "vel_x" => 2.01, "vel_y" => 12.0, "vel_z" => 129.0 }, %{ "pos_x" => 10000.0, "pos_y" => -2.0, "pos_z" => -1.022, "vel_x" => -2.01, "vel_y" => -12.0, "vel_z" => -129.0 } ]}

My goal is to be able to take item one from the states map and put it in the first struct. And then take item two from the states map and put it in the second struct. And so on and so on. The amount of structs will always match the amount of states.

I have tried list = Enum.map(structs, fn s -> Enum.map(states, fn x -> Map.merge(s, x) end) end) but obviously this duplicates the structs list that is returned.

I have also tried making states a list of maps instead, therefore having two lists, states, and structs. And then doing a simple Enum.zip(structs, states). This returns me a list of tuples, which cannot be used in the Ecto schema.

Any help/guidance on this?

Welcome!

Alongside Enum.zip/2, there’s an Enum.zip_with/2 function that might be what you’re looking for.

# tested with a list of maps, but should work with structs as well
iex(1)> structs = [%{}, %{}]
[%{}, %{}]
iex(2)> states = [%{ "pos_x" => 1.0, "pos_y" => 2.0, "pos_z" => 1.022, "vel_x" => 2.01, "vel_y" => 12.0, "vel_z" => 129.0 }, %{ "pos_x" => 10000.0, "pos_y" => -2.0, "pos_z" => -1.022, "vel_x" => -2.01, "vel_y" => -12.0, "vel_z" => -129.0 }]
[
  %{
    "pos_x" => 1.0,
    "pos_y" => 2.0,
    "pos_z" => 1.022,
    "vel_x" => 2.01,
    "vel_y" => 12.0,
    "vel_z" => 129.0
  },
  %{
    "pos_x" => 10000.0,
    "pos_y" => -2.0,
    "pos_z" => -1.022,
    "vel_x" => -2.01,
    "vel_y" => -12.0,
    "vel_z" => -129.0
  }
]
iex(3)> Enum.zip_with([structs, states], fn [struct, state] -> Map.put(struct, :state, state) end)
[
  %{
    state: %{
      "pos_x" => 1.0,
      "pos_y" => 2.0,
      "pos_z" => 1.022,
      "vel_x" => 2.01,
      "vel_y" => 12.0,
      "vel_z" => 129.0
    }
  },
  %{
    state: %{
      "pos_x" => 10000.0,
      "pos_y" => -2.0,
      "pos_z" => -1.022,
      "vel_x" => -2.01,
      "vel_y" => -12.0,
      "vel_z" => -129.0
    }
  }
]
4 Likes

You could do something like this:

iex(1)> structs = [%{"x" => 1}, %{"x" => 2}]
iex(2)> states = %{states: [%{"a" => 1},%{"a" => 2}]}
iex(3)> defmodule Merger do
...(3)>   def merge(xs, ys, acc \\ [])
...(3)>   def merge([], [], acc), do: acc
...(3)>   def merge([x|xs], [y|ys], acc), do: merge(xs,ys,[Map.merge(x,y)|acc])
...(3)> end
iex(4)> Merger.merge(structs, states.states)
[%{"a" => 2, "x" => 2}, %{"a" => 1, "x" => 1}]
2 Likes

I’m a little unclear what “put it into the second struct” specifically means, so for the purposes of this reply I’m assuming it means “there are pos_x / pos_y / pos_z etc fields in %Struct{} that should be filled based on the input”.

Enum.zip_with is handy here:

Enum.zip_with([structs, states], fn [struct, state] ->
  %{struct | pos_x: state["pos_x"], etc etc etc }
end)

Using the update syntax (%{ | }) ensures that this only sets fields that %Struct{} knows about.

2 Likes

I think this is confusing because maps do not maintain order, so talking about item one or item two from a map is not reliable. From the code example the map has a key states with a corresponding value that is a list of maps. If the order of that list matches the order of the list of structs then the Enum.zip_with solution should work. I would suggest you could use Enum.zip_with/3 instead of Enum.zip_with/2 since you are starting with two separate collections. The zip_with functions are the same as your attempt using Enum.zip to get a list of tuples but then passing that list to Enum.map to apply the transformation step. The zip_with functions are a little more efficient because they avoid actually building that intermediate list of tuples.

1 Like

Thank you so much for showing me Enum.zip_with/2. The only thing I need to change to get what I was looking for was since states was a map with a list of maps, I need to do Enum.zip_with([structs, Map.get(states, :states)], ... so that I could do the zipping on two lists instead.

It’s helpful if you show the final code that worked for you + mark it as the chosen solution. Helps future readers.

2 Likes