Avatar of davearonson

davearonson's solution

to Tournament in the Elixir Track

Published at Jul 13 2018 · 0 comments
Instructions
Test suite
Solution

Note:

This solution was written on an old version of Exercism. The tests below might not correspond to the solution code, and the exercise may have changed since this code was written.

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

Your tallying program will receive input that looks like:

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

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

Allegoric Alaskans;Blithering Badgers;win

Means that the Allegoric Alaskans beat the Blithering Badgers.

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:

$ elixir tournament_test.exs

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

For more detailed information about the Elixir track, please see the help page.

Submitting Incomplete Solutions

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

tournament_test.exs

if !System.get_env("EXERCISM_TEST_EXAMPLES") do
  Code.load_file("tournament.exs", __DIR__)
end

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

defmodule TournamentTest do
  use ExUnit.Case

  # @tag :pending
  test "typical input" do
    input = [
      "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"
    ]

    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 = [
      "Allegoric Alaskans;Blithering Badgers;loss",
      "Devastating Donkeys;Allegoric Alaskans;loss",
      "Courageous Californians;Blithering Badgers;draw",
      "Allegoric Alaskans;Courageous Californians;win"
    ]

    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",
      "Allegoric Alaskans;Blithering Badgers;win",
      "Devastating Donkeys;Allegoric Alaskans;loss",
      "Courageous Californians;Blithering Badgers;win",
      "Blithering Badgers;Devastating Donkeys;draw",
      "Allegoric Alaskans;Courageous Californians;draw"
    ]

    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 = [
      "",
      "Allegoric Alaskans@Blithering Badgers;draw",
      "Blithering Badgers;Devastating Donkeys;loss",
      "Devastating Donkeys;Courageous Californians;win;5",
      "Courageous Californians;Allegoric Alaskans;los"
    ]

    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
    do_tally(input, %{})
    |> sort_teams
    |> format_results
  end

  defp do_tally([head|tail], acc) do
    do_tally(tail, tally_game(head |> String.split(";"), acc))
  end
  defp do_tally([], acc), do: acc

  defp tally_game([t1, t2, "draw"], acc) do
    acc |> record(t1, "draw") |> record(t2, "draw")
  end
  defp tally_game([t1, t2, "win" ], acc) do
    acc |> record(t1, "win") |> record(t2, "loss")
  end
  defp tally_game([t1, t2, "loss"], acc), do: tally_game([t2, t1, "win"], acc)
  defp tally_game(_anything_else, acc), do: acc

  defp record(acc, team, result) do
    with(old_stats <- get_team_stats(team, acc),
         new_stats <- update_old_stats(old_stats, result)) do
      acc |> Map.put(team, new_stats)
    end
  end

  defp get_team_stats(team, acc) do
    Map.get(acc, team, %{"matches" => 0,
                         "win"     => 0,
                         "draw"    => 0,
                         "loss"    => 0})
  end

  defp update_old_stats(old_stats, result) do
    %{old_stats | "matches" => old_stats["matches"] + 1,
                  result    => old_stats[result] + 1}
  end

  defp sort_teams(teams) do
    teams
    |> Enum.map(&assign_points_and_names/1)
    # note: at this point it's no longer %{nameN => statsN}, but
    # [stats1, stats2, etc.] w/ names and point-totals inside.
    |> Enum.sort(&compare_teams/2)
  end

  @points %{"win" => 3, "draw" => 1}
  defp assign_points_and_names({name, stats}) do
    stats
    |> Map.put("name", name)
    |> Map.put("points", stats["win"] * @points["win"] +
                         stats["draw"] * @points["draw"])
  end

  defp compare_teams(%{"name" => n1, "points"=> p1},
                     %{"name" => n2, "points"=> p2}) do
    cond do
      p1 > p2 -> true
      p1 < p2 -> false
      true    -> n1 < n2
    end
  end

  defp format_results(all_stats) do
    "Team#{String.duplicate(" ", 27)}| MP |  W |  D |  L |  P\n" <>
      (all_stats |> Enum.map(&format_line/1) |> Enum.join("\n"))
  end

  defp format_line(stats) do
    name = stats["name"] |> String.pad_trailing(30)
    matches = stats["matches"] |> pad_num_to(2)
    win = stats["win"] |> pad_num_to(2)
    draw = stats["draw"] |> pad_num_to(2)
    loss = stats["loss"] |> pad_num_to(2)
    points = stats["points"] |> pad_num_to(2)
    "#{name} | #{matches} | #{win} | #{draw} | #{loss} | #{points}"
  end

  defp pad_num_to(num, size) do
    num |> Integer.to_string |> String.pad_leading(size)
  end

end

Community comments

Find this solution interesting? Ask the author a question to learn more.

What can you learn from this solution?

A huge amount can be learned from reading other people’s code. This is why we wanted to give exercism users the option of making their solutions public.

Here are some questions to help you reflect on this solution and learn the most from it.

  • What compromises have been made?
  • Are there new concepts here that you could read more about to improve your understanding?