diff --git a/python/tournament/.exercism/metadata.json b/python/tournament/.exercism/metadata.json new file mode 100644 index 0000000..3e81a65 --- /dev/null +++ b/python/tournament/.exercism/metadata.json @@ -0,0 +1 @@ +{"track":"python","exercise":"tournament","id":"9d29ba55dcb24a2599bd0feab54e2300","url":"https://exercism.io/my/solutions/9d29ba55dcb24a2599bd0feab54e2300","handle":"DmitryKokorin","is_requester":true,"auto_approve":false} \ No newline at end of file diff --git a/python/tournament/README.md b/python/tournament/README.md new file mode 100644 index 0000000..0a11cb7 --- /dev/null +++ b/python/tournament/README.md @@ -0,0 +1,108 @@ +# Tournament + +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: + +```text +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: + +```text +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 + +```text +Allegoric Alaskans;Blithering Badgers;win +``` + +Means that the Allegoric Alaskans beat the Blithering Badgers. + +This line: + +```text +Courageous Californians;Blithering Badgers;loss +``` + +Means that the Blithering Badgers beat the Courageous Californians. + +And this line: + +```text +Devastating Donkeys;Courageous Californians;draw +``` + +Means that the Devastating Donkeys and Courageous Californians tied. + + +## Exception messages + +Sometimes it is necessary to raise an exception. When you do this, you should include a meaningful error message to +indicate what the source of the error is. This makes your code more readable and helps significantly with debugging. Not +every exercise will require you to raise an exception, but for those that do, the tests will only pass if you include +a message. + +To raise a message with an exception, just write it as an argument to the exception type. For example, instead of +`raise Exception`, you should write: + +```python +raise Exception("Meaningful message indicating the source of the error") +``` + +## Running the tests + +To run the tests, run `pytest tournament_test.py` + +Alternatively, you can tell Python to run the pytest module: +`python -m pytest tournament_test.py` + +### Common `pytest` options + +- `-v` : enable verbose output +- `-x` : stop running tests on first failure +- `--ff` : run failures from previous test before running other test cases + +For other options, see `python -m pytest -h` + +## Submitting Exercises + +Note that, when trying to submit an exercise, make sure the solution is in the `$EXERCISM_WORKSPACE/python/tournament` directory. + +You can find your Exercism workspace by running `exercism debug` and looking for the line that starts with `Workspace`. + +For more detailed information about running tests, code style and linting, +please see [Running the Tests](http://exercism.io/tracks/python/tests). + +## Submitting Incomplete Solutions + +It's possible to submit an incomplete solution so you can see how others have completed the exercise. diff --git a/python/tournament/tournament.py b/python/tournament/tournament.py new file mode 100644 index 0000000..e452c33 --- /dev/null +++ b/python/tournament/tournament.py @@ -0,0 +1,51 @@ +from collections import defaultdict + +SCORES = {'win': 3, 'draw': 1, 'loss': 0} +GUEST_RESULT = {'win': 'loss', 'draw': 'draw', 'loss': 'win'} +FORMAT = '{0:<30} | {1:>2} | {2:>2} | {3:>2} | {4:>2} | {5:>2}' + + +class TeamStats: + def __init__(self): + self.win = 0 + self.draw = 0 + self.loss = 0 + self.__points = None + + def add_game(self, result): + setattr(self, result, getattr(self, result) + 1) + self.__points = None + + def played(self): + return sum(getattr(self, x) for x in SCORES.keys()) + + def points(self): + + if self.__points is None: + self.__points = sum(SCORES[x]*getattr(self, x) + for x in SCORES.keys()) + + return self.__points + + +def tally(rows): + + table = [FORMAT.format('Team', 'MP', 'W', 'D', 'L', 'P')] + stats = defaultdict(TeamStats) + + for team_stats in rows: + home_team, guest_team, result = team_stats.split(';') + + if result not in SCORES.keys(): + raise ValueError("Invalid game result") + + stats[home_team].add_game(result) + stats[guest_team].add_game(GUEST_RESULT[result]) + + for team, team_stats in sorted(stats.items(), + key=lambda x: (-x[1].points(), x[0])): + table.append(FORMAT.format(team, team_stats.played(), team_stats.win, + team_stats.draw, team_stats.loss, + team_stats.points())) + + return table diff --git a/python/tournament/tournament_test.py b/python/tournament/tournament_test.py new file mode 100644 index 0000000..4d0a092 --- /dev/null +++ b/python/tournament/tournament_test.py @@ -0,0 +1,142 @@ +import unittest + +from tournament import tally + +# Tests adapted from `problem-specifications//canonical-data.json` + + +class TournamentTest(unittest.TestCase): + def test_just_the_header_if_no_input(self): + results = [] + table = ["Team | MP | W | D | L | P"] + self.assertEqual(tally(results), table) + + def test_a_win_is_three_points_a_loss_is_zero_points(self): + results = ["Allegoric Alaskans;Blithering Badgers;win"] + table = [ + "Team | MP | W | D | L | P", + "Allegoric Alaskans | 1 | 1 | 0 | 0 | 3", + "Blithering Badgers | 1 | 0 | 0 | 1 | 0", + ] + self.assertEqual(tally(results), table) + + def test_a_win_can_also_be_expressed_as_a_loss(self): + results = ["Blithering Badgers;Allegoric Alaskans;loss"] + table = [ + "Team | MP | W | D | L | P", + "Allegoric Alaskans | 1 | 1 | 0 | 0 | 3", + "Blithering Badgers | 1 | 0 | 0 | 1 | 0", + ] + self.assertEqual(tally(results), table) + + def test_a_different_team_can_win(self): + results = ["Blithering Badgers;Allegoric Alaskans;win"] + table = [ + "Team | MP | W | D | L | P", + "Blithering Badgers | 1 | 1 | 0 | 0 | 3", + "Allegoric Alaskans | 1 | 0 | 0 | 1 | 0", + ] + self.assertEqual(tally(results), table) + + def test_a_draw_is_one_point_each(self): + results = ["Allegoric Alaskans;Blithering Badgers;draw"] + table = [ + "Team | MP | W | D | L | P", + "Allegoric Alaskans | 1 | 0 | 1 | 0 | 1", + "Blithering Badgers | 1 | 0 | 1 | 0 | 1", + ] + self.assertEqual(tally(results), table) + + def test_there_can_be_more_than_one_match(self): + results = [ + "Allegoric Alaskans;Blithering Badgers;win", + "Allegoric Alaskans;Blithering Badgers;win", + ] + table = [ + "Team | MP | W | D | L | P", + "Allegoric Alaskans | 2 | 2 | 0 | 0 | 6", + "Blithering Badgers | 2 | 0 | 0 | 2 | 0", + ] + self.assertEqual(tally(results), table) + + def test_there_can_be_more_than_one_winner(self): + results = [ + "Allegoric Alaskans;Blithering Badgers;loss", + "Allegoric Alaskans;Blithering Badgers;win", + ] + table = [ + "Team | MP | W | D | L | P", + "Allegoric Alaskans | 2 | 1 | 0 | 1 | 3", + "Blithering Badgers | 2 | 1 | 0 | 1 | 3", + ] + self.assertEqual(tally(results), table) + + def test_there_can_be_more_than_two_teams(self): + results = [ + "Allegoric Alaskans;Blithering Badgers;win", + "Blithering Badgers;Courageous Californians;win", + "Courageous Californians;Allegoric Alaskans;loss", + ] + table = [ + "Team | MP | W | D | L | P", + "Allegoric Alaskans | 2 | 2 | 0 | 0 | 6", + "Blithering Badgers | 2 | 1 | 0 | 1 | 3", + "Courageous Californians | 2 | 0 | 0 | 2 | 0", + ] + self.assertEqual(tally(results), table) + + def test_typical_input(self): + results = [ + "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", + ] + table = [ + "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", + ] + self.assertEqual(tally(results), table) + + def test_incomplete_competition_not_all_pairs_have_played(self): + results = [ + "Allegoric Alaskans;Blithering Badgers;loss", + "Devastating Donkeys;Allegoric Alaskans;loss", + "Courageous Californians;Blithering Badgers;draw", + "Allegoric Alaskans;Courageous Californians;win", + ] + table = [ + "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", + ] + self.assertEqual(tally(results), table) + + def test_ties_broken_alphabetically(self): + results = [ + "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", + ] + table = [ + "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", + ] + self.assertEqual(tally(results), table) + + +if __name__ == "__main__": + unittest.main()