sports 0.1.0 → 0.2.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.
@@ -1,215 +0,0 @@
1
-
2
-
3
- module Sports
4
-
5
-
6
- class Matchlist ## todo: find a better name - MatchStats, MatchFixtures, MatchSchedule, ...
7
- ## use MatchCache/Buffer/Summary/Snippet/Segment/List...
8
- ## or MatchAnalyzer/Checker/Proofer/Query - why? why not?
9
- attr_reader :matches # count of matches
10
- ## :name,
11
- ## :goals, # count of (total) goals - use total_goals - why? why not?
12
- ## :teams, -- has its own reader
13
- ## :rounds # note: use if all teams have same match count
14
- ## add last_updated/updated or something - why? why not?
15
-
16
- def initialize( matches )
17
- @matches = matches
18
- end
19
-
20
-
21
- def usage
22
- @usage ||= build_usage( @matches )
23
- @usage
24
- end
25
-
26
- def team_usage() usage.team_usage; end
27
-
28
- def teams
29
- @team_names ||= team_usage.keys.sort
30
- @team_names
31
- end
32
-
33
- def goals() usage.goals; end
34
-
35
- ## note: start_date and end_date might be nil / optional missing!!!!
36
- def start_date?() usage.start_date?; end
37
- def end_date?() usage.end_date?; end
38
-
39
- def start_date() usage.start_date; end
40
- def end_date() usage.end_date; end
41
-
42
- def has_dates?() usage.has_dates?; end
43
- def dates_str() usage.dates_str; end
44
- def days() usage.days; end
45
-
46
-
47
- def rounds() usage.rounds; end
48
-
49
- ## todo: add has_rounds? alias for rounds? too
50
- ## return true if all match_played in team_usage are the same
51
- ## e.g. assumes league with matchday rounds
52
- def rounds?() usage.rounds?; end
53
-
54
- def match_counts() usage.match_counts; end
55
- def match_counts_str() usage.match_counts_str; end
56
-
57
-
58
-
59
- def stage_usage
60
- @stage_usage ||= build_stage_usage( @matches )
61
- @stage_usage
62
- end
63
-
64
- def stages() stage_usage.keys; end ## note: returns empty array for stages for now - why? why not?
65
-
66
-
67
- ############################
68
- # matchlist helpers
69
- private
70
- class StatLine
71
- attr_reader :team_usage,
72
- :matches,
73
- :goals,
74
- :rounds, ## keep rounds - why? why not?
75
- :start_date,
76
- :end_date
77
-
78
- def teams() @team_usage.keys.sort; end ## (auto-)sort here always - why? why not?
79
-
80
- def start_date?() @start_date.nil? == false; end
81
- def end_date?() @end_date.nil? == false; end
82
-
83
- def has_dates?() @start_date && @end_date; end
84
- def dates_str
85
- ## note: start_date/end_date might be optional/missing
86
- if has_dates?
87
- "#{start_date.strftime( '%a %d %b %Y' )} - #{end_date.strftime( '%a %d %b %Y' )}"
88
- else
89
- "??? - ???"
90
- end
91
- end
92
-
93
- def days() end_date.jd - start_date.jd; end
94
-
95
-
96
- def rounds
97
- rounds? ## note: use rounds? to calculate (cache) rounds
98
- @rounds ## note: return number of rounds or nil (for uneven matches played by teams)
99
- end
100
-
101
- ## todo: add has_rounds? alias for rounds? too
102
- def rounds?
103
- ## return true if all match_played in team_usage are the same
104
- ## e.g. assumes league with matchday rounds
105
- if @has_rounds.nil? ## check/todo: if undefined attribute is nil by default??
106
- ## check/calc rounds
107
- ## note: values => matches_played by team
108
- if match_counts.size == 1
109
- @rounds = match_counts[0][0]
110
- else
111
- @rounds = nil
112
- end
113
- @has_rounds = @rounds ? true : false
114
- end
115
- @has_rounds
116
- end
117
-
118
-
119
- def build_match_counts ## use/rename to matches_played - why? why not?
120
- counts = Hash.new(0)
121
- team_usage.values.each do |count|
122
- counts[count] += 1
123
- end
124
-
125
- ## sort (descending) highest usage value first (in returned array)
126
- ## e.g. [[32,8],[31,2]] ## 32 matches by 8 teams, 31 matches by 2 teams etc.
127
- counts.sort_by {|count, usage| -count }
128
- end
129
-
130
- def match_counts
131
- # match counts / nos played per team
132
- @match_counts ||= build_match_counts
133
- @match_counts
134
- end
135
-
136
- def match_counts_str
137
- ## pretty print / formatted match_counts
138
- buf = String.new('')
139
- match_counts.each_with_index do |rec,i|
140
- buf << ' ' if i > 0 ## add (space) separator
141
- buf << "#{rec[0]}×#{rec[1]}"
142
- end
143
- buf
144
- end
145
-
146
-
147
-
148
- def initialize
149
- @matches = 0
150
- @goals = 0
151
-
152
- @start_date = nil
153
- @end_date = nil
154
-
155
- @team_usage = Hash.new(0)
156
-
157
- @match_counts = nil
158
- end
159
-
160
-
161
- def update( match )
162
- @matches += 1 ## match counter
163
-
164
- if match.score1 && match.score2
165
- @goals += match.score1
166
- @goals += match.score2
167
-
168
- ## todo: add after extra time? if knock out (k.o.) - why? why not?
169
- ## make it a flag/opt?
170
- end
171
-
172
- @team_usage[ match.team1 ] += 1
173
- @team_usage[ match.team2 ] += 1
174
-
175
- if match.date
176
- ## return / store date as string as is - why? why not?
177
- date = Date.strptime( match.date, '%Y-%m-%d' )
178
-
179
- @start_date = date if @start_date.nil? || date < @start_date
180
- @end_date = date if @end_date.nil? || date > @end_date
181
- end
182
- end
183
- end # class StatLine
184
-
185
-
186
- ## collect total usage stats (for all matches)
187
- def build_usage( matches )
188
- stat = StatLine.new
189
- matches.each do |match|
190
- stat.update( match )
191
- end
192
- stat
193
- end
194
-
195
- ## collect usage stats by stage (e.g. regular / playoff / etc.)
196
- def build_stage_usage( matches )
197
- stages = {}
198
-
199
- matches.each do |match|
200
- stage_key = if match.stage.nil?
201
- 'Regular' ## note: assume Regular stage if not defined (AND not explicit unknown)
202
- else
203
- match.stage
204
- end
205
-
206
- stages[ stage_key ] ||= StatLine.new
207
- stages[ stage_key ].update( match )
208
- end
209
-
210
- stages
211
- end
212
-
213
- end # class Matchlist
214
-
215
- end # module Sports
@@ -1,23 +0,0 @@
1
- module Sports
2
-
3
- class Round
4
- attr_reader :name, :start_date, :end_date, :knockout
5
- attr_accessor :num # note: make read & writable - why? why not?
6
-
7
- def initialize( name:,
8
- num: nil,
9
- start_date: nil,
10
- end_date: nil,
11
- knockout: false,
12
- auto: true )
13
- @name = name
14
- @num = num
15
- @start_date = start_date
16
- @end_date = end_date
17
- @knockout = knockout
18
- @auto = auto # auto-created (inline reference/header without proper definition before)
19
- end
20
- end # class Round
21
-
22
- end # module Sports
23
-
@@ -1,271 +0,0 @@
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
@@ -1,147 +0,0 @@
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