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 :+1: :+1:

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:

1 Like