sports 0.0.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.
@@ -0,0 +1,264 @@
1
+ ##########
2
+ # todo/fix:
3
+ ## reuse standings helper/calculator from sportdb
4
+ ## do NOT duplicate
5
+
6
+
7
+ module Sports
8
+
9
+
10
+ class Standings
11
+
12
+ class StandingsLine ## nested class StandinsLine - todo/fix: change to Line - why? why not?
13
+ attr_accessor :rank, :team,
14
+ :played, :won, :lost, :drawn, ## -- total
15
+ :goals_for, :goals_against, :pts,
16
+ :home_played, :home_won, :home_lost, :home_drawn, ## -- home
17
+ :home_goals_for, :home_goals_against, :home_pts,
18
+ :away_played, :away_won, :away_lost, :away_drawn, ## -- away
19
+ :away_goals_for, :away_goals_against, :away_pts
20
+
21
+ alias_method :team_name, :team ## note: team for now always a string
22
+ alias_method :pos, :rank ## rename back to use pos instead of rank - why? why not?
23
+
24
+
25
+ def initialize( team )
26
+ @rank = nil # use 0? why? why not?
27
+ ## change rank back to pos - why? why not?
28
+ @team = team
29
+ @played = @home_played = @away_played = 0
30
+ @won = @home_won = @away_won = 0
31
+ @lost = @home_lost = @away_lost = 0
32
+ @drawn = @home_drawn = @away_drawn = 0
33
+ @goals_for = @home_goals_for = @away_goals_for = 0
34
+ @goals_against = @home_goals_against = @away_goals_against = 0
35
+ @pts = @home_pts = @away_pts = 0
36
+ end
37
+ end # (nested) class StandingsLine
38
+
39
+
40
+ def initialize( opts={} )
41
+ ## fix:
42
+ # passing in e.g. pts for win (3? 2? etc.)
43
+ # default to 3 for now
44
+
45
+ ## lets you pass in 2 as an alterantive, for example
46
+ @pts_won = opts[:pts_won] || 3
47
+
48
+ @lines = {} # StandingsLines cached by team name/key
49
+ end
50
+
51
+
52
+ def update( match_or_matches )
53
+ ## convenience - update all matches at once
54
+ ## todo/check: check for ActiveRecord_Associations_CollectionProxy and than use to_a (to "force" array) - why? why not?
55
+ matches = if match_or_matches.is_a?(Array)
56
+ match_or_matches
57
+ else
58
+ [match_or_matches]
59
+ end
60
+
61
+ matches.each_with_index do |match,i| # note: index(i) starts w/ zero (0)
62
+ update_match( match )
63
+ end
64
+ self # note: return self to allow chaining
65
+ end
66
+
67
+
68
+ def to_a
69
+ ## return lines; sort and add rank
70
+ ## note: will update rank!!!! (side effect)
71
+
72
+ #############################
73
+ ### calc ranking position (rank)
74
+ ## fix/allow same rank e.g. all 1 or more than one team 3rd etc.
75
+
76
+ # build array from hash
77
+ ary = []
78
+ @lines.each do |k,v|
79
+ ary << v
80
+ end
81
+
82
+ ary.sort! do |l,r|
83
+ ## note: reverse order (thus, change l,r to r,l)
84
+ value = r.pts <=> l.pts
85
+ if value == 0 # same pts try goal diff
86
+ value = (r.goals_for-r.goals_against) <=> (l.goals_for-l.goals_against)
87
+ if value == 0 # same goal diff too; try assume more goals better for now
88
+ value = r.goals_for <=> l.goals_for
89
+ end
90
+ end
91
+ value
92
+ end
93
+
94
+ ## update rank using ordered array
95
+ ary.each_with_index do |line,i|
96
+ line.rank = i+1 ## add ranking (e.g. 1,2,3 etc.) - note: i starts w/ zero (0)
97
+ end
98
+
99
+ ary
100
+ end # to_a
101
+
102
+
103
+
104
+ #####
105
+ ###
106
+ ## fix: move build to StandingsPart/Report !!!!
107
+ def build( source: nil ) ## build / pretty print standings table in string buffer
108
+ ## keep pretty printer in struct - why? why not?
109
+
110
+
111
+ ## add standings table in markdown to buffer (buf)
112
+
113
+ ## todo: use different styles/formats (simple/ etc ???)
114
+
115
+ ## simple table (only totals - no home/away)
116
+ ## standings.to_a.each do |l|
117
+ ## buf << '%2d. ' % l.rank
118
+ ## buf << '%-28s ' % l.team
119
+ ## buf << '%2d ' % l.played
120
+ ## buf << '%3d ' % l.won
121
+ ## buf << '%3d ' % l.drawn
122
+ ## buf << '%3d ' % l.lost
123
+ ## buf << '%3d:%-3d ' % [l.goals_for,l.goals_against]
124
+ ## buf << '%3d' % l.pts
125
+ ## buf << "\n"
126
+ ## end
127
+
128
+ buf = ''
129
+ buf << "\n"
130
+ buf << "```\n"
131
+ buf << " - Home - - Away - - Total -\n"
132
+ buf << " Pld W D L F:A W D L F:A F:A +/- Pts\n"
133
+
134
+ to_a.each do |l|
135
+ buf << '%2d. ' % l.rank
136
+ buf << '%-28s ' % l.team
137
+ buf << '%2d ' % l.played
138
+
139
+ buf << '%2d ' % l.home_won
140
+ buf << '%2d ' % l.home_drawn
141
+ buf << '%2d ' % l.home_lost
142
+ buf << '%3d:%-3d ' % [l.home_goals_for,l.home_goals_against]
143
+
144
+ buf << '%2d ' % l.away_won
145
+ buf << '%2d ' % l.away_drawn
146
+ buf << '%2d ' % l.away_lost
147
+ buf << '%3d:%-3d ' % [l.away_goals_for,l.away_goals_against]
148
+
149
+ buf << '%3d:%-3d ' % [l.goals_for,l.goals_against]
150
+
151
+ goals_diff = l.goals_for-l.goals_against
152
+ if goals_diff > 0
153
+ buf << '%3s ' % "+#{goals_diff}"
154
+ elsif goals_diff < 0
155
+ buf << '%3s ' % "#{goals_diff}"
156
+ else ## assume 0
157
+ buf << ' '
158
+ end
159
+
160
+ buf << '%3d' % l.pts
161
+ buf << "\n"
162
+ end
163
+
164
+ buf << "```\n"
165
+ buf << "\n"
166
+
167
+ ## optinal: add data source if known / present
168
+ ## assume (relative) markdown link for now in README.md
169
+ if source
170
+ buf << "(Source: [`#{source}`](#{source}))\n"
171
+ buf << "\n"
172
+ end
173
+
174
+ buf
175
+ end
176
+
177
+
178
+ private
179
+ def update_match( m ) ## add a match
180
+
181
+ ## note: always use team as string for now
182
+ ## for now allow passing in of string OR struct - why? why not?
183
+ ## todo/fix: change to m.team1_name and m.team2_name - why? why not?
184
+ team1 = m.team1.is_a?( String ) ? m.team1 : m.team1.name
185
+ team2 = m.team2.is_a?( String ) ? m.team2 : m.team2.name
186
+
187
+ score = m.score_str
188
+
189
+ ## puts " #{team1} - #{team2} #{score}"
190
+
191
+ unless m.over?
192
+ puts " !!!! skipping match - not yet over (date in the future) => #{m.date}"
193
+ return
194
+ end
195
+
196
+ unless m.complete?
197
+ puts "!!! [calc_standings] skipping match #{team1} - #{team2} w/ past date #{m.date} - scores incomplete => #{score}"
198
+ return
199
+ end
200
+
201
+
202
+
203
+ line1 = @lines[ team1 ] || StandingsLine.new( team1 )
204
+ line2 = @lines[ team2 ] || StandingsLine.new( team2 )
205
+
206
+ line1.played += 1
207
+ line1.home_played += 1
208
+
209
+ line2.played += 1
210
+ line2.away_played += 1
211
+
212
+ if m.winner == 1
213
+ line1.won += 1
214
+ line1.home_won += 1
215
+
216
+ line2.lost += 1
217
+ line2.away_lost += 1
218
+
219
+ line1.pts += @pts_won
220
+ line1.home_pts += @pts_won
221
+ elsif m.winner == 2
222
+ line1.lost += 1
223
+ line1.home_lost += 1
224
+
225
+ line2.won += 1
226
+ line2.away_won += 1
227
+
228
+ line2.pts += @pts_won
229
+ line2.away_pts += @pts_won
230
+ else ## assume drawn/tie (that is, 0)
231
+ line1.drawn += 1
232
+ line1.home_drawn += 1
233
+
234
+ line2.drawn += 1
235
+ line2.away_drawn += 1
236
+
237
+ line1.pts += 1
238
+ line1.home_pts += 1
239
+ line2.pts += 1
240
+ line2.away_pts += 1
241
+ end
242
+
243
+ if m.score1 && m.score2
244
+ line1.goals_for += m.score1
245
+ line1.home_goals_for += m.score1
246
+ line1.goals_against += m.score2
247
+ line1.home_goals_against += m.score2
248
+
249
+ line2.goals_for += m.score2
250
+ line2.away_goals_for += m.score2
251
+ line2.goals_against += m.score1
252
+ line2.away_goals_against += m.score1
253
+ else
254
+ puts "*** warn: [standings] skipping match with missing scores: #{m.inspect}"
255
+ end
256
+
257
+ @lines[ team1 ] = line1
258
+ @lines[ team2 ] = line2
259
+ end # method update_match
260
+
261
+ end # class Standings
262
+
263
+
264
+ end # module Sports
@@ -0,0 +1,147 @@
1
+
2
+ module Sports
3
+
4
+ ##
5
+ ## todo/fix: remove self.create in structs!!! use just new!!!
6
+
7
+ class Team
8
+ ## todo: use just names for alt_names - why? why not?
9
+ attr_accessor :key, :name, :alt_names,
10
+ :code, ## code == abbreviation e.g. ARS etc.
11
+ :year, :year_end, ## todo/fix: change year to start_year and year_end to end_year (like in season)!!!
12
+ :country
13
+
14
+
15
+ def names
16
+ ## todo/check: add alt_names_auto too? - why? why not?
17
+ [@name] + @alt_names
18
+ end ## all names
19
+
20
+ def key
21
+ ## note: auto-generate key "on-the-fly" if missing for now - why? why not?
22
+ ## note: quick hack - auto-generate key, that is, remove all non-ascii chars and downcase
23
+ @key || @name.downcase.gsub( /[^a-z]/, '' )
24
+ end
25
+
26
+
27
+ ## special import only attribs
28
+ attr_accessor :alt_names_auto ## auto-generated alt names
29
+ attr_accessor :wikipedia # wikipedia page name (for english (en))
30
+
31
+
32
+ def historic?() @year_end ? true : false; end
33
+ alias_method :past?, :historic?
34
+
35
+ def wikipedia?() @wikipedia; end
36
+ def wikipedia_url
37
+ if @wikipedia
38
+ ## note: replace spaces with underscore (-)
39
+ ## e.g. Club Brugge KV => Club_Brugge_KV
40
+ ## todo/check/fix:
41
+ ## check if "plain" dash (-) needs to get replaced with typographic dash??
42
+ "https://en.wikipedia.org/wiki/#{@wikipedia.gsub(' ','_')}"
43
+ else
44
+ nil
45
+ end
46
+ end
47
+
48
+
49
+ def initialize( **kwargs )
50
+ @alt_names = []
51
+ @alt_names_auto = []
52
+
53
+ update( kwargs ) unless kwargs.empty?
54
+ end
55
+
56
+ def update( **kwargs )
57
+ @key = kwargs[:key] if kwargs.has_key? :key
58
+ @name = kwargs[:name] if kwargs.has_key? :name
59
+ @code = kwargs[:code] if kwargs.has_key? :code
60
+ @alt_names = kwargs[:alt_names] if kwargs.has_key? :alt_names
61
+ self ## note - MUST return self for chaining
62
+ end
63
+
64
+
65
+
66
+ ##############################
67
+ ## helper methods for import only??
68
+ ## check for duplicates
69
+ include NameHelper
70
+
71
+ def duplicates?
72
+ names = [name] + alt_names + alt_names_auto
73
+ names = names.map { |name| normalize( sanitize(name) ) }
74
+
75
+ names.size != names.uniq.size
76
+ end
77
+
78
+ def duplicates
79
+ names = [name] + alt_names + alt_names_auto
80
+
81
+ ## calculate (count) frequency and select if greater than one
82
+ names.reduce( {} ) do |h,name|
83
+ norm = normalize( sanitize(name) )
84
+ h[norm] ||= []
85
+ h[norm] << name; h
86
+ end.select { |norm,names| names.size > 1 }
87
+ end
88
+
89
+
90
+ def add_variants( name_or_names )
91
+ names = name_or_names.is_a?(Array) ? name_or_names : [name_or_names]
92
+ names.each do |name|
93
+ name = sanitize( name )
94
+ self.alt_names_auto += variants( name )
95
+ end
96
+ end
97
+ end # class Team
98
+
99
+
100
+
101
+ class NationalTeam < Team
102
+ def initialize( **kwargs )
103
+ super
104
+ end
105
+
106
+ def update( **kwargs )
107
+ super
108
+ self ## note - MUST return self for chaining
109
+ end
110
+
111
+ end # class NationalTeam
112
+
113
+
114
+ ########
115
+ # more attribs - todo/fix - also add "upstream" to struct & model!!!!!
116
+ # district, geos, year_end, country, etc.
117
+
118
+ class Club < Team
119
+ attr_accessor :ground
120
+
121
+ attr_accessor :a, :b
122
+ def a?() @a == nil; end ## is a (1st) team / club (i)? if a is NOT set
123
+ def b?() @a != nil; end ## is b (2nd/reserve/jr) team / club (ii) if a is set
124
+
125
+ ## note: delegate/forward all geo attributes for team b for now (to team a) - keep - why? why not?
126
+ attr_writer :city, :district, :geos
127
+ def city() @a == nil ? @city : @a.city; end
128
+ def district() @a == nil ? @district : @a.district; end
129
+ def country() @a == nil ? @country : @a.country; end
130
+ def geos() @a == nil ? @geos : @a.geos; end
131
+
132
+
133
+ def initialize( **kwargs )
134
+ super
135
+ end
136
+
137
+ def update( **kwargs )
138
+ super
139
+ @city = kwargs[:city] if kwargs.has_key? :city
140
+ ## todo/fix: use city struct - why? why not?
141
+ ## todo/fix: add country too or report unused keywords / attributes - why? why not?
142
+
143
+ self ## note - MUST return self for chaining
144
+ end
145
+ end # class Club
146
+
147
+ end # module Sports
@@ -0,0 +1,84 @@
1
+
2
+ module Sports
3
+
4
+
5
+ class TeamUsage
6
+
7
+ class TeamUsageLine ## nested class
8
+ attr_accessor :team,
9
+ :matches, ## number of matches (played),
10
+ :seasons, ## (optianl) array of seasons, use seasons.size for count
11
+ :levels ## (optional) hash of levels (holds mapping level to TeamUsageLine)
12
+
13
+ def initialize( team )
14
+ @team = team
15
+
16
+ @matches = 0
17
+ @seasons = []
18
+ @levels = {}
19
+ end
20
+ end # (nested) class TeamUsageLine
21
+
22
+
23
+
24
+ def initialize( opts={} )
25
+ @lines = {} # StandingsLines cached by team name/key
26
+ end
27
+
28
+
29
+ def update( matches, season: '?', level: nil )
30
+ ## convenience - update all matches at once
31
+ matches.each_with_index do |match,i| # note: index(i) starts w/ zero (0)
32
+ update_match( match, season: season, level: level )
33
+ end
34
+ self # note: return self to allow chaining
35
+ end
36
+
37
+ def to_a
38
+ ## return lines; sort
39
+
40
+ # build array from hash
41
+ ary = []
42
+ @lines.each do |k,v|
43
+ ary << v
44
+ end
45
+
46
+ ## for now sort just by name (a-z)
47
+ ary.sort! do |l,r|
48
+ ## note: reverse order (thus, change l,r to r,l)
49
+ l.team <=> r.team
50
+ end
51
+
52
+ ary
53
+ end # to_a
54
+
55
+
56
+ private
57
+ def update_match( m, season: '?', level: nil ) ## add a match
58
+
59
+ line1 = @lines[ m.team1 ] ||= TeamUsageLine.new( m.team1 )
60
+ line2 = @lines[ m.team2 ] ||= TeamUsageLine.new( m.team2 )
61
+
62
+ line1.matches +=1
63
+ line2.matches +=1
64
+
65
+ ## include season if not seen before (allow season in multiple files!!!)
66
+ line1.seasons << season unless line1.seasons.include?( season )
67
+ line2.seasons << season unless line2.seasons.include?( season )
68
+
69
+ if level
70
+ line1_level = line1.levels[ level ] ||= TeamUsageLine.new( m.team1 )
71
+ line2_level = line2.levels[ level ] ||= TeamUsageLine.new( m.team2 )
72
+
73
+ line1_level.matches +=1
74
+ line2_level.matches +=1
75
+
76
+ line1_level.seasons << season unless line1_level.seasons.include?( season )
77
+ line2_level.seasons << season unless line2_level.seasons.include?( season )
78
+ end
79
+ end # method update_match
80
+
81
+
82
+ end # class TeamUsage
83
+
84
+ end # module Sports