Comparing, Combining, and Deduping Lists of Strings within a List

Still fairly new to Elixir and am coming from OOP, so bear with me here…

I have a list of lists of strings, each with a length of 2, ie:

list = [ ["Tony", "Glove"] , ["Mark", "Shirt"], ["Tony", "Hat"] ]

and I want to combine and dedup the lists that have the same value in index[0], so the list above would turn into:

convert_list(list) = [ ["Tony", "Glove", "Hat"], ["Mark", "Shirt"] ]

My first attempt was turning each list into a MapSet, then using:
if MapSet.member do _nothing else MapSet.union(Enum.at(list, x),Enum.at(list,y)

within a recursive for x <- 0..Enum.count(list) function, but I am pretty much in over my head on this one.

Any help/guidance would be greatly appreciated!

Whenever I need to check for key unicity, I would use a map. I would also use Enum.reduce… with some translation due to atom key. The last command line transform the map back to a list.

iex> list 
|> Enum.reduce(%{}, fn [k, v], acc -> Map.update(acc, String.to_atom(k), [v], fn y -> [v | y] end) end) 
|> Enum.map(fn {k, v} -> [to_string(k) | v] end)
[["Mark", "Shirt"], ["Tony", "Hat", "Glove"]]

You might need to reverse some order, as I always add to the head of a list [h | t]

1 Like

In fact, I don’t even need to transform the keys…

iex> list 
|> Enum.reduce(%{}, fn [k, v], acc -> Map.update(acc, k, [v], fn y -> [v | y] end) end) 
|> Enum.map(fn {k, v} -> [k | v] end)
1 Like

To guarantee list values are deduped one option is to use a MapSet as collector, or just add Enum.uniq/1 to previous solution.

list
|> Enum.reduce(%{}, fn [k, v], m -> Map.update(m, k, MapSet.new([v]), &MapSet.put(&1, v)) end)
|> Enum.map(fn {k, v} -> [k | Enum.to_list(v)] end)
2 Likes

That is true, your solution is nicer with the use of MapSet :slight_smile:

1 Like

Fascinating, thank you both for the help! I am starting to wrap my head around accumulators and Enum.reduce now.

One more layer of complexity: what if there is another list of items, where it is possible for items to belong to items that belong to a person, ie:

list2 = [ ["Tony", "Glove"] , ["Mark", "Shirt"], ["Tony", "Wallet"], ["Wallet","Money" ] ]

How would you pipe in another Enum.reduce/another function to get:

convert_list2(list2) = [ ["Tony", "Glove", "Wallet","Money" ], ["Mark", "Shirt"] ]

Or would this just require a different initial Enum.reduce?

That is more complex.

A hint would be to get all root keys, by filtering the keys that does not exists in the list of values.

values = list2 |> Enum.map(& List.last(&1))
root_keys = list2 |> Enum.map(& List.first(&1)) |> Enum.filter(& !Enum.member?(values, &1)) |> Enum.uniq

Then, in some loop, treat the root keys as is, and the others as belonging to a sub tree.

Not to mention edge case like this…

[“Mark”, “Wallet”], [“Tony”, “Wallet”]

1 Like

What immediately jumps to my mind is:

╰─➤  iex
Erlang/OTP 20 [erts-9.1] [source] [64-bit] [smp:2:2] [ds:2:2:10] [async-threads:10] [hipe] [kernel-poll:false]

Interactive Elixir (1.6.0-dev) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> list = [ ["Tony", "Glove"] , ["Mark", "Shirt"], ["Tony", "Hat"] ]                     
[["Tony", "Glove"], ["Mark", "Shirt"], ["Tony", "Hat"]]
iex(2)> list |> Enum.group_by(&hd/1) |> Enum.map(&Enum.uniq(List.flatten(elem(&1, 1))))
[["Mark", "Shirt"], ["Tony", "Glove", "Hat"]] 
iex(3)> 

2 Likes