ðŸŽ‰ Exercism Research is now launched. Help Exercism, help science and have some fun at research.exercism.io ðŸŽ‰

# angelikatyborska's solution

## to Tournament in the Elixir Track

Published at Jan 19 2020 · 0 comments
Instructions
Test suite
Solution

Tally the results of a small football competition.

Based on an input file containing which team played against which and what the outcome was, create a file with a table like 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
``````

What do those abbreviations mean?

• MP: Matches Played
• W: Matches Won
• D: Matches Drawn (Tied)
• L: Matches Lost
• P: Points

A win earns a team 3 points. A draw earns 1. A loss earns 0.

The outcome should be ordered by points, descending. In case of a tie, teams are ordered alphabetically.

Input

``````Allegoric Alaskans;Blithering Badgers;win
Devastating Donkeys;Courageous Californians;draw
``````

The result of the match refers to the first team listed. So this line

``````Allegoric Alaskans;Blithering Badgers;win
``````

This line:

``````Courageous Californians;Blithering Badgers;loss
``````

Means that the Blithering Badgers beat the Courageous Californians.

And this line:

``````Devastating Donkeys;Courageous Californians;draw
``````

Means that the Devastating Donkeys and Courageous Californians tied.

Formatting the output is easy with `String`'s padding functions. All number columns can be left-padded with spaces to a width of 2 characters, while the team name column can be right-padded with spaces to a width of 30.

## Running tests

Execute the tests with:

``````\$ mix test
``````

### Pending tests

In the test suites, all but the first test have been skipped.

Once you get a test passing, you can unskip the next one by commenting out the relevant `@tag :pending` with a `#` symbol.

For example:

``````# @tag :pending
test "shouting" do
assert Bob.hey("WATCH OUT!") == "Whoa, chill out!"
end
``````

Or, you can enable all the tests by commenting out the `ExUnit.configure` line in the test suite.

``````# ExUnit.configure exclude: :pending, trace: true
``````

If you're stuck on something, it may help to look at some of the available resources out there where answers might be found.

## Submitting Incomplete Solutions

It's possible to submit an incomplete solution so you can see how others have completed the exercise.

### test_helper.exs

``````ExUnit.start()
ExUnit.configure(exclude: :pending, trace: true)``````

### tournament_test.exs

``````defmodule TournamentTest do
use ExUnit.Case

# @tag :pending
test "typical input" do
input = [
"Devastating Donkeys;Courageous Californians;draw",
]

expected =
"""
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
"""
|> String.trim()

assert Tournament.tally(input) == expected
end

@tag :pending
test "incomplete competition (not all pairs have played)" do
input = [
]

expected =
"""
Team                           | MP |  W |  D |  L |  P
Allegoric Alaskans             |  3 |  2 |  0 |  1 |  6
Blithering Badgers             |  2 |  1 |  1 |  0 |  4
Courageous Californians        |  2 |  0 |  1 |  1 |  1
Devastating Donkeys            |  1 |  0 |  0 |  1 |  0
"""
|> String.trim()

assert Tournament.tally(input) == expected
end

@tag :pending
test "ties broken alphabetically" do
input = [
"Courageous Californians;Devastating Donkeys;win",
]

expected =
"""
Team                           | MP |  W |  D |  L |  P
Allegoric Alaskans             |  3 |  2 |  1 |  0 |  7
Courageous Californians        |  3 |  2 |  1 |  0 |  7
Blithering Badgers             |  3 |  0 |  1 |  2 |  1
Devastating Donkeys            |  3 |  0 |  1 |  2 |  1
"""
|> String.trim()

assert Tournament.tally(input) == expected
end

@tag :pending
test "mostly invalid lines" do
# Invalid input lines in an otherwise-valid game still results in valid
# output.
input = [
"",
"Devastating Donkeys;Courageous Californians;win;5",
]

expected =
"""
Team                           | MP |  W |  D |  L |  P
Devastating Donkeys            |  1 |  1 |  0 |  0 |  3
Blithering Badgers             |  1 |  0 |  0 |  1 |  0
"""
|> String.trim()

assert Tournament.tally(input) == expected
end
end``````
``````defmodule Tournament do
@doc """
Given `input` lines representing two teams and whether the first of them won,
lost, or reached a draw, separated by semicolons, calculate the statistics
for each team's number of games played, won, drawn, lost, and total points
for the season, and return a nicely-formatted string table.

A win earns a team 3 points, a draw earns 1 point, and a loss earns nothing.

Order the outcome by most total points for the season, and settle ties by
listing the teams in alphabetical order.
"""
@spec tally(input :: list(String.t())) :: String.t()
def tally(input) do
input
|> process_input()
|> calculate_stats()
|> print_table()
end

defp process_input(input) do
Enum.reduce(input, %{}, fn line, score ->
case Regex.run(~r/^([^;]*);([^;]*);(win|draw|loss)\$/, line) do
nil -> score
[_, team1, team2, result] -> score_match(team1, team2, result, score)
end
end)
end

defp score_match(team1, team2, outcome, score) do
score =
score
|> Map.put_new(team1, [])
|> Map.put_new(team2, [])

case outcome do
"win" ->
score
|> Map.update!(team1, &[:win | &1])
|> Map.update!(team2, &[:loss | &1])

"draw" ->
score
|> Map.update!(team1, &[:draw | &1])
|> Map.update!(team2, &[:draw | &1])

"loss" ->
score
|> Map.update!(team1, &[:loss | &1])
|> Map.update!(team2, &[:win | &1])
end
end

defp calculate_stats(score) do
score
|> Enum.map(fn {team, matches} ->
stats = %{
played: Enum.count(matches),
win: Enum.count(matches, &(&1 == :win)),
draw: Enum.count(matches, &(&1 == :draw)),
loss: Enum.count(matches, &(&1 == :loss)),
points: calculate_points(matches)
}

{team, stats}
end)
|> Enum.into(%{})
end

defp calculate_points(matches) do
Enum.reduce(matches, 0, fn
:win, points -> points + 3
:draw, points -> points + 1
:loss, points -> points
end)
end

defp print_table(stats) do
teams = Map.keys(stats)
teams = Enum.sort_by(teams, &(stats[&1][:points] * -1))

headers = ~w(Team MP W D L P)

rows =
Enum.map(teams, fn team ->
%{played: played, win: win, draw: draw, loss: loss, points: points} = stats[team]
[team, played, win, draw, loss, points]
end)

|> Enum.map(fn row ->
row
|> Enum.with_index()
|> Enum.map(fn {row, index} ->
case index do
0 -> String.pad_trailing(row, 30, " ")
end
end)
|> Enum.join(" | ")
end)
|> Enum.join("\n")
end
end``````