简体   繁体   中英

How do I transform and group a set of maps in elixir?

Given this data structure:

[
  %{ collection: [ %{ description: "Foo", score: 4 }, %{ description: "Bar", score: 5 } ] },
  %{ collection: [ %{ description: "Baz", score: 3 }, %{ description: "Foo", score: 4 } ] },
  %{ collection: [ %{ description: "Bar", score: 1 }, %{ description: "Baz", score: 1 } ] }
]

I would like to create a set of new maps for each unique description with a summation of the corresponding scores. Like this:

[
  %{ description: "Foo", score: 8 },
  %{ description: "Bar", score: 6 },
  %{ description: "Baz", score: 4 }
]

One could break this down to the following:

  1. Iterate through initial dataset
  2. Build a set of maps for each unique description
  3. Reduce set with a summation function
  4. Build set of maps with new results for each group.

I imagine this process will involve a combination of reduce and map , but what I'm struggling with is the initial grouping that is necessary to run a summation function. An idiomatic elixir solution would be great. Thanks!

I would use Enum.flat_map/2 and Enum.group_by/2 :

list = [
  %{ collection: [ %{ description: "Foo", score: 4 }, %{ description: "Bar", score: 5 } ] },
  %{ collection: [ %{ description: "Baz", score: 3 }, %{ description: "Foo", score: 4 } ] },
  %{ collection: [ %{ description: "Bar", score: 1 }, %{ description: "Baz", score: 1 } ] }
]

list
|> Enum.flat_map(fn x -> x.collection end)
|> Enum.group_by(fn x -> x.description end)
|> Enum.map(fn {key, value} ->
    %{description: key, score: value |> Enum.map(fn x -> x.score end) |> Enum.sum}
end)
|> IO.inspect

Output:

[%{description: "Bar", score: 6}, %{description: "Baz", score: 4},
 %{description: "Foo", score: 8}]

Note that since group_by uses a Map under the hood, this does not preserve the order of items in the resulting list.

How about this:

defmodule Group do

  @groups [
    %{ collection: [ %{ description: "Foo", score: 4 }, %{ description: "Bar", score: 5 } ] },
    %{ collection: [ %{ description: "Baz", score: 3 }, %{ description: "Foo", score: 4 } ] },
    %{ collection: [ %{ description: "Bar", score: 1 }, %{ description: "Baz", score: 1 } ] }
  ]

  def transform(groups \\ @groups) do
    groups 
    |> Enum.reduce([], fn group, list -> list ++ group[:collection] end)
    |> Enum.group_by(&(&1.description), &(&1.score))
    |> Enum.map(fn {key, v} -> {key, Enum.sum(v)} end)
    |> Enum.into(%{})
  end
end

iex(24)> Group.transform
%{"Bar" => 6, "Baz" => 4, "Foo" => 8}

Another way:

Enum.reduce(collections, %{}, fn(%{collection: list}, acc) -> 
  Map.merge(acc, Enum.reduce(list, %{}, fn(map, acc2) -> Map.put(acc2, map[:description], map) end), fn(_k, v1, v2) -> 
    Map.put(v1, :score, v1[:score] + v2[:score]) 
  end) 
end) 
|> Map.values() 

The last line |> Map.values() is optional to me. I'd remove this line so that you have a unique key and a map which seems better to me.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM