How to perform operations on a dynamic list

Hi,

i am having a dynamic list of tuples where the data is continuous and the last element of the tuple is an integer.

[
  {:a, "John", 1},
  {:b, 1},
  {:a, "Alpha", 28, 1}, 
  {:a, "John", -1}
]

Here 1 means insertion and -1 means deletion.

As you can see in the list I am having two same entries {:a, "John", 1} and {:a, "John", -1}. The sum of last element gives us 0 and hence i want to delete it from the list.

How can i get the output where my final result looks like

[
  {:b, 1},
  {:a, "Alpha", 28, 1}, 
]

The tuple (being of variable len) is not very convenient here, because you have to isolate the last element and use the rest to keep track.
So I’d first think about using sth like

{{:a, "Alpha", 28}, 1}

instead. (But you can go on with the flat tuple also, will become a little messy though.

Then you could reduce the list with %{} as accumulator init and for each list-element use

Map.update/4

to accumulate your ‘actions’.

Map.update(acc, key, action, fn old -> old + action end)

with eg {:a, "Alpha", 28} = key, 1 = action.

Then you just map over the resulting dict and throw all with 0 values away.

1 Like

Is the elimination of these elements based on the last element of the tuple, always?

Also, what makes those tuples “same”? You say that those starting with {:a, "John", ...} are same but then you also have a tuple that’s only {:b, 1}. What would another “same” tuple be that starts with :b?

1 Like

OK, I needed 30 minutes break from work so I tried my hand at this.

Assuming that the “key” of a tuple is all except the last element, and the value (append / delete) is always only the last element, then this code does what you need:

defmodule Play
  defguard is_valid_record(x) when is_tuple(x) and tuple_size(x) > 1

  def input,
    do: [
      {:a, "John", 1},
      {:b, 1},
      {:a, "Alpha", 28, 1},
      {:a, "John", -1}
    ]

  def key(tuple) when is_valid_record(tuple) do
    Tuple.delete_at(tuple, tuple_size(tuple) - 1)
  end

  def value(tuple) when is_valid_record(tuple) do
    elem(tuple, tuple_size(tuple) - 1)
  end

  def combine({a, b}) when is_tuple(a), do: Tuple.append(a, b)

  def process(list) when is_list(list) do
    list
    |> Enum.map(fn tuple -> {key(tuple), value(tuple)} end)
    |> Enum.reduce(%{}, fn {key, value}, acc ->
      Map.update(acc, key, value, fn previous_value -> value + previous_value end)
    end)
    |> Enum.reject(fn {_key, value} -> value == 0 end)
    |> Enum.map(&combine/1)
  end

  def test do
    input() |> process()
  end
end

Just run Play.test in iex and you got it.

4 Likes

This is how it looks with a little adjustment to the data structure.

test "it" do
  l = [
    {{:a, "John"}, 1},
    {{:b}, 1},
    {{:a, "Alpha", 28}, 1},
    {{:a, "John"}, -1}
  ]

  assert [{{:b}, 1}, {{:a, "Alpha", 28}, 1}] ==
            Enum.reduce(l, %{}, fn {key, action}, acc ->
              Map.update(acc, key, action, fn old -> old + action end)
            end)
            |> Enum.filter(fn {k, v} -> v != 0 end)
end

Such a neat solution. I was using Enum.group_by and taking the values. Learnt a lot from you today.

Thanks, @Sebb but I didn’t want to change my structure. So, @dimitarvp solution works best in my case.

While I do support the idea of using an appropriate data structure for solving a certain problem it’s not much more code converting the datastructure on demand:

l = [
  {:a, "John", 1},
  {:b, 1},
  {:a, "Alpha", 28, 1},
  {:a, "John", -1}
]

intermediate = 
  Enum.reduce(l, %{}, fn tuple, acc ->
    {action, key} = tuple |> Tuple.to_list |> List.pop_at(-1)
    Map.update(acc, List.to_tuple(key), action, fn old -> old + action end)
  end)

result = for {k, v} <- intermediate, v != 0, do: Tuple.append(k, v)

assert [{:b, 1}, {:a, "Alpha", 28, 1}] == result

Edit: Updated result as well.

3 Likes

Yeah, I absolutely agree with that and I have learnt my lesson from it.

It isn’t much more code, but it’s way more complex.

I would definitely change the data structure here.
This will not be the only place it introduces overhead.

I think using tuples this way is just wrong.
At least the tuples, that are tagged the same should have the same structure.