Filter a list of map by a key inside a list of those maps

Hello there! Elixir-newie question here: :slight_smile:
I would like to filter a list of plans by the category name inside it:

My list of plans:

plans = [
 %{name: "All In", categories: [%{name: "basketball"}, %{name: "football"}]},
 %{name: "Gold Plan", categories: [%{name: "tennis"}, %{name: "football"}]},
 %{name: "Abc Plan", categories: [%{name: "yoga"}, %{name: "fitness"}]},
 %{name: "123 Plan", categories: [%{name: "basketball"}]}
]

And now I would like to get only the plans that have a category inside this list
selected_categories = [“basketball”]
** The list can have 1 or more categories inside, for example selected_categories = [“basketball”, “yoga”]

I did this:

filter = Enum.filter(plans, fn p -> Enum.filter(p.categories, fn c -> 
   c.name in selected_categories end) 
end)

and

filter = Enum.filter(plans, fn p -> for category <- p.categories do 
  category.name in selected_categories end
end)

But for both cases, I got the same list of plans instead of the plans “123 Plan” && “All in” .

Thanks in advance! :slight_smile:

1 Like

given:

selected_categories = ["basketball"]

Enum.filter(plans, fn plan -> 
  Enum.any?(plan.categories, &(&1.name in selected_categories))
end)

returns:

[
  %{categories: [%{name: "basketball"}, %{name: "football"}], name: "All In"},
  %{categories: [%{name: "basketball"}], name: "123 Plan"}
]
3 Likes
Enum.filter(plans, fn p ->
  cats = Enum.map(p.categories, &(&1.name))
  Enum.any?(cats, &(&1 in selected_categories))
)

But if selected_categories is going to be more than one or two items, you should probably make it a MapSet instead of a list so you can check for membership without traversing the whole list.

selected_categories = MapSet.new(selected_categories)

Enum.filter(plans, fn p ->
  cats = Enum.map(p.categories, &(&1.name))
  Enum.any?(cats, &MapSet.member?(selected_categories, &1))
)

EDIT: The solution by @tomkonidas offers an additional improvement by only iterating each plans.categories once.

EDIT 2: Alternatively, replace Enum.map with Stream.map

1 Like

If possible, making the categories from plans a MapSet, would allow to simply check the size of the intersection of the two sets, leading to simpler code.

https://hexdocs.pm/elixir/1.12/MapSet.html#intersection/2

1 Like
selected_categories = MapSet.new(selected_categories)

Enum.filter(plans, fn p ->
  cats = MapSet.new(p.categories, &(&1.name))
  i = MapSet.intersection(cats, selected_categories)
  MapSet.size(i) > 0
)

Certainly possible, but this doesn’t seem any simpler than @tomkonidas’ solution, and it still adds the additional list traversal that my solution had - with MapSet.new instead of Enum.map.

EDIT: Maybe you could transform selected_categories to match the format of the cats, instead of the other way around - since you only have to do it once:

selected_categories = MapSet.new(selected_categories, fn c -> %{name: c} end)

Enum.filter(plans, fn p ->
  i = MapSet.intersection(cats, selected_categories)
  MapSet.size(i) > 0
)

2 Likes

Yes, your edit is what I meant.

2 Likes

Thank you, it really helped :slight_smile: !