1. 11
  1. 5

    Here you have much smaller, and IMHO much more readable example of the same:

    defmodule Ranking do
      def ranking(participants) do
        participants
        |> Enum.sort(&(&1.points >= &2.points))
        |> Enum.map_reduce({nil, 0, 0}, &do_ranking/2)
        |> elem(0)
      end
    
      defp do_ranking(%{points: points} = curr, {%{points: points}, rank, count}) do
        count = count + 1
        {Map.put(curr, :ranking, rank), {curr, rank, count}}
      end
    
      defp do_ranking(curr, {_, rank, count}) do
        next_rank = rank + count + 1
        {Map.put(curr, :ranking, next_rank), {curr, next_rank, 0}}
      end
    
      def rank_mock_data() do
        [
          %{id: 1, points: 39},
          %{id: 2, points: 26},
          %{id: 3, points: 50},
          %{id: 4, points: 24},
          %{id: 5, points: 39},
          %{id: 6, points: 67},
          %{id: 7, points: 39},
          %{id: 8, points: 50},
          %{id: 9, points: 48},
          %{id: 10, points: 24}
        ]
        |> ranking()
      end
    end
    
    Ranking.rank_mock_data()
    |> inspect(pretty: true)
    |> IO.puts()
    

    EDIT: Earlier version was slightly buggy, as it didn’t returned ranks always increased by 1, this fixes it.

    1. 1

      Cool! Thanks for sharing :)

    2. 3

      Inspired by @geocar, here’s the J version. It assumes the values are stored in an Nx2 array where the first column is the score and the second is the id:

      r ,. >./\(* >:@i.@#) ~: 0{"1 r =: \:~ x
      

      This makes the ranking column three. There’s probably a more elegant way to do this but I’m on my phone rn. Breakdown:

      1. Sort the participants by score, descending
      2. Get the “nub seive”, the bitstring corresponding to the indices with the first occurrence of each element
      3. Multiply the bitstring by the index array. First occurrences become their index, duplicates remain zero.
      4. Get the running maximum of the list, which replaces zeros with the index of the first occurrence
      5. Combine that with the original sorted array.
      1. 3

        Perl5:

        #! /usr/bin/env perl
        use Modern::Perl '2015';
        ###
        
        my %hist;
        my $id = 1;
        while (<DATA>) {
            chomp;
            push @{$hist{$_}}, $id;
            $id++;
        }
        my $rank =1;
        foreach my $score (sort {$b<=>$a} keys %hist) {
            foreach my $id (@{$hist{$score}}) {
                printf("id: %2d, points: %2d, ranking: %d\n",
                       $id, $score,$rank
                      );
            }
            $rank += scalar @{$hist{$score}};
        }
        __DATA__
        39
        26
        50
        24
        39
        67
        39
        50
        48
        24
        
        1. 2

          That encouraged me to do it in (mostly) AWK:

          $ cat ranking.txt
          1,39
          2,26
          3,50
          4,24
          5,39
          6,67
          7,39
          8,50
          9,48
          10,24
          
          $ cat ranking.awk
          BEGIN {
              FS = ",";
              OFS = ",";
          
              ranking = 0;
              count = 0;
              last_points = 0;
          
              print "ranking", "points", "id";
          }
          
          {
              if (last_points == $2) {
                  count++;
              } else {
                  ranking += count + 1;
                  count = 0;
                  last_points = $2;
              };
          
              if (ranking == 0) {
                  ranking = 1;
              };
          
              print ranking, $2, $1;
          }
          
          $ sort -t, -k2nr ranking.txt | awk -f ranking.awk
          ranking,points,id
          1,67,6
          2,50,3
          2,50,8
          4,48,9
          5,39,1
          5,39,5
          5,39,7
          8,26,2
          9,24,10
          9,24,4
          
        2. 2

          Here’s another Elixir implementation, similar to @hauleth’s!

          By using Enum.group_by/2 up front, our reduce step is a little simpler, because the rank of every set of data is the rank of the prior set plus the length of the prior set. I also changed the output a little bit, it’s just a map whose keys are the ranks, and the values are the data at that rank.

          defmodule Ranking do
            @mock_data [
              %{id: 1, points: 39},
              %{id: 2, points: 26},
              %{id: 3, points: 50},
              %{id: 4, points: 24},
              %{id: 5, points: 39},
              %{id: 6, points: 67},
              %{id: 7, points: 39},
              %{id: 8, points: 50},
              %{id: 9, points: 48},
              %{id: 10, points: 24}
            ]
          
            def mock_data do
              @mock_data
            end
          
            def rank(data) do
              data
              # Gives us a list of {points, data} tuples
              |> Enum.group_by(& &1.points)
              |> Enum.sort(&(elem(&1, 0) > elem(&2, 0)))
              |> Enum.reduce({1, %{}}, fn {_points, data}, {next_rank, ranking} ->
                {next_rank + length(data), Map.put(ranking, next_rank, data)}
              end)
              |> elem(1)
            end
          end
          
          Ranking.mock_data()
          |> Ranking.rank()
          |> IO.inspect()
          

          And here’s a similar approach in JavaScript:

          const mockData = [
            {id: 1, points: 39},
            {id: 2, points: 26},
            {id: 3, points: 50},
            {id: 4, points: 24},
            {id: 5, points: 39},
            {id: 6, points: 67},
            {id: 7, points: 39},
            {id: 8, points: 50},
            {id: 9, points: 48},
            {id: 10, points: 24}
          ]
          
          function rank(data) {
            const grouped = data.reduce((grouped, datum) => {
              if (!grouped.get(datum.points)) grouped.set(datum.points, [])
              grouped.get(datum.points).push(datum)
              return grouped
            }, new Map()) // Use a Map so we don't have to parseInt on object keys
          
            const sorted = Array.from(grouped.entries()).sort(([a], [b]) => b - a)
          
            return sorted.reduce(([nextRank, ranking], [, data]) => {
              ranking[nextRank] = data
              return [nextRank + data.length, ranking]
            }, [1, {}])[1]
          }
          
          console.log(JSON.stringify(rank(mockData), null, 2))
          
          1. 1

            My problem with that solution is that it needlessly iterates through all data at least 3 times (1 for Enum.group_by/2, 1 for sort/2, and 1 for length/1 inside Enum.reduce/3). This can be a little bit of the problem.

            1. 1

              That’s true! A good point.

              I think the original solution linked iterates over the data twice (sorting, then the iteration over each item using recursion). This solution is at worst once over the data to group by score, then twice over the groups, which is indeed worst-case an extra iteration than the linked solution if each piece of the data has a unique point value within the data. It only iterates over all the data 3 times in this case (each piece of data has a unique point value). The second and third iterations are actually sized by the unique point values in the data set. They’re both still O(n), though.

          2. 1

            My solution:

            items
            |> Enum.sort_by(& &1.points * -1)
            |> Enum.map_reduce(nil, fn
              item, nil -> {Map.put(item, :ranking, 1), {1, item.points, 1}}
              %{points: points} = item, {rank, points, count} -> {Map.put(item, :ranking, rank), {rank, points, count + 1}}
              %{points: points} = item, {rank, _, count} -> {Map.put(item, :ranking, rank + count), {rank + count, points, 1}}
            end)
            |> elem(0)
            |> IO.inspect()
            
            1. 1

              Nice trick with that Enum.sort_by/2. I used tuple in my solution to not need to handle item, nil as a separate case. I thought about using pattern match in the closure, but I thought that separate helper function will be clearer.

            2. 1

              Come on guys, there are data structures for this sort of thing…

              $ cat <<'EOF' > ranking.erl
              #!/usr/bin/env escript
              
              -define(DATA, [
                      #{ id =>  1, points => 39 },
                      #{ id =>  2, points => 26 },
                      #{ id =>  3, points => 50 },
                      #{ id =>  4, points => 24 },
                      #{ id =>  5, points => 39 },
                      #{ id =>  6, points => 67 },
                      #{ id =>  7, points => 39 },
                      #{ id =>  8, points => 50 },
                      #{ id =>  9, points => 48 },
                      #{ id => 10, points => 24 }
              ]).
              
              main([]) ->
                      Data0 = lists:foldl(fun(X, A) ->
                              orddict:append(1 / maps:get(points, X), X, A)
                      end, orddict:new(), ?DATA),
                      {_Rank, Data} = orddict:fold(fun(_K, V0, {R, A}) ->
                              V = lists:map(fun(X) -> X#{ ranking => R } end, V0),
                              {R + length(V), lists:append(A, V)}
                      end, {1, []}, Data0),
                      io:format("~p~n", [Data]),
                      halt(0).
              EOF
              
              $ escript ranking.erl 
              [#{id => 6,points => 67,ranking => 1},
               #{id => 3,points => 50,ranking => 2},
               #{id => 8,points => 50,ranking => 2},
               #{id => 9,points => 48,ranking => 4},
               #{id => 1,points => 39,ranking => 5},
               #{id => 5,points => 39,ranking => 5},
               #{id => 7,points => 39,ranking => 5},
               #{id => 2,points => 26,ranking => 8},
               #{id => 4,points => 24,ranking => 9},
               #{id => 10,points => 24,ranking => 9}]
              
              1. 4

                Challenge accepted!

                q)t:([] id:1+til 10; points:39 26 50 24 39 67 39 50 48 24)
                q)ungroup select id,points,first each rnk from `points xgroup update rnk:1+i from `points xdesc t
                id points rnk
                -------------
                6  67     1  
                3  50     2  
                8  50     2  
                9  48     4  
                1  39     5  
                5  39     5  
                7  39     5  
                2  26     8  
                4  24     9  
                10 24     9