如何在 Elixir 中将映射键从字符串转换为原子

问题描述 投票:0回答:16

在 Elixir 中如何将

%{"foo" => "bar"}
转换为
%{foo: "bar"}

elixir
16个回答
106
投票

使用推导式

iex(1)> string_key_map = %{"foo" => "bar", "hello" => "world"}
%{"foo" => "bar", "hello" => "world"}

iex(2)> for {key, val} <- string_key_map, into: %{}, do: {String.to_atom(key), val}
%{foo: "bar", hello: "world"}

71
投票

我认为最简单的方法是使用

Map.new
:

%{"a" => 1, "b" => 2}
|> Map.new(fn {k, v} -> {String.to_atom(k), v} end)

%{a: 1, b: 2}

25
投票

您可以组合使用 Enum.reduce/3String.to_atom/1

%{"foo" => "bar"}
|> Enum.reduce(%{}, fn {key, val}, acc -> Map.put(acc, String.to_atom(key), val) end)

%{foo: "bar"}

但是,您应该警惕根据用户输入转换为原子,因为它们不会被垃圾收集,这可能导致内存泄漏。请参阅此问题

如果原子已经存在,您可以使用 String.to_existing_atom/1 来防止这种情况。


14
投票

下面的代码片段将嵌套的类似 json 的映射的键转换为现有原子:

iex(2)> keys_to_atoms(%{"a" => %{"b" => [%{"c" => "d"}]}})

%{a: %{b: [%{c: "d"}]}}
  def keys_to_atoms(json) when is_map(json) do
    Map.new(json, &reduce_keys_to_atoms/1)
  end

  def reduce_keys_to_atoms({key, val}) when is_map(val), do: {String.to_existing_atom(key), keys_to_atoms(val)}
  def reduce_keys_to_atoms({key, val}) when is_list(val), do: {String.to_existing_atom(key), Enum.map(val, &keys_to_atoms(&1))}
  def reduce_keys_to_atoms({key, val}), do: {String.to_existing_atom(key), val}

9
投票

要以@emaillenin的答案为基础,您可以检查键是否已经是原子,以避免当 String.to_atom 获取已经是原子的键时引发

ArgumentError

for {key, val} <- string_key_map, into: %{} do
  cond do
    is_atom(key) -> {key, val}
    true -> {String.to_atom(key), val}
  end
end

7
投票

您可以使用 Jason 库。

迈克尔穆斯卡拉/杰森

%{
  "key" => "1",
  "array" => [%{"key" => "1"}],
  "inner_map" => %{"another_inner_map" => %{"key" => 100}}
}
|> Jason.encode!()
|> Jason.decode!(keys: :atoms)

%{array: [%{key: "1"}], inner_map: %{another_inner_map: %{key: 100}}, key: "1"}

5
投票

有一个用于此目的的库,https://hex.pm/packages/morphix。它还具有嵌入式密钥的递归功能。

大部分工作都是在这个函数中完成的:

defp atomog(map) do
  atomkeys = fn {k, v}, acc ->
    Map.put_new(acc, atomize_binary(k), v)
  end

  Enum.reduce(map, %{}, atomkeys)
end

defp atomize_binary(value) do
  if is_binary(value), do: String.to_atom(value), else: value
end

这就是递归调用。阅读@Galzer的答案后,我可能很快就会将其转换为使用

String.to_existing_atom


4
投票

首先,@Olshansk 的回答对我来说就像一个魅力。谢谢你。

接下来,由于@Olshansk提供的初始实现缺乏对地图列表的支持,下面是我扩展它的代码片段。

def keys_to_atoms(string_key_map) when is_map(string_key_map) do
  for {key, val} <- string_key_map, into: %{}, do: {String.to_atom(key), keys_to_atoms(val)}
end

def keys_to_atoms(string_key_list) when is_list(string_key_list) do
  string_key_list
  |> Enum.map(&keys_to_atoms/1)
end

def keys_to_atoms(value), do: value

这是我使用的示例,然后是传递给上述函数后的输出 -

keys_to_atoms(attrs)

# Input
%{
  "school" => "School of Athens",
  "students" => [
    %{
      "name" => "Plato",
      "subjects" => [%{"name" => "Politics"}, %{"name" => "Virtues"}]
    },
    %{
      "name" => "Aristotle",
      "subjects" => [%{"name" => "Virtues"}, %{"name" => "Metaphysics"}]
    }
  ]
}

# Output
%{
  school: "School of Athens",
  students: [
    %{name: "Plato", subjects: [%{name: "Politics"}, %{name: "Virtues"}]},
    %{name: "Aristotle", subjects: [%{name: "Virtues"}, %{name: "Metaphysics"}]}
  ]
}

对此的解释非常简单。第一个方法是为类型映射的输入调用的所有方法的核心。 for 循环解构键值对中的属性并返回键的原子表示形式。 接下来在返回值的同时,又出现了三种可能。

  1. 价值又是一张地图。
  2. 该值是地图列表。
  3. 该值不是以上任何一个,它是原始值。

所以这一次,当在赋值时调用

keys_to_atoms
方法时,它可能会根据输入的类型调用这三个方法之一。 这些方法以类似的顺序组织在代码片段中。

希望这有帮助。干杯!


4
投票

这是@emaillenin 的模块形式答案的一个版本:

