sportdb-structs 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,271 @@
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( match_or_matches=nil, 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
+
50
+ ## add init and update all-in-one convenience shortcut
51
+ update( match_or_matches ) if match_or_matches
52
+ end
53
+
54
+
55
+ def update( match_or_matches )
56
+ ## convenience - update all matches at once
57
+ ## todo/check: check for ActiveRecord_Associations_CollectionProxy and than use to_a (to "force" array) - why? why not?
58
+ matches = if match_or_matches.is_a?(Array)
59
+ match_or_matches
60
+ else
61
+ [match_or_matches]
62
+ end
63
+
64
+ matches.each_with_index do |match,i| # note: index(i) starts w/ zero (0)
65
+ update_match( match )
66
+ end
67
+ self # note: return self to allow chaining
68
+ end
69
+
70
+
71
+ ## note: add a convenience shortcut
72
+ ## to_a will sort and add rank (1,2,3) to standing lines
73
+ def each( &block ) to_a.each( &block ); end
74
+
75
+ def to_a
76
+ ## return lines; sort and add rank
77
+ ## note: will update rank!!!! (side effect)
78
+
79
+ #############################
80
+ ### calc ranking position (rank)
81
+ ## fix/allow same rank e.g. all 1 or more than one team 3rd etc.
82
+
83
+ # build array from hash
84
+ ary = []
85
+ @lines.each do |k,v|
86
+ ary << v
87
+ end
88
+
89
+ ary.sort! do |l,r|
90
+ ## note: reverse order (thus, change l,r to r,l)
91
+ value = r.pts <=> l.pts
92
+ if value == 0 # same pts try goal diff
93
+ value = (r.goals_for-r.goals_against) <=> (l.goals_for-l.goals_against)
94
+ if value == 0 # same goal diff too; try assume more goals better for now
95
+ value = r.goals_for <=> l.goals_for
96
+ end
97
+ end
98
+ value
99
+ end
100
+
101
+ ## update rank using ordered array
102
+ ary.each_with_index do |line,i|
103
+ line.rank = i+1 ## add ranking (e.g. 1,2,3 etc.) - note: i starts w/ zero (0)
104
+ end
105
+
106
+ ary
107
+ end # to_a
108
+
109
+
110
+
111
+ #####
112
+ ###
113
+ ## fix: move build to StandingsPart/Report !!!!
114
+ def build( source: nil ) ## build / pretty print standings table in string buffer
115
+ ## keep pretty printer in struct - why? why not?
116
+
117
+
118
+ ## add standings table in markdown to buffer (buf)
119
+
120
+ ## todo: use different styles/formats (simple/ etc ???)
121
+
122
+ ## simple table (only totals - no home/away)
123
+ ## standings.to_a.each do |l|
124
+ ## buf << '%2d. ' % l.rank
125
+ ## buf << '%-28s ' % l.team
126
+ ## buf << '%2d ' % l.played
127
+ ## buf << '%3d ' % l.won
128
+ ## buf << '%3d ' % l.drawn
129
+ ## buf << '%3d ' % l.lost
130
+ ## buf << '%3d:%-3d ' % [l.goals_for,l.goals_against]
131
+ ## buf << '%3d' % l.pts
132
+ ## buf << "\n"
133
+ ## end
134
+
135
+ buf = ''
136
+ buf << "\n"
137
+ buf << "```\n"
138
+ buf << " - Home - - Away - - Total -\n"
139
+ buf << " Pld W D L F:A W D L F:A F:A +/- Pts\n"
140
+
141
+ to_a.each do |l|
142
+ buf << '%2d. ' % l.rank
143
+ buf << '%-28s ' % l.team
144
+ buf << '%2d ' % l.played
145
+
146
+ buf << '%2d ' % l.home_won
147
+ buf << '%2d ' % l.home_drawn
148
+ buf << '%2d ' % l.home_lost
149
+ buf << '%3d:%-3d ' % [l.home_goals_for,l.home_goals_against]
150
+
151
+ buf << '%2d ' % l.away_won
152
+ buf << '%2d ' % l.away_drawn
153
+ buf << '%2d ' % l.away_lost
154
+ buf << '%3d:%-3d ' % [l.away_goals_for,l.away_goals_against]
155
+
156
+ buf << '%3d:%-3d ' % [l.goals_for,l.goals_against]
157
+
158
+ goals_diff = l.goals_for-l.goals_against
159
+ if goals_diff > 0
160
+ buf << '%3s ' % "+#{goals_diff}"
161
+ elsif goals_diff < 0
162
+ buf << '%3s ' % "#{goals_diff}"
163
+ else ## assume 0
164
+ buf << ' '
165
+ end
166
+
167
+ buf << '%3d' % l.pts
168
+ buf << "\n"
169
+ end
170
+
171
+ buf << "```\n"
172
+ buf << "\n"
173
+
174
+ ## optinal: add data source if known / present
175
+ ## assume (relative) markdown link for now in README.md
176
+ if source
177
+ buf << "(Source: [`#{source}`](#{source}))\n"
178
+ buf << "\n"
179
+ end
180
+
181
+ buf
182
+ end
183
+
184
+
185
+ private
186
+ def update_match( m ) ## add a match
187
+
188
+ ## note: always use team as string for now
189
+ ## for now allow passing in of string OR struct - why? why not?
190
+ ## todo/fix: change to m.team1_name and m.team2_name - why? why not?
191
+ team1 = m.team1.is_a?( String ) ? m.team1 : m.team1.name
192
+ team2 = m.team2.is_a?( String ) ? m.team2 : m.team2.name
193
+
194
+ score = m.score.to_s
195
+
196
+ ## puts " #{team1} - #{team2} #{score}"
197
+
198
+ unless m.over?
199
+ puts " !!!! skipping match - not yet over (date in the future) => #{m.date}"
200
+ return
201
+ end
202
+
203
+ unless m.complete?
204
+ puts "!!! [calc_standings] skipping match #{team1} - #{team2} w/ past date #{m.date} - scores incomplete => #{score}"
205
+ return
206
+ end
207
+
208
+
209
+
210
+ line1 = @lines[ team1 ] || StandingsLine.new( team1 )
211
+ line2 = @lines[ team2 ] || StandingsLine.new( team2 )
212
+
213
+ line1.played += 1
214
+ line1.home_played += 1
215
+
216
+ line2.played += 1
217
+ line2.away_played += 1
218
+
219
+ if m.winner == 1
220
+ line1.won += 1
221
+ line1.home_won += 1
222
+
223
+ line2.lost += 1
224
+ line2.away_lost += 1
225
+
226
+ line1.pts += @pts_won
227
+ line1.home_pts += @pts_won
228
+ elsif m.winner == 2
229
+ line1.lost += 1
230
+ line1.home_lost += 1
231
+
232
+ line2.won += 1
233
+ line2.away_won += 1
234
+
235
+ line2.pts += @pts_won
236
+ line2.away_pts += @pts_won
237
+ else ## assume drawn/tie (that is, 0)
238
+ line1.drawn += 1
239
+ line1.home_drawn += 1
240
+
241
+ line2.drawn += 1
242
+ line2.away_drawn += 1
243
+
244
+ line1.pts += 1
245
+ line1.home_pts += 1
246
+ line2.pts += 1
247
+ line2.away_pts += 1
248
+ end
249
+
250
+ if m.score1 && m.score2
251
+ line1.goals_for += m.score1
252
+ line1.home_goals_for += m.score1
253
+ line1.goals_against += m.score2
254
+ line1.home_goals_against += m.score2
255
+
256
+ line2.goals_for += m.score2
257
+ line2.away_goals_for += m.score2
258
+ line2.goals_against += m.score1
259
+ line2.away_goals_against += m.score1
260
+ else
261
+ puts "*** warn: [standings] skipping match with missing scores: #{m.inspect}"
262
+ end
263
+
264
+ @lines[ team1 ] = line1
265
+ @lines[ team2 ] = line2
266
+ end # method update_match
267
+
268
+ end # class Standings
269
+
270
+
271
+ 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 SportDb::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