In today’s problem we need to implement a function that accepts a list of strings showing the results of football matches and return a nicely formatted table showing the leaderboard (sorted by points descending where each win is worth 3 points, a draw 1 point and loss 0 points).

In other words, we need to turn this:

```
[
"Allegoric Alaskans;Blithering Badgers;win",
"Devastating Donkeys;Courageous Californians;draw",
"Devastating Donkeys;Allegoric Alaskans;win",
"Courageous Californians;Blithering Badgers;loss",
"Blithering Badgers;Devastating Donkeys;loss",
"Allegoric Alaskans;Courageous Californians;win"
]
```

Into this:

```
Team | MP | W | D | L | P
Devastating Donkeys | 3 | 2 | 1 | 0 | 7
Allegoric Alaskans | 3 | 2 | 0 | 1 | 6
Blithering Badgers | 3 | 1 | 0 | 2 | 3
Courageous Californians | 3 | 0 | 1 | 2 | 1
```

This is the first `medium`

difficulty problem on the Elixir track and it’s definitely a fairly in-depth problem. We cover a lot of skills in this solution, including string processing, reduce, complex `case`

statements and string formatting.

## Solution video with explanations

This is my daily live stream video solution to this problem, in which I guide you through each step of the way and explain my logic in approaching the problem and selecting the best Elixir functions for the job.

- Video available here: https://youtu.be/V6LqglUNyNo
- Also streaming on Twitch: https://www.twitch.tv/percygrunwald

## Explanation of the solution

I decided early on in solving this problem that I would need to split the solution into 2 parts:

- Translating the lines of results into a “statistics” map with each team as the key
- Converting the statistics map into a list of formatted lines that can be printed with the header line

### Convert the results into a statistics map

The input we get is in the form:

```
[
"Allegoric Alaskans;Blithering Badgers;win",
"Devastating Donkeys;Courageous Californians;draw",
"Devastating Donkeys;Allegoric Alaskans;win",
"Courageous Californians;Blithering Badgers;loss",
"Blithering Badgers;Devastating Donkeys;loss",
"Allegoric Alaskans;Courageous Californians;win"
]
```

And I would like to convert those lines into a map like this:

```
%{
"Allegoric Alaskans" => %{
draws: 0,
losses: 1,
matches_played: 3,
points: 6,
wins: 2
},
"Blithering Badgers" => %{
draws: 0,
losses: 2,
matches_played: 3,
points: 3,
wins: 1
},
"Courageous Californians" => %{
draws: 1,
losses: 2,
matches_played: 3,
points: 1,
wins: 0
},
"Devastating Donkeys" => %{
draws: 1,
losses: 0,
matches_played: 3,
points: 7,
wins: 2
}
}
```

Since we are translating a list into another data structure, my go-to function will always be `Enum.reduce/3`

.

For each line in the input, we’re going to split the string on `";"`

with `String.split/3`

:

```
"Allegoric Alaskans;Blithering Badgers;win" => ["Allegoric Alaskans", "Blithering Badgers", "win"]
```

Splitting the string in this way, we can match on the 3 possible valid outcomes:

`[winner, loser, "win"]`

`[team1, team2, "draw"]`

`[loser, winner, "loss"]`

For each one of these cases, we need to update our accumulator 2 times: once for each team. Here’s the full function before we implement the helper functions to update the accumulator:

```
defp calculate_stats(input_lines) do
Enum.reduce(input_lines, %{}, fn line, acc ->
line
|> String.split(";")
|> case do
[winner, loser, "win"] ->
acc
|> update_win(winner)
|> update_loss(loser)
[team1, team2, "draw"] ->
acc
|> update_draw(team1)
|> update_draw(team2)
[loser, winner, "loss"] ->
acc
|> update_win(winner)
|> update_loss(loser)
_ ->
acc
end
end)
end
```

The default path in the `case`

statement will handle any invalid input that doesn’t fit the expected shape of the results.

The `update_...`

helper functions do the updates to the accumulator based on the result. For a win we increase the `wins`

by 1, the `matches_played`

by 1 and the `points`

by 3:

```
defp update_win(acc, winner) do
Map.update(acc, winner, merge_init(%{wins: 1, matches_played: 1, points: 3}), fn
current_stats ->
current_stats
|> Map.update(:wins, 1, fn current_wins -> current_wins + 1 end)
|> Map.update(:matches_played, 1, fn current_matches_played ->
current_matches_played + 1
end)
|> Map.update(:points, 3, fn current_points -> current_points + 3 end)
end)
end
```

