Update map's value by its index in a list

I have a list of maps as below and I would like to modify the map based on its index within the list. Hopefully it makes sense. Sample maps below.

Btw Coming from other programming languages where a collection is mutable I find it a bit hard to perform with similar operations in Elixir. Once I “get it” it is then easy… until next problem :wink:

my_list = %{
  data: %{
    items: [
      %{k: "aaa", s: %{}},
      %{k: "bbb", s: %{}},
      %{k: "ccc", s: %{}},
    ]
  }
}

Now I’ve got other collection that I iterate through that contains “index” of specific map that I want to update:

coll = [
  %{"1" => "child of bbb"}
]

So it’s saying “find a 2 map in items” and modify its “s” map to include the “child”, so it then becomes:

my_list = %{
  data: %{
    items: [
      %{k: "aaa", s: %{}},
      %{k: "bbb", s: %{k: "child of bbb"}},
      %{k: "ccc", s: %{}},
    ]
  }
}

Can someone help please?

I looked at various functions like Kernel.update_in/3 or Kernel.put_in/2 but I’m not sure how I get to find the given map (and update it) by the index.

I believe Enum.at is what you’re looking for if you know the index.

coll = [
  %{"1" => "child of bbb"}
]

items = [
      %{k: "aaa", s: %{}},
      %{k: "bbb", s: %{k: "child of bbb"}},
      %{k: "ccc", s: %{}},
    ]

items
|> Enum.with_index()
|> Enum.map(fn {map, idx} ->
  Map.put(map, :s, Map.get(coll, to_string(idx), %{}))
end)
1 Like

Here is my take at it :slight_smile:

items =
  for values <- coll, {index, value} <- values, reduce: my_list.data.items do
    items ->
      item = Enum.at(items, index)
      List.replace_at(items, index, %{item | s: value})
  end

%{data: %{items: items}}

put_in can do it as well:

coll 
# Not sure why this is a list of maps
|> Enum.reduce(&Map.merge/2)
|> Enum.reduce(my_list, fn {index_str, child}, list -> 
  index = String.to_integer(index_str)
  put_in(list, [:data, :items, Access.at(index), :s], %{k: child})
end)
3 Likes

I knew the answer would be already here, so I wrote this, without reading the thread. Turns out I wrote a mix of @cblavier and @LostKobrakai 's solutions :smiley:

for child_by_index <- coll, {index_str, child} <- child_by_index, reduce: my_list do
  my_list ->
    index = String.to_integer(index_str)
    put_in(my_list, [:data, :items, Access.at(index), :s, :k], child)
end

But this feels a bit like an XYProblem. The data is a bit awkward and the question is so abstract we can only answer what you literally asked, but actually the answer is probably that you need to rethink the approach to the problem you’re trying to solve. :slight_smile:

3 Likes

Thank you all! I chose @LostKobrakai’s for elegant (and working) code.

Yes, I’m rubbish at explaining things. Also, looks like I overthought it ??

I’m building a grid that displays some data. The user can configure columns on that grid, and some columns can be stacked against each other (displayed one underneath another). To keep it simple each column can have max 1 child, and the data on my HTML form looks like so:

Column A
Column B
- Subcolumn BB
Column C
Column D
- Subcolumn DD

Max 1 child per column. There could be no children at all.

This data is saved as JSONB in psql, so before I save it to database I try to build the map in Elixir and pass that to Ecto.

But maybe the fields of HTML form could be named/structured different so it’s easier to handle this in Elixir ??

So far I have inputs with “columns[]” names for (parent) columns, so this becomes a list of maps by doing Enum.map(fn x -> %{k: x, s: %{}} end)

Then the POST request also contains inputs with children[x] names (where x indicates index position of parent), hence this question for merging child into a corresponding parent column.

How does one serialise (nested) form inputs in simpler way then?

===
edit:

given html form with “nested” fields:

<input type="text" value="Column A">
<input type="text" value="Column B">
<input type="text" value="SubColumn BB">
<input type="text" value="Column C">

produce a Elixir map:

my_list: [
  %{k: "Column A", s: %{}},
  %{k: "Column B", s: %{k: "SubColumn BB"}},
  %{k: "Column C", s: %{}},
]

I guess I could name the html fields somehow differently and handle the “serialise” in Javascript to produce nice nested JSON and send to server… but I don’t wanna touch JS at all (btw thanks for LiveView!)

Anyhow, I changed the fields slightly to not rely on “indexes” but on corresponding (parent) names, e.g.

<input name="col[]" value="aaa" .../>
<input name="col[]" value="bbb" .../>
<input name="child[aaa]" value="child of aaa" .../>

then the “col” in request is a list that I convert to my data structure so it becomes:

my_list = %{
  data: %{
    items: [
      %{k: "aaa", s: %{}},
      %{k: "bbb", s: %{}},
      %{k: "ccc", s: %{}},
      %{k: "ddd", s: %{}},
    ]
  }
}

the child comes in as a map:

the_kids = %{"bbb" => "child of bbb", "ddd" => "child of ddd"}

then to assign children to their parents I came up with this “crazy” code:

{_old_list, my_list} = get_and_update_in(my_list, [:data, :items, Access.all()], fn prev ->
  {prev, %{k: prev.k, s: (if Map.has_key?(the_kids, prev.k), do: %{k: Map.get(the_kids, prev.k)}, else: %{})}}
 end)

which then produces:

%{
  data: %{
    items: [
      %{k: "aaa", s: %{}},
      %{k: "bbb", s: %{k: "child of bbb"}},
      %{k: "ccc", s: %{}},
      %{k: "ddd", s: %{k: "child of ddd"}}
    ]
  }
}

It ain’t probably very efficient code and ugly, but works for what I need it for. I don’t expect my users to change the grid columns, and even if they do there’s like max 10 fields to run through.