Filter list of maps

Hello!
I’m trying to filter list of maps to remove items where value of key was declared previously in this list. List that I have:

list = [%{
    "distance" => "4",
    "source" => "source_1",
},
%{
    "distance" => "2",
    "source" => "source_1",
},
%{
    "distance" => "5",
    "source" => "source_2",
},
%{
    "distance" => "1",
    "source" => "source_2",
},
%{
    "distance" => "2",
    "source" => "source_2",
}]

List what I want to get:

[%{
    "distance" => "4",
    "source" => "source_1",
},
%{
    "distance" => "5",
    "source" => "source_2",
}]

I tried next code:

sources = []
list
|> Enum.filter(fn p ->
    if p["source"] in sources do
        false
    else
        sources ++ p["source"]
        true
    end
end) |> IO.inspect

But looks like list “sources” not accessed from Enum.filter. Probably there exist more correct solution for this?

There’s Enum.uniq_by/2 that you can use, as in Enum.uniq_by(list, fn(%{"source" => s}) -> s end)

The reason your filter doesn’t work is because you’re building the list as you enumerate the elements and you can’t mutate the variable sources. You can achieve what you want with a reduction, where you keep track of them as you exemplified:

{_, new_list} = Enum.reduce(list, {[], []}, fn(%{"source" => s} = el, {sources, acc}) ->
                               if(s in sources, do: {sources, acc}, else: {[s | sources], [el | acc]}) 
                end)

final = :lists.reverse(new_list)

https://hexdocs.pm/elixir/Enum.html#uniq_by/2

3 Likes

Here is my one line ugly try :slight_smile:

iex> list |> Enum.reduce(%{}, fn %{"source" => s} = x, acc -> Map.put_new(acc, s, x) end) |> Enum.map(fn {_k, v} -> v end)        
[
  %{"distance" => "4", "source" => "source_1"},
  %{"distance" => "5", "source" => "source_2"}
]

I use Map.put_new to ensure only first source is persisted

3 Likes

For note, your sources ++ p["source"] expression is evaluated here but the result is thrown away since it’s not being returned.

And it looks like you aren’t just wanting a unique one based on the name, but also the one with the highest value in each set, thus I’d probably do either this:

Enum.group_by(list, & &1["source"]) |> Enum.map(fn {_, v} -> Enum.max_by(v, & &1["distance"]) end)

Or this:

Enum.sort_by(list, & {&1["source"], &1["distance"]}, &>/2) |> Enum.dedup_by(& &1["source"])

Or any of a variety of other ways. :slight_smile:

3 Likes
  def single(list) do
   Enum.uniq_by(list, &(%{"source" => &1.source}))
  end
2 Likes