# How to access the last iterated Item in an iteration

Hello, I’m trying to calculate a leaderboard of user just attended in a contest.
here is the structure of list I have. it is sorted descending so I’m pretty much sure that the first item has the most point and the next of has equal or less point.

`[%{user_id:1, total_point:20}, %{user_id: 2, total_point:20}, %{user_id: 3, total_point: 16}]`

The thing I want to do in this function is: add a attribute of rank to each map and return it.
And here is my code

``````result = user_list
|> Enum.with_index
|> Enum.map(fn({x, i}) ->
case i do
0 ->
x = Map.put(x, :rank, 1)
_ ->
case Enum.at(user_list, i-1).total_point > x.total_point do
true ->
x = Map.put(x, :rank, x.last.rank + 1)
false ->
x = Map.put(x, :rank, x.last.rank)
end
end
end)
``````

the problem is if I want to access the current user rank attribute ( which I just dynamically added to the map ) with x.last or even with Enum.at(user_list, i-1).rank it say it does not exist which is true because it will be returned in my result variable when the job is done. so I can not access the last modified ( new map with rank attribute ). how can I have it and use it in this step.

and what I expect from my function to return is:

`[%{rank: 1, user_id:1, total_point:20}, %{rank: 1, user_id: 2, total_point:20}, %{rank: 2, user_id: 3, total_point: 16}]`

I would appreciate any help on this

There is no magical way of accessing last item - you have store it somewhere.

You can use `Enum.reduce` :

``````def rank_users([]), do: []
def rank_users(user_list) do
{first, rest} =
case user_list do
[first] ->
{first, []}

_ ->
[first | rest] = user_list
{first, rest}

end
first = Map.put(first, :rank, 1)
{_, list} =
Enum.reduce(rest, {first, [first]}, fn x, acc ->
{prev, list} = acc
rank =
cond do
prev.total_point > x.total_point ->
prev.rank + 1

true ->
prev.rank
end
curr = Map.put(x, :rank, rank)
list = [curr | list]
{curr, list}
end)
list # <-- or Enum.reverse(list)
end
``````

You can fine tune the logic. If you want list in the same order as user_list, add `Enum.reverse` .

1 Like

Thanks, I will give it a try  I did it this way but I’m not sure about the efficiency:

``````user_list
|> Enum.group_by(fn x -> x.total_point end)
|> Enum.sort_by( &(&1), :desc)
|> Enum.with_index
|> Enum.into([],fn {{point , users} , rank} -> Enum.map(users,fn user -> Map.put(user,:rank,rank + 1) end ) end )
|> List.flatten()
``````

Benchmark using benchee and decide for yourself.

Your code iterates the list multiple times - depends on the length of user_lists you are expecting.

1 Like

you don’t need those extra steps, something like this would suffice:

``````user_list
|> Enum.sort_by(& &1.total_point, :desc)
|> Enum.with_index()
|> Enum.map(fn {el, idx} -> Map.put(el, :rank, idx + 1) end)
``````
1 Like

`Enum.chunk_by` can help

``````iex(43)> user_list = [%{user_id: 1, total_point: 20}, %{user_id: 2, total_point: 20}, %{user_id: 3, total_point: 16}]

iex(44)> for {users, rank} <- Enum.chunk_by(user_list, &Map.get(&1, :total_point)) |> Enum.with_index(1), user <- users, do: Map.put(user, :rank, rank)
[
%{rank: 1, total_point: 20, user_id: 1},
%{rank: 1, total_point: 20, user_id: 2},
%{rank: 2, total_point: 16, user_id: 3}
]
``````
2 Likes

Enum.with_index/2 accepts 2nd param - `fun_or_offset`

You can pass 1 for the index to start at 1 instead of 0.

``````iex(3)> Enum.with_index([:a, :b, :c])
[a: 0, b: 1, c: 2]
iex(4)> Enum.with_index([:a, :b, :c], 1)
[a: 1, b: 2, c: 3]
``````

Also you can merge below into one:

by passing function to `Enum.with_index`:

``````Enum.with_index(t, fn {_, list}, index -> Enum.map(list, &(Map.put(&1, :rank, index + 1))) end)
``````
2 Likes

It is so useful, thanks a million 1 Like