defmodule App.Utils do
  # Implementation based on: http://stackoverflow.com/a/31990445/175830
  def map_keys_to_atoms(map) do
    for {key, val} <- map, into: %{}, do: {String.to_atom(key), val}
  end

  def map_keys_to_strings(map) do
    for {key, val} <- map, into: %{}, do: {Atom.to_string(key), val}
  end
end

2
投票
defmodule Service.MiscScripts do
  @doc """
  Changes String Map to Map of Atoms e.g. %{"c"=> "d", "x" => %{"yy" => "zz"}} to
          %{c: "d", x: %{yy: "zz"}}, i.e changes even the nested maps.
  """

  def convert_to_atom_map(map), do: to_atom_map(map)

  defp to_atom_map(map) when is_map(map),
    do: Map.new(map, fn {k, v} -> {String.to_atom(k), to_atom_map(v)} end)

  defp to_atom_map(v), do: v
end

1
投票
m = %{"key" => "value", "another_key" => "another_value"}
k = Map.keys(m) |> Enum.map(&String.to_atom(&1))
v = Map.values(m)
result = Enum.zip(k, v) |> Enum.into(%{})

1
投票

我真的很喜欢 Roman Bedichevskii 的答案……但我需要一些能够彻底原子化深层嵌套 yaml 文件的键的东西。这就是我想到的:

@doc """
Safe version, will only atomize to an existing key
"""
def atomize_keys(map) when is_map(map), do: Map.new(map, &atomize_keys/1)
def atomize_keys(list) when is_list(list), do: Enum.map(list, &atomize_keys/1)

def atomize_keys({key, val}) when is_binary(key),
  do: atomize_keys({String.to_existing_atom(key), val})

def atomize_keys({key, val}), do: {key, atomize_keys(val)}
def atomize_keys(term), do: term

@doc """
Unsafe version, will atomize all string keys
"""
def unsafe_atomize_keys(map) when is_map(map), do: Map.new(map, &unsafe_atomize_keys/1)
def unsafe_atomize_keys(list) when is_list(list), do: Enum.map(list, &unsafe_atomize_keys/1)

def unsafe_atomize_keys({key, val}) when is_binary(key),
  do: unsafe_atomize_keys({String.to_atom(key), val})

def unsafe_atomize_keys({key, val}), do: {key, unsafe_atomize_keys(val)}
def unsafe_atomize_keys(term), do: term

它的主要限制是,如果你向它提供一个元组 {key, value} 并且键是一个二进制文件,它会将其原子化。这是您想要的关键字列表,但这可能是某人的边缘情况。无论如何,YAML 和 JSON 文件没有元组的概念,因此对于处理它们来说,这并不重要。


1
投票

这是我用来递归地(1)将映射键格式化为蛇形格式并(2)将它们转换为原子的方法。请记住,您应该永远不将非白名单用户数据转换为原子,因为它们不会被垃圾收集。

defp snake_case_map(map) when is_map(map) do
  Enum.reduce(map, %{}, fn {key, value}, result ->
    Map.put(result, String.to_atom(Macro.underscore(key)), snake_case_map(value))
  end)
end

defp snake_case_map(list) when is_list(list), do: Enum.map(list, &snake_case_map/1)
defp snake_case_map(value), do: value

1
投票

我喜欢使用 Enum.into/3,这样我就可以轻松地在地图、关键字或任何其他 Collectable

之间进行选择
%{"foo" => "bar"}
|> Enum.into(Map.new(), fn {k, v} -> {String.to_atom(k), v} end)

%{foo: "bar"}
%{"foo" => "bar"}
|> Enum.into(Keyword.new(), fn {k, v} -> {String.to_atom(k), v} end)

[foo: "bar"]

0
投票

当您的地图位于另一张地图中时

def keys_to_atom(map) do
  Map.new(
    map,
    fn {k, v} ->
      v2 =
        cond do
          is_map(v) -> keys_to_atom(v)
          v in [[nil], nil] -> nil
          is_list(v) -> Enum.map(v, fn o -> keys_to_atom(o) end)
          true -> v
        end

      {String.to_atom("#{k}"), v2}
    end
  )
end

样本:

my_map = %{"a" => "1", "b" => [%{"b1" => "1"}], "c" => %{"d" => "4"}}

结果

%{a: "1", b: [%{b1: "1"}], c: %{d: "4"}}

注意: 当您有 "b" => [1,2,3] 时,is_list 将失败,因此如果是这种情况,您可以注释/删除此行:

# is_list(v) -> Enum.map(v, fn o -> keys_to_atom(o) end)

0
投票

我们发现自己在各种

Elixir/Phoenix
项目中经常这样做......
所以我们创建了一个经过测试+记录的实用函数
Useful.atomize_map_keys/1

它考虑了该线程中的所有答案。

通过将此行添加到

deps
你的 `mix.exs:

来安装
{:useful, "~> 1.0.8"},

然后使用:

my_map = %{"name" => "Alex", "age": 17}
Useful.atomize_map_keys(my_map)
%{name: "Alex", age: 17}

我们在阅读代码时发现这一点更清晰,并且允许使用管道,例如:

MyApp.fetch_json_data()
|> Jason.decode()
|> Useful.atomize_map_keys()
© www.soinside.com 2019 - 2024. All rights reserved.