For a loss, we increase the `losses`

by 1, the `matches_played`

by 1 and make no modification to `points`

:

```
defp update_loss(acc, loser) do
Map.update(acc, loser, merge_init(%{losses: 1, matches_played: 1, points: 0}), fn
current_stats ->
current_stats
|> Map.update(:losses, 1, fn current_losses -> current_losses + 1 end)
|> Map.update(:matches_played, 1, fn current_matches_played ->
current_matches_played + 1
end)
end)
end
```

For a draw, we increase the `draws`

by 1, the `matches_played`

by 1 and increase `points`

by 1:

```
defp update_draw(acc, team) do
Map.update(acc, team, merge_init(%{draws: 1, matches_played: 1, points: 1}), fn
current_stats ->
current_stats
|> Map.update(:draws, 1, fn current_draws -> current_draws + 1 end)
|> Map.update(:matches_played, 1, fn current_matches_played ->
current_matches_played + 1
end)
|> Map.update(:points, 1, fn current_points -> current_points + 1 end)
end)
end
```

To make sure we have all the keys available for each team, we use a `merge_init/1`

helper function to initialize our map for each team with `Map.merge/2`

:

```
defp merge_init(map) do
Map.merge(%{wins: 0, losses: 0, draws: 0, matches_played: 0, points: 0}, map)
end
```

### Convert the statistics map into a list of formatted strings

We now have a standard representation of our statistics for each team:

```
%{
"Allegoric Alaskans" => %{
draws: 0,
losses: 1,
matches_played: 3,
points: 6,
wins: 2
},
"Blithering Badgers" => %{
draws: 0,
losses: 2,
matches_played: 3,
points: 3,
wins: 1
},
"Courageous Californians" => %{
draws: 1,
losses: 2,
matches_played: 3,
points: 1,
wins: 0
},
"Devastating Donkeys" => %{
draws: 1,
losses: 0,
matches_played: 3,
points: 7,
wins: 2
}
}
```

Which we need to convert into sorted tabular data (excluding the header row):

```
Devastating Donkeys | 3 | 2 | 1 | 0 | 7
Allegoric Alaskans | 3 | 2 | 0 | 1 | 6
Blithering Badgers | 3 | 1 | 0 | 2 | 3
Courageous Californians | 3 | 0 | 1 | 2 | 1
```

Firstly, we can convert the `map`

into a `list`

, which we can then sort, using `Map.to_list/1`

. The output of `Map.to_list/1`

is this:

```
[
{"Allegoric Alaskans",
%{draws: 0, losses: 1, matches_played: 3, points: 6, wins: 2}},
{"Blithering Badgers",
%{draws: 0, losses: 2, matches_played: 3, points: 3, wins: 1}},
{"Courageous Californians",
%{draws: 1, losses: 2, matches_played: 3, points: 1, wins: 0}},
{"Devastating Donkeys",
%{draws: 1, losses: 0, matches_played: 3, points: 7, wins: 2}}
]
```

Because of the complexity of the items in this list, we can sort it with `Enum.sort_by/3`

and sort on the `points`

. The second argument we pass to `Enum.sort_by/3`

will be an anonymous function to extract just the `:points`

for each item, to which the `sorter`

is applied.

The default `sorter`

function for `Enum.sort/3`

is `Kernel.<=/2`

, which will sort the list in ascending order. Since we would like the list sorted in *descending* order, we can either use `Enum.reverse/1`

to reverse the list after it has been sorted or change the `sorter`

to `Kernel.>=/2`

. I prefer changing the sorter:

```
Enum.sort_by(list, fn {_team, %{points: points}} -> points end, &Kernel.>=/2)
```

Now all that’s left to do is format each line. Since we are transforming a list into another list of the same length, the default function we should us is `Enum.map/2`

. Each line needs to follow this format:

```
"#{team} | #{matches} | #{wins} | #{draws} | #{losses} | #{points}"
```

The special thing to note is that that `team`

should be trailing padded to maximum width of `30`

characters and each of the scores should be leading padded to a maximum width of `2`

characters. We can simply use `String.pad_leading/3`

and `String.pad_trailing/3`

.

All that’s left to do is join together each line with a `newline`

(`"\n"`

) and we’re set:

```
defp get_lines_to_print(stats) do
stats
|> Map.to_list()
|> Enum.sort_by(fn {_team, %{points: points}} -> points end, &Kernel.>=/2)
|> Enum.map(fn {team, stats} ->
matches_played = "#{stats.wins + stats.draws + stats.losses}"
team_string = String.pad_trailing(team, 30)
matches_string = String.pad_leading(matches_played, 2)
wins_string = String.pad_leading("#{stats.wins}", 2)
draws_string = String.pad_leading("#{stats.draws}", 2)
losses_string = String.pad_leading("#{stats.losses}", 2)
points_string = String.pad_leading("#{stats.points}", 2)
"#{team_string} | #{matches_string} | #{wins_string} | #{draws_string} |" <>
" #{losses_string} | #{points_string}"
end)
|> Enum.join("\n")
end
```

### Tying it all together

Now that we have our 2 helper functions to convert the input into stats and stats into formatted lines, we can simply return the formatted lines underneath the static header line and we’re all done:

```
@spec tally(input :: list(String.t())) :: String.t()
def tally(input) do
lines_to_print =
input
|> calculate_stats()
|> get_lines_to_print()
"""
Team | MP | W | D | L | P
#{lines_to_print}
"""
|> String.trim()
end
```

## Full solution in text form

Here’s the full solution in text form, in case you want to look over the final product:

```
defmodule Tournament do
@spec tally(input :: list(String.t())) :: String.t()
def tally(input) do
lines_to_print =
input
|> calculate_stats()
|> get_lines_to_print()
"""
Team | MP | W | D | L | P
#{lines_to_print}
"""
|> String.trim()
end
defp get_lines_to_print(stats) do
stats
|> Map.to_list()
|> Enum.sort_by(fn {_team, %{points: points}} -> points end, &Kernel.>=/2)
|> Enum.map(fn {team, stats} ->
matches_played = "#{stats.wins + stats.draws + stats.losses}"
team_string = String.pad_trailing(team, 30)
matches_string = String.pad_leading(matches_played, 2)
wins_string = String.pad_leading("#{stats.wins}", 2)
draws_string = String.pad_leading("#{stats.draws}", 2)
losses_string = String.pad_leading("#{stats.losses}", 2)
points_string = String.pad_leading("#{stats.points}", 2)
"#{team_string} | #{matches_string} | #{wins_string} | #{draws_string} |" <>
" #{losses_string} | #{points_string}"
end)
|> Enum.join("\n")
end
defp calculate_stats(input_lines) do
Enum.reduce(input_lines, %{}, fn line, acc ->
line
|> String.split(";")
|> case do
[winner, loser, "win"] ->
acc
|> update_win(winner)
|> update_loss(loser)
[team1, team2, "draw"] ->
acc
|> update_draw(team1)
|> update_draw(team2)
[loser, winner, "loss"] ->
acc
|> update_win(winner)
|> update_loss(loser)
_ ->
acc
end
end)
end
defp update_win(acc, winner) do
Map.update(acc, winner, merge_init(%{wins: 1, matches_played: 1, points: 3}), fn
current_stats ->
current_stats
|> Map.update(:wins, 1, fn current_wins -> current_wins + 1 end)
|> Map.update(:matches_played, 1, fn current_matches_played ->
current_matches_played + 1
end)
|> Map.update(:points, 3, fn current_points -> current_points + 3 end)
end)
end
defp update_loss(acc, loser) do
Map.update(acc, loser, merge_init(%{losses: 1, matches_played: 1, points: 0}), fn
current_stats ->
current_stats
|> Map.update(:losses, 1, fn current_losses -> current_losses + 1 end)
|> Map.update(:matches_played, 1, fn current_matches_played ->
current_matches_played + 1
end)
end)
end
defp update_draw(acc, team) do
Map.update(acc, team, merge_init(%{draws: 1, matches_played: 1, points: 1}), fn
current_stats ->
current_stats
|> Map.update(:draws, 1, fn current_draws -> current_draws + 1 end)
|> Map.update(:matches_played, 1, fn current_matches_played ->
current_matches_played + 1
end)
|> Map.update(:points, 1, fn current_points -> current_points + 1 end)
end)
end
defp merge_init(map) do
Map.merge(%{wins: 0, losses: 0, draws: 0, matches_played: 0, points: 0}, map)
end
end
```

Did you have a different solution or think that my solution could be improved? Please hit me up in the comments below!