DogEatDog
DogEatDog

Reputation: 3037

Elixir map_reduce referencing last element

How can I loop (recursively or with reduce) a List of Maps while comparing to the last element in the list?

For example, let's say I have a List of maps like this:

datetime = Timex.beginning_of_day(Timex.now)
data = [
  %{a: 0, cluster: 0, time: datetime},
  %{a: 1, cluster: 0, time: Timex.shift(datetime, minutes: 3)},
  %{a: 2, cluster: 0, time: Timex.shift(datetime, minutes: 6)},
  %{a: 3, cluster: 0, time: Timex.shift(datetime, minutes: 9)},
  %{a: 4, cluster: 1, time: Timex.shift(datetime, minutes: 12)},
  %{a: 5, cluster: 1, time: Timex.shift(datetime, minutes: 15)},
  %{a: 6, cluster: 0, time: Timex.shift(datetime, minutes: 18)},
  %{a: 7, cluster: 0, time: Timex.shift(datetime, minutes: 21)},
  %{a: 8, cluster: 0, time: Timex.shift(datetime, minutes: 23)},
  %{a: 9, cluster: 2, time: Timex.shift(datetime, minutes: 26)},
  %{a: 10, cluster: 2, time: Timex.shift(datetime, minutes: 29)},
  %{a: 11, cluster: 2, time: Timex.shift(datetime, minutes: 32)},
  %{a: 12, cluster: 1, time: Timex.shift(datetime, minutes: 35)},
  %{a: 13, cluster: 1, time: Timex.shift(datetime, minutes: 38)},
]

I want to modify the cluster attribute of each map to represent the group that it is in with respect to the order that it is in.

Using the group_by is great if the cluster ids were not repeating.

I want to group them as they change group, resulting in something like this:

[
  %{a: 0, cluster: 0, time: datetime},
  %{a: 1, cluster: 0, time: Timex.shift(datetime, minutes: 3)},
  %{a: 2, cluster: 0, time: Timex.shift(datetime, minutes: 6)},
  %{a: 3, cluster: 0, time: Timex.shift(datetime, minutes: 9)},
  %{a: 4, cluster: 1, time: Timex.shift(datetime, minutes: 12)},
  %{a: 5, cluster: 1, time: Timex.shift(datetime, minutes: 15)},
  %{a: 6, cluster: 2, time: Timex.shift(datetime, minutes: 18)},
  %{a: 7, cluster: 2, time: Timex.shift(datetime, minutes: 21)},
  %{a: 8, cluster: 2, time: Timex.shift(datetime, minutes: 23)},
  %{a: 9, cluster: 3, time: Timex.shift(datetime, minutes: 26)},
  %{a: 10, cluster: 3, time: Timex.shift(datetime, minutes: 29)},
  %{a: 11, cluster: 3, time: Timex.shift(datetime, minutes: 32)},
  %{a: 12, cluster: 4, time: Timex.shift(datetime, minutes: 35)},
  %{a: 13, cluster: 4, time: Timex.shift(datetime, minutes: 38)},
]

To do this, I need to compare the current item in the list to the previous one. I had started with something like this (below) and stopped because I know it will not reference the previous item to compare the current item to the previous one:

Enum.map_reduce(data, 0, fn(x, acc) -> cluster_grouping(x, acc) end)

def cluster_grouping(x, acc) do
  cond do
    x.cluster == acc -> {Map.put(x, :cluster, acc), acc}
    x.cluster > acc -> {Map.put(x, :cluster, acc), acc + 1}
  end
end

Upvotes: 3

Views: 936

Answers (2)

Dogbert
Dogbert

Reputation: 222040

You'll need to keep two integers in the accumulator: the current resulting cluster (which will increment by 1 on every change) and the last raw value of cluster.

datetime = Timex.beginning_of_day(Timex.now)
data = [
  %{a: 0, cluster: 0, time: datetime},
  %{a: 1, cluster: 0, time: Timex.shift(datetime, minutes: 3)},
  %{a: 2, cluster: 0, time: Timex.shift(datetime, minutes: 6)},
  %{a: 3, cluster: 0, time: Timex.shift(datetime, minutes: 9)},
  %{a: 4, cluster: 1, time: Timex.shift(datetime, minutes: 12)},
  %{a: 5, cluster: 1, time: Timex.shift(datetime, minutes: 15)},
  %{a: 6, cluster: 0, time: Timex.shift(datetime, minutes: 18)},
  %{a: 7, cluster: 0, time: Timex.shift(datetime, minutes: 21)},
  %{a: 8, cluster: 0, time: Timex.shift(datetime, minutes: 23)},
  %{a: 9, cluster: 2, time: Timex.shift(datetime, minutes: 26)},
  %{a: 10, cluster: 2, time: Timex.shift(datetime, minutes: 29)},
  %{a: 11, cluster: 2, time: Timex.shift(datetime, minutes: 32)},
  %{a: 12, cluster: 1, time: Timex.shift(datetime, minutes: 35)},
  %{a: 13, cluster: 1, time: Timex.shift(datetime, minutes: 38)},
]

Enum.map_reduce(data, {0, 0}, fn x, {i, last} -> 
  i = if x.cluster == last, do: i, else: i + 1
  {Map.put(x, :cluster, i), {i, x.cluster}}
end)
|> elem(0)
|> IO.inspect

Output:

[
  %{a: 0, cluster: 0, time: #DateTime<2018-02-18 00:00:00Z>},
  %{a: 1, cluster: 0, time: #DateTime<2018-02-18 00:03:00Z>},
  %{a: 2, cluster: 0, time: #DateTime<2018-02-18 00:06:00Z>},
  %{a: 3, cluster: 0, time: #DateTime<2018-02-18 00:09:00Z>},
  %{a: 4, cluster: 1, time: #DateTime<2018-02-18 00:12:00Z>},
  %{a: 5, cluster: 1, time: #DateTime<2018-02-18 00:15:00Z>},
  %{a: 6, cluster: 2, time: #DateTime<2018-02-18 00:18:00Z>},
  %{a: 7, cluster: 2, time: #DateTime<2018-02-18 00:21:00Z>},
  %{a: 8, cluster: 2, time: #DateTime<2018-02-18 00:23:00Z>},
  %{a: 9, cluster: 3, time: #DateTime<2018-02-18 00:26:00Z>},
  %{a: 10, cluster: 3, time: #DateTime<2018-02-18 00:29:00Z>},
  %{a: 11, cluster: 3, time: #DateTime<2018-02-18 00:32:00Z>},
  %{a: 12, cluster: 4, time: #DateTime<2018-02-18 00:35:00Z>},
  %{a: 13, cluster: 4, time: #DateTime<2018-02-18 00:38:00Z>}
]

Upvotes: 3

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 120990

The more elixirish approach would be to use pattern matching on function clauses inside map-reducer:

Enum.map_reduce(data, {0, 0}, fn
  %{cluster: last} = x, {i, last} ->
    {%{x | cluster: i}, {i, last}}
  %{cluster: last} = x, {i, _} ->
    {%{x | cluster: i + 1}, {i + 1, last}}
end)

Upvotes: 0

Related Questions