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 +7 -0
- data/bin/cool_soccer +21 -0
- data/lib/cool_soccer/soccer_helpers.rb +172 -0
- data/lib/cool_soccer.rb +54 -0
- metadata +96 -0
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
|
data/lib/cool_soccer.rb
ADDED
@@ -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: []
|