Keep unique elements on list based on comparison

So I have a DB query that returns a list of lists that looks like this :

[["foobar", "boolean", true], ["baz", "actor", true], ["foobar", "actor", false]]

What I want to do is : if there are two lists with the same first element, but one has “boolean” as the second element, and the other has “actor”, I want to only keep the element with “actor” in the list

Therefore, if the above-given list is the input of my function, I expect the output of that function to be [["baz", "actor", true], ["foobar", "actor", false]]

I want to keep any “boolean” member if there is no other “actor” member with the same first element in my whole list.

The third value is then the one I am interested in.

How can I do that?

Let me know if this isn’t clear, have a good day!

1 Like

Hi Julien,
Do you want the solution or to be guided through?

3 Likes

hi @eksperimental ,

well I’d love to be guided through but I don’t want to take your precious time too much !

My guess was to first split the list between actors and boolean, then I’m stuck

Edit : I edited my post since I forgot a little part of the problem, not sure it is important but each list actually has three elements and I am interested in the third value.

I will guide you. No worries.
First, filter the lists that only have “boolean” as its second element, which are the ones you care about. You can use Enum.filter or Enum.reject

3 Likes

Ok so something like Enum.filter(my_list, fn [_, type, _] -> type == "boolean" end) should do the trick for the first step

You can utilize list pattern matching quite well for this case. Here’s a partial solution to your problem that only filters out the first element and blindly returns everything after it. But it’s IMO a good start so you can craft your own afterwards.

defmodule Xyz do
  @input [["foobar", "boolean", true], ["baz", "actor", true], ["foobar", "actor", false]]

  def test(), do: pick(@input)

  def pick([[_, "boolean", _] | rest]) do
    rest
  end

  def pick(list) when is_list(list), do: list
end
2 Likes

ok. I see your changes.
Ok. from that filtered list, just keep the first element of it.
so you will end up with [“foobar”]
let’s name this list with_boolean

Now what you need to do is iterate through your original list, and you keep a second copy of your original list (let’s name it accumulator) which you will be modifying as we go though your original list.

So for every element in your original list, you check if the second element is “boolean” and if the first element is in with_boolean,
if so, delete/reject in the accumulator all items where the first element is the current first item in the iteration, and the second one is “boolean”
and voila.

UPDATE:
To check the validity of your algorithm,
it is better that you use as your input:
[["foobar", "boolean", true], ["baz", "actor", true], ["foobar", "actor", false], ["xyz", "boolean", true]]

then your output should be:
[["baz", "actor", true], ["foobar", "actor", false], ["xyz", "boolean", true]]

SUMMARY:
The trick is to keep a copy of your original list and you drop elements as you iterate over your original list.

2 Likes

That could even be more elegant, but you will have to keep an accumulator somewhere I guess

1 Like

@JulienCorb try to implement both if you can, One calling the functions in Enum module, the other with recursive functions.

Once you post your solutions I will share .livebook file with mine so you can compare.

1 Like

Yep thank you so much for your help, I’m working on it right now :sweat_smile:

1 Like

I went with a slightly different approach :

I group my “flags” by name, then if there are more than two in the same group, I filter out the boolean ones.

What do you think ? My only problem is that I have a list of list of lists at the end :

  def format_result(rows) do
    rows
    |> Enum.group_by(fn [name, _, _] -> name end)
    |> Enum.map(fn {_, v} -> manage_priority(v) end)
  end
  defp manage_priority(flags) when length(flags) > 1 do
    Enum.filter(flags, fn [_, type, _] -> type == "actor" end)
  end

  defp manage_priority(flags), do: flags

output:

[
  [["baz", "actor", true]],
  [["foobar", "actor", false]],
  [["xyz", "boolean", true]]
]

you are grouping by the first element in the list, if you have other elements with the same first element, you are discarding them.

Consider this input

lists = [
  ["aaa", "actor", true],
  ["aaa", "actress", true],

  ["bbb", "BBB", true],
  ["bbb", "bbb", true],
]

the output will be:

[[["aaa", "actor", true]], []]

whereas the output should have been the same (I am ignoring the nested lists for now)

The approach is good, i like it.

1 Like

Indeed but in my problem, the "actor" value has precedence over everything, so if ever I have any other value than "actor" it isn’t really a problem to discard them

I’m going to try and find a way to get rid of that unnecessary nested list

I’m open to sharing solutions from now :innocent:

Note that are you discarding everything that does not have “actors” as its first element like the “bbb” elements in my example.

oh yeah I understand now,

This shouldn’t be a problem for my current need but that might be if ever I have to add new types in the logic in the future.

Here’s my take:

lists = [
  ["foobar", "boolean", true],
  ["baz", "actor", true],
  ["foobar", "actor", false],
  ["xyz", "boolean", true]
]

# This is an optimization for Enum.filter + Enum.map
with_boolean =
  Enum.reduce(lists, [], fn
    [first, "boolean", _third], acc -> [first | acc]
    _, acc -> acc
  end)

Enum.reduce(lists, lists, fn
  [first, second, _third], acc ->
    if second == "actor" and first in with_boolean do
      Enum.reject(acc, fn
        [head, "boolean", _third] when head == first -> true
        _ -> false
      end)
    else
      acc
    end
end)

UPDATE:

Ported to a more “digestible” format

defmodule Julien do
  def format_result(lists) when is_list(lists) do
    Enum.reduce(lists, lists, &filter_actor(&1, &2, with_boolean(lists)))
  end

  defp with_boolean(lists) do
    Enum.reduce(lists, [], fn
      [first, "boolean", _third], acc -> [first | acc]
      _, acc -> acc
    end)
  end

  defp filter_actor([first, second, _third], acc, with_boolean) do
    if second == "actor" and first in with_boolean do
      Enum.reject(acc, fn
        [head, "boolean", _third] when head == first -> true
        _ -> false
      end)
    else
      acc
    end
  end
end
1 Like

Approaching this from a different angle: how about converting the list of lists into something more structured? A list of maps or maybe even a list of structs? This would bring some clarity to the code.

1 Like

Julien’s approach is pretty much this, groups them by the head of the lists into a map, which is a much simpler solution than mine I think.

thank you very much for sharing! Indeed your solution would keep any other kind of type other than "actor", except "boolean" of course. + You don’t have problems of nested list like my solution.

I’ve tried to avoid nested loops as much as possible but I don’t think it is possible here :sweat_smile:

Like this?

l 
|> Enum.group_by(fn [k, _, _] -> k end) 
|> Enum.map(fn {_k, v} -> v |> Enum.sort_by(fn [_, x, _] -> x end) |> List.first() end)
[["baz", "actor", true], ["foobar", "actor", false]]

It works because actor comes before boolean :slight_smile:

1 Like