如何使用另一个地图列表中的值更新地图列表?

How do I update a list of maps with the values from another list of maps?

我有这样的地图列表:

primary_list = [
  %{"id" => "cue", "is_idle" => true, "velocity_x" => 0.0, "velocity_z" => 0.0, "x" => -15.0, "z" => 0.0},
  %{"id" => "ball_1", "is_idle" => true, "velocity_x" => 0.0, "velocity_z" => 0.0, "x" => 15.0, "z" => 0.0},
  %{"id" => "ball_2", "is_idle" => true, "velocity_x" => 0.0, "velocity_z" => 0.0, "x" => 17.0, "z" => 1.1},
  %{"id" => "ball_3", "is_idle" => true, "velocity_x" => 0.0, "velocity_z" => 0.0, "x" => 17.0, "z" => -1.1}
]

还有一个,像这样:

new_list = [
    %{"ball_1" => %{x: 14.684729230724962, y: 0.0, z: 0.0}, 
    "cue" => %{x: 16.68472923072496, y: 0.0, z: 0.0}}, 
    %{"ball_2" => %{x: 17.68472923072496, y: 0.0, z: 1.1}, 
    "cue" => %{x: 15.684729230724962, y: 0.0, z: 1.1}}, 
    %{"ball_3" => %{x: 17.68472923072496, y: 0.0, z: -1.1}, 
    "cue" => %{x: 15.684729230724962, y: 0.0, z: -1.1}}, 
    %{"ball_2" => %{x: 17.68472923072496, y: 0.0, z: 0.0}, 
    "cue" => %{x: 15.684729230724962, y: 0.0, z: 0.0}}, 
    %{"ball_3" => %{x: 17.68472923072496, y: 0.0, z: 0.0}, 
    "cue" => %{x: 15.684729230724962, y: 0.0, z: 0.0}}
]

如何迭代第二张地图,并根据 ID 将对 x、y 和 z 值的更改应用到第一张地图,同时保持第一张地图的格式以使最终结果看起来像像这样:

new_primary_list = [
    %{"id" => "cue", "is_idle" => true, "velocity_x" => 0.0, "velocity_z" => 0.0, "x" => 15.684729230724962, "z" => 0.0},
    %{"id" => "ball_1", "is_idle" => true, "velocity_x" => 0.0, "velocity_z" => 0.0, "x" => 14.684729230724962, "z" => 0.0},
    %{"id" => "ball_2", "is_idle" => true, "velocity_x" => 0.0, "velocity_z" => 0.0, "x" => 17.68472923072496, "z" => 0.0},
    %{"id" => "ball_3", "is_idle" => true, "velocity_x" => 0.0, "velocity_z" => 0.0, "x" => 17.68472923072496, "z" => 0.0}
]

有些项目是重复的,但最后的值应该是最后更新的(比如ball_2ball_3的情况)。我试过这个:

new_result = Enum.map(result, fn  res ->
    Enum.map(collision_map, fn cmap -> 
        Enum.map(cmap, fn {k, v} ->
            if res["id"] == k do
                res
                |> Map.put("x", v["x"])
                |> Map.put("z", v["z"])
            end
        end)
    end)
end)

但我最终得到的只是一堆乱七八糟的列表,其中包含很多 nil 个值:

[
    [nil, nil],  [nil, nil], 
    [%{"id" => "ball_3", "is_idle" => true, "velocity_x" => 0.0, "velocity_z" => 0.0, "x" => nil, "z" => nil}, nil], 
    [nil, nil], 
    [%{"id" => "ball_3", "is_idle" => true, "velocity_x" => 0.0, "velocity_z" => 0.0, "x" => nil, "z" => nil}, nil]
]

还有什么我可以尝试的吗?

展平 new_list 使其易于迭代,然后逐步更新 primary_list

请注意,这是非常低效的,您最好重新考虑使用 Maps(例如,使用 id 作为键。)

new_list
|> Enum.flat_map(& &1)
|> Enum.reduce(primary_list, fn {id, values}, acc ->
  update_in(
    acc,
    [Access.filter(& &1["id"] == id)],
    &Map.merge(&1, values)
  )
end)

