cool_soccer 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1b3fd94f126775096201987af7f36fb3d66d9edb50e514b536c1af8d92cd5d68
4
+ data.tar.gz: ec3b056a6abe39c7f130d9d306b4e50e031e82b9c0c39ecc0c686cd5eb2d0019
5
+ SHA512:
6
+ metadata.gz: 3854a4121220bc1b298bd1747801a5d087d780a2d37321bb1f9786bc0fd47be11d82faae40baa4288c50945359255197e2e99f09d12859bdcd14b0fa3bb7e547
7
+ data.tar.gz: f0725e89a483f9ac95819eb1a32cff5e5b6864caabbb7c35411ff0c40d537c6974d764ce842a539240f540588f3d10dfc35d8a8b471cdeed924fcd4361aaebe4
data/bin/cool_soccer ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/cool_soccer'
5
+
6
+ app = CoolSoccer.new
7
+
8
+ if !ARGV.empty?
9
+ file_path = ARGV[0]
10
+ File.foreach(file_path) do |line|
11
+ app.execute(line: line)
12
+ end
13
+ else
14
+ $stdin.each_line do |line|
15
+ app.execute(line: line)
16
+ end
17
+
18
+ # If we haven't reset the games_today to 0, we stopped before the end of a match day,
19
+ # so we need to print out the current top three
20
+ end
21
+ puts app.format_match_day(day: app.match_day, leaderboard: app.leaderboard) if app.games_today.positive?
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SoccerHelpers is a module that provides a "functional core" to the program,
4
+ # and allows us to answer discrete questions about the input/output of the program.
5
+ module SoccerHelpers
6
+ # valid_game_result? answers the question: "Is a particular string a valid game result?"
7
+ #
8
+ # Valid game results consist of:
9
+ # Two team results, delimited by a comma, where each team result consists of:
10
+ # a team name (only alphabetic characters), and a score (only numeric characters).
11
+ #
12
+ # Game results are invalid if they only have one team, if they're missing a comma,
13
+ # if they have missing scores, or if they have missing team names.
14
+ #
15
+ # For example:
16
+ # Valid: "San Jose Earthquakes 3, Santa Cruz Slugs 3",
17
+ # Invalid: "San Jose Earthquakes 3", "San Jose Earthquakes, Santa Cruz Slugs",
18
+ #
19
+ # @param result [String] the string to validate
20
+ # @return [Boolean] is the input string a valid game result string?
21
+ def valid_game_result?(result:)
22
+ # Make sure this string has two parts, delimited by a comment
23
+ parts = result.split(',')
24
+ return false if parts.length != 2
25
+
26
+ # For each part, make sure it contains some numeric value, the value is an integer,
27
+ # and we have a team name that consists of alphabetic characters.
28
+ parts.each do |part|
29
+ return false if part.count('0-9').zero?
30
+ return false if part.count('a-zA-Z').zero?
31
+ end
32
+
33
+ # If we have passed all of the above checks, then the string is a valid game result
34
+ true
35
+ end
36
+
37
+ # extract_team_and_score answers the question:
38
+ # "In this result string, what teams are included, and what did they score?"
39
+ # The response is a hash in the form:
40
+ # { 'San Jose Earthquakes' => 1, 'Santa Cruz Slugs' => 1 }
41
+ #
42
+ # As per the prompt, draws are worth one point,
43
+ # wins are worth 3 points, and losses are worth 0 points.
44
+ #
45
+ # @param result [String] the string to parse
46
+ # @return [Hash] a hash of team names and scores
47
+ def extract_team_and_score(result:)
48
+ # Split the string into two parts, separated by a comma
49
+ parts = result.split(',')
50
+
51
+ # Team names are everything in a given part,
52
+ # excluding any numeric characters.
53
+ # We preserve the whitespace between words,
54
+ # and we remove any leading or trailing whitespace.
55
+ team_names = parts.map { |part| part.gsub(/[^a-zA-Z ]/, '').strip }
56
+
57
+ # Scores are everything in a given part,
58
+ # excluding any non-numeric characters.
59
+ scores = parts.map { |part| part.gsub(/[^0-9]/, '').to_i }
60
+
61
+ # Compare the scores, and assign the win/loss/draw points.
62
+ scores = if scores[0] == scores[1]
63
+ [1, 1]
64
+ elsif scores[0] > scores[1]
65
+ [3, 0]
66
+ else
67
+ [0, 3]
68
+ end
69
+
70
+ # Zip the team names and scores together into a hash
71
+ Hash[team_names.zip(scores)]
72
+ end
73
+
74
+ # top_three answers the question: "What are the top three teams in the league?"
75
+ # It takes a hash of team names and scores, and returns the top three teams.
76
+ # If there are any ties in the top three, we return the tied teams in alphabetical order.
77
+ #
78
+ # param scores [Hash] a hash of team names and scores
79
+ # @return [Array] an array of the top three teams, in the format [team, score]
80
+ def top_three(scores:)
81
+ # Alphabetize by team name
82
+ scores = scores.sort_by { |name, _| name }.reverse
83
+
84
+ # Now we need to find the top three teams by score
85
+ sorted_scores = scores.sort_by { |_, score| score }.reverse
86
+
87
+ # Return only the top three
88
+ sorted_scores[0..2]
89
+ end
90
+
91
+ # update_leaderboard answers the question: "What is the new leaderboard, given a game result?"
92
+ # It takes a current leaderboard, and a game result as hashes.
93
+ # Then it updates the leaderboard - either adding a team's score if we haven't seen them,
94
+ # or incrementing a team's score if we have seen them.
95
+ #
96
+ # param leaderboard [Hash] the current leaderboard
97
+ # param changes [Hash] the changes to make to the leaderboard
98
+ # @return [Hash] the updated leaderboard
99
+ def update_leaderboard(leaderboard:, changes:)
100
+ # Make a copy of the leaderboard
101
+ new_leaderboard = leaderboard.clone || {}
102
+
103
+ # Loop through the changes, and update the leaderboard with the changes
104
+ changes.each do |team, score|
105
+ if new_leaderboard.key?(team)
106
+ new_leaderboard[team] += score
107
+ else
108
+ new_leaderboard[team] = score
109
+ end
110
+ end
111
+
112
+ # Return the updated leaderboard
113
+ new_leaderboard
114
+ end
115
+
116
+ # format_match_day answers the question of: "How would we format the match day results?"
117
+ # It takes an integer representing the current match day,
118
+ # and a hash of team names and scores that represent the current leaderboard.
119
+ # It returns a string in the format of expected-output.txt
120
+ #
121
+ # param day [Integer] the day number of the recently concluded day
122
+ # param leaderboard [Hash] the current leaderboard
123
+ # @return [String] the formatted leaderboard
124
+ def format_match_day(day:, leaderboard:)
125
+ # Get the top three from the leaderboard
126
+ leaders = top_three(scores: leaderboard)
127
+
128
+ # Format the output
129
+ output = "Matchday #{day}\n"
130
+ leaders.each do |leader|
131
+ # If it's just one point, we use `pt`, otherwise, we use `pts`
132
+ ordinal_points = leader[1] == 1 ? 'pt' : 'pts'
133
+ output += "#{leader[0]}, #{leader[1]} #{ordinal_points}\n"
134
+ end
135
+
136
+ # Return the output
137
+ output
138
+ end
139
+
140
+ # match_day_over answers the question: "Is the match day over?"
141
+ # There are two scenarios where the match day is over:
142
+ # 1. It is match day 1, and the leaderboard already has a key that matches one of the input scores teams
143
+ # 2. It is any match day past 1, and the number of games today is equal to num_teams / 2
144
+ #
145
+ # This function takes a lot of parameters, which I don't love,
146
+ # but I prefer to call one function, rather than have two similar functions,
147
+ # (one for match day 1, and one for match day 2 and beyond).
148
+ #
149
+ # Using named params helps make the long parameter list manageable:
150
+ # callers will get specific error messages if they pass in the wrong params.
151
+ #
152
+ # param leaderboard [Hash] the current leaderboard
153
+ # param scores [Hash] the scores of the current match day
154
+ # param match_day [Integer] the current match day
155
+ # param num_teams [Integer] the number of teams in the league
156
+ # param games_today [Integer] the number of games played today
157
+ # @return [Boolean] is the current match day over?
158
+ def match_day_over?(leaderboard:, scores:, match_day:, num_teams:, games_today:)
159
+ # If this is the first match day, and the leaderboard already has a key that matches one of the input scores teams,
160
+ # then the match day is over
161
+ return true if match_day == 1 && leaderboard.key?(scores.keys.first)
162
+
163
+ return true if match_day == 1 && leaderboard.key?(scores.keys.last)
164
+
165
+ # If this is any match day past 1, and the number of games today is equal to num_leagues / 2,
166
+ # then the match day is over
167
+ return true if match_day > 1 && games_today == num_teams / 2
168
+
169
+ # Otherwise, the match day is not over yet.
170
+ false
171
+ end
172
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'cool_soccer/soccer_helpers'
4
+
5
+ # CoolSoccer is an "imperative shell"
6
+ # that allows the CLI to read a stream of game results from a soccer league,
7
+ # and return the top teams at the end of each matchday.
8
+ class CoolSoccer
9
+ include SoccerHelpers
10
+ attr_accessor :leaderboard, :match_day, :num_teams, :games_today
11
+
12
+ # When this class is instantiated, we want to provide it with some default instance variables.
13
+ def initialize
14
+ # First, let's keep track of a leaderboard, and initialize it as an empty hash.
15
+ @leaderboard = {}
16
+ # We'll also keep track of the current match day, initialized at 1
17
+ @match_day = 1
18
+ # And we'll want to know how many teams are in the league. At first,
19
+ # we'll initialize this at 0.
20
+ @num_teams = 0
21
+ # Finally, we'll want to know how many games we've seen today,
22
+ # which we'll initialize at 0.
23
+ @games_today = 0
24
+ end
25
+
26
+ def execute(line:)
27
+ # If the line is invalid, skip it
28
+ return unless valid_game_result?(result: line)
29
+
30
+ # Increment the number of games we've played today
31
+ @games_today += 1
32
+
33
+ # If it's still match day 1, increment the number of teams by 2
34
+ @num_teams += 2 if @match_day == 1
35
+
36
+ # Extract the team names and scores from the line
37
+ scores = extract_team_and_score(result: line)
38
+
39
+ # Check if the match day is over
40
+ if match_day_over?(leaderboard: @leaderboard, scores: scores, match_day: @match_day, num_teams: @num_teams,
41
+ games_today: @games_today)
42
+
43
+ # Print out the current top three
44
+ puts "#{format_match_day(day: @match_day, leaderboard: @leaderboard)}\n"
45
+
46
+ # If so, increment the match day and reset the games today counter
47
+ @match_day += 1
48
+ @games_today = 1
49
+ end
50
+
51
+ # Add the scores to the leaderboard
52
+ @leaderboard = update_leaderboard(leaderboard: @leaderboard, changes: scores)
53
+ end
54
+ end
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cool_soccer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Tyler Williams
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-05-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3.4'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3.4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rubocop
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.29'
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 1.29.1
37
+ type: :development
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: '1.29'
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 1.29.1
47
+ - !ruby/object:Gem::Dependency
48
+ name: yard
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: 0.9.27
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: 0.9.27
61
+ description: |2
62
+ A command-line application that reads a listing of game results for a soccer league as a stream,
63
+ and returns the top teams at the end of each matchday.
64
+ email: tyler@coolsoftware.dev
65
+ executables:
66
+ - cool_soccer
67
+ extensions: []
68
+ extra_rdoc_files: []
69
+ files:
70
+ - bin/cool_soccer
71
+ - lib/cool_soccer.rb
72
+ - lib/cool_soccer/soccer_helpers.rb
73
+ homepage: https://rubygems.org/gems/cool_soccer
74
+ licenses:
75
+ - MIT
76
+ metadata: {}
77
+ post_install_message:
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '2.5'
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubygems_version: 3.2.22
93
+ signing_key:
94
+ specification_version: 4
95
+ summary: Tyler Williams Jane Technologies Coding Challenge
96
+ test_files: []