sports 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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