您可能需要将 new_list 的键转换为字符串或使其工作。


结果:

我更新了一点 Aleksei Matiushkin 的回答:

primary_list = Map.new(primary_list, & {&1["id"], &1}) # convert primary_list into a map

new_list 
|> Enum.flat_map(& &1) 
|> Enum.reduce(primary_list, fn {k, v}, acc -> # iterate over new_list and pass primary_list as accumulator
   Map.update!(
     acc, 
     k, 
     & Map.merge(&1, %{"x" => v.x, "y" => v.y, "z" => v.z}) # update primary_list  values with new_list
  )

 end)
|> Map.values() # covert result back into a list

正如我在您的 中提到的,列表不适合随机访问。因为您想通过 ID 更新特定元素,所以最好将您的状态存储在地图中,因为地图就是为此而设计的。

如果您清理数据以使其更易于使用,则实施会非常简单:

def update_state do
  {primary_map, new_list} = sanitize()

  Enum.reduce(new_list, primary_map, fn %{ball: ball, cue: cue}, primary_map ->
    primary_map = put_in(primary_map["cue"]["x"], cue.x)
    primary_map = put_in(primary_map["cue"]["y"], cue.y)
    primary_map = put_in(primary_map["cue"]["z"], cue.z)
    primary_map = put_in(primary_map[ball.id]["x"], ball.pos.x)
    primary_map = put_in(primary_map[ball.id]["y"], ball.pos.y)
    put_in(primary_map[ball.id]["z"], ball.pos.z)
  end)
  |> Map.values()
end

输出:

[
  %{
    "id" => "ball_1",
    "is_idle" => true,
    "velocity_x" => 0.0,
    "velocity_z" => 0.0,
    "x" => 14.684729230724962,
    "y" => 0.0,
    "z" => 0.0
  },
  %{
    "id" => "ball_2",
    "is_idle" => true,
    "velocity_x" => 0.0,
    "velocity_z" => 0.0,
    "x" => 17.68472923072496,
    "y" => 0.0,
    "z" => 1.1
  },
  %{
    "id" => "ball_3",
    "is_idle" => true,
    "velocity_x" => 0.0,
    "velocity_z" => 0.0,
    "x" => 17.68472923072496,
    "y" => 0.0,
    "z" => -1.1
  },
  %{
    "id" => "cue",
    "is_idle" => true,
    "velocity_x" => 0.0,
    "velocity_z" => 0.0,
    "x" => 15.684729230724962,
    "y" => 0.0,
    "z" => 0.0
  }
]

sanitize我做了两件事:

  1. 将状态转换为地图。
Map.new(primary_list, fn item -> {item["id"], item} end)

这给了我们:

%{
  "ball_1" => %{
    "id" => "ball_1",
    "is_idle" => true,
    "velocity_x" => 0.0,
    "velocity_z" => 0.0,
    "x" => 15.0,
    "z" => 0.0
  }, ...
}

这更容易使用,允许我们使用 put_in 和上面的球 ID。

  1. new_list 转换为具有一致的密钥。您的球键是 ball_1ball_2 等,但这使得很难从循环中以编程方式访问它们。拥有一致的 ID 会更好:ball.
for item <- new_list do
  [id] = Map.keys(item) -- ["cue"]

  %{
    ball: %{id: id, pos: item[id]},
    cue: item["cue"]
  }
end

这为我们提供了这种格式的 new_list,使我们能够为每个项目查找 ball

[
   %{
     ball: %{id: "ball_1", pos: %{x: 14.684729230724962, y: 0.0, z: 0.0}},
     cue: %{x: 16.68472923072496, y: 0.0, z: 0.0}
   }, ...
]

这是完整的 sanitize 函数:

def sanitize do
  primary_list = primary_list()
  primary_map = Map.new(primary_list, fn item -> {item["id"], item} end)

  new_list = new_list()

  new_list =
    for item <- new_list do
      [id] = Map.keys(item) -- ["cue"]

      %{
        ball: %{id: id, pos: item[id]},
        cue: item["cue"]
      }
    end

  {primary_map, new_list}
end