sportdb-structs 0.1.0

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,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