I would like to transform the input
elems = [a, b, c]
terms = [t1, t2]
@spec contains?(term, elem) :: boolean
def contains?(term, elem), do: #implementation
to either of
%{t1 => [a, b], t2 => [c]}
or
%{t1 => [a], t2 => [b, c]}
where
t1 |> contains?(a) #=> true
t1 |> contains?(b) #=> true
t1 |> contains?(c) #=> false
t2 |> contains?(a) #=> false
t2 |> contains?(b) #=> true
t2 |> contains?(c) #=> true
My current solution is as follows
defmodule Test do
def contains?(term, elem) do
elem in term
end
def test do
elems = [1,2,3]
terms = [[1,2], [2,3]]
elems
|> Enum.into(%{}, fn elem ->
{elem,
terms |> Enum.find(&contains?(&1, elem))}
end)
|> reverse_map()
end
defp reverse_map(map, reversed \\ %{})
defp reverse_map(map, reversed) when map_size(map) == 0, do: reversed
defp reverse_map(map, reversed) do
[key | _] = Map.keys(map)
{value, map} = Map.pop!(map, key)
reversed = Map.update(reversed, value, [key], &[key | &1])
reverse_map(map, reversed)
end
end
With this solution I'm generating the map
%{a => t1, b => t1, c => t2}
then reversing it and collecting collisions in a list.
But I feel this intermediate map is unnecessary and a solution could exist without it.
In addition I'm not sure my implementation of reverse_map
is as elegant as it could be.
1 Answer 1
defmodule Test do
def contains?(term, elem) do
elem in term
end
def test do
elems = [1,2,3]
terms = [[1,2], [2,3]]
for e <- elems, reduce: %{} do
acc ->
key = Enum.find(terms, &contains?(&1, e))
Map.update(acc, key, [e], &[e|&1])
end
#=> %{[1, 2] => [2, 1], [2, 3] => [3]}
end
end
Found a simple and elegant solution using a reduce comprehension.
Here, we iterate over each elem
in elems
, find the first term
which contains it, then update our map with either term => [elem]
or term => [elem|previous_elems]
depending on if that key exists in the map yet.
Side note:
If I needed the elems in the same as order they were originally I could use & &1 ++ [e]
instead of &[e|&1]
, but as I don't have that requirement it is more efficient to prepend the list.