sportdb-formats 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,223 @@
1
+ # encoding: utf-8
2
+
3
+
4
+ ##
5
+ # note: add all "former" structs to the SportDb::Import module / namespace
6
+
7
+ module SportDb
8
+ module Import
9
+
10
+
11
+ class Matchlist ## todo: find a better name - MatchStats, MatchFixtures, MatchSchedule, ...
12
+ ## use MatchCache/Buffer/Summary/Snippet/Segment/List...
13
+ ## or MatchAnalyzer/Checker/Proofer/Query - why? why not?
14
+ attr_reader :matches # count of matches
15
+ ## :name,
16
+ ## :goals, # count of (total) goals - use total_goals - why? why not?
17
+ ## :teams, -- has its own reader
18
+ ## :rounds # note: use if all teams have same match count
19
+ ## add last_updated/updated or something - why? why not?
20
+
21
+ def initialize( matches )
22
+ @matches = matches
23
+ end
24
+
25
+
26
+ def usage
27
+ @usage ||= build_usage( @matches )
28
+ @usage
29
+ end
30
+
31
+ def team_usage() usage.team_usage; end
32
+
33
+ def teams
34
+ @team_names ||= team_usage.keys.sort
35
+ @team_names
36
+ end
37
+
38
+ def goals() usage.goals; end
39
+
40
+ ## note: start_date and end_date might be nil / optional missing!!!!
41
+ def start_date?() usage.start_date?; end
42
+ def end_date?() usage.end_date?; end
43
+
44
+ def start_date() usage.start_date; end
45
+ def end_date() usage.end_date; end
46
+
47
+ def has_dates?() usage.has_dates?; end
48
+ def dates_str() usage.dates_str; end
49
+ def days() usage.days; end
50
+
51
+
52
+ def rounds() usage.rounds; end
53
+
54
+ ## todo: add has_rounds? alias for rounds? too
55
+ ## return true if all match_played in team_usage are the same
56
+ ## e.g. assumes league with matchday rounds
57
+ def rounds?() usage.rounds?; end
58
+
59
+ def match_counts() usage.match_counts; end
60
+ def match_counts_str() usage.match_counts_str; end
61
+
62
+
63
+
64
+ def stage_usage
65
+ @stage_usage ||= build_stage_usage( @matches )
66
+ @stage_usage
67
+ end
68
+
69
+ def stages() stage_usage.keys; end ## note: returns empty array for stages for now - why? why not?
70
+
71
+
72
+ ############################
73
+ # matchlist helpers
74
+ private
75
+ class StatLine
76
+ attr_reader :team_usage,
77
+ :matches,
78
+ :goals,
79
+ :rounds, ## keep rounds - why? why not?
80
+ :start_date,
81
+ :end_date
82
+
83
+ def teams() @team_usage.keys.sort; end ## (auto-)sort here always - why? why not?
84
+
85
+ def start_date?() @start_date.nil? == false; end
86
+ def end_date?() @end_date.nil? == false; end
87
+
88
+ def has_dates?() @start_date && @end_date; end
89
+ def dates_str
90
+ ## note: start_date/end_date might be optional/missing
91
+ if has_dates?
92
+ "#{start_date.strftime( '%a %d %b %Y' )} - #{end_date.strftime( '%a %d %b %Y' )}"
93
+ else
94
+ "??? - ???"
95
+ end
96
+ end
97
+
98
+ def days() end_date.jd - start_date.jd; end
99
+
100
+
101
+ def rounds
102
+ rounds? ## note: use rounds? to calculate (cache) rounds
103
+ @rounds ## note: return number of rounds or nil (for uneven matches played by teams)
104
+ end
105
+
106
+ ## todo: add has_rounds? alias for rounds? too
107
+ def rounds?
108
+ ## return true if all match_played in team_usage are the same
109
+ ## e.g. assumes league with matchday rounds
110
+ if @has_rounds.nil? ## check/todo: if undefined attribute is nil by default??
111
+ ## check/calc rounds
112
+ ## note: values => matches_played by team
113
+ if match_counts.size == 1
114
+ @rounds = match_counts[0][0]
115
+ else
116
+ @rounds = nil
117
+ end
118
+ @has_rounds = @rounds ? true : false
119
+ end
120
+ @has_rounds
121
+ end
122
+
123
+
124
+ def build_match_counts ## use/rename to matches_played - why? why not?
125
+ counts = Hash.new(0)
126
+ team_usage.values.each do |count|
127
+ counts[count] += 1
128
+ end
129
+
130
+ ## sort (descending) highest usage value first (in returned array)
131
+ ## e.g. [[32,8],[31,2]] ## 32 matches by 8 teams, 31 matches by 2 teams etc.
132
+ counts.sort_by {|count, usage| -count }
133
+ end
134
+
135
+ def match_counts
136
+ # match counts / nos played per team
137
+ @match_counts ||= build_match_counts
138
+ @match_counts
139
+ end
140
+
141
+ def match_counts_str
142
+ ## pretty print / formatted match_counts
143
+ buf = String.new('')
144
+ match_counts.each_with_index do |rec,i|
145
+ buf << ' ' if i > 0 ## add (space) separator
146
+ buf << "#{rec[0]}×#{rec[1]}"
147
+ end
148
+ buf
149
+ end
150
+
151
+
152
+
153
+ def initialize
154
+ @matches = 0
155
+ @goals = 0
156
+
157
+ @start_date = nil
158
+ @end_date = nil
159
+
160
+ @team_usage = Hash.new(0)
161
+
162
+ @match_counts = nil
163
+ end
164
+
165
+
166
+ def update( match )
167
+ @matches += 1 ## match counter
168
+
169
+ if match.score1 && match.score2
170
+ @goals += match.score1
171
+ @goals += match.score2
172
+
173
+ ## todo: add after extra time? if knock out (k.o.) - why? why not?
174
+ ## make it a flag/opt?
175
+ end
176
+
177
+ @team_usage[ match.team1 ] += 1
178
+ @team_usage[ match.team2 ] += 1
179
+
180
+ if match.date
181
+ ## return / store date as string as is - why? why not?
182
+ date = Date.strptime( match.date, '%Y-%m-%d' )
183
+
184
+ @start_date = date if @start_date.nil? || date < @start_date
185
+ @end_date = date if @end_date.nil? || date > @end_date
186
+ end
187
+ end
188
+ end # class StatLine
189
+
190
+
191
+ ## collect total usage stats (for all matches)
192
+ def build_usage( matches )
193
+ stat = StatLine.new
194
+ matches.each do |match|
195
+ stat.update( match )
196
+ end
197
+ stat
198
+ end
199
+
200
+ ## collect usage stats by stage (e.g. regular / playoff / etc.)
201
+ def build_stage_usage( matches )
202
+ stages = {}
203
+
204
+ matches.each do |match|
205
+ stage_key = if match.stage.nil?
206
+ 'Regular' ## note: assume Regular stage if not defined (AND not explicit unknown)
207
+ else
208
+ match.stage
209
+ end
210
+
211
+ stages[ stage_key ] ||= StatLine.new
212
+ stages[ stage_key ].update( match )
213
+ end
214
+
215
+ stages
216
+ end
217
+
218
+ end # class Matchlist
219
+
220
+
221
+
222
+ end # module Import
223
+ end # module SportDb
@@ -0,0 +1,123 @@
1
+ # encoding: utf-8
2
+
3
+
4
+ ##
5
+ # note: add all "former" structs to the SportDb::Import module / namespace
6
+
7
+ module SportDb
8
+ module Import
9
+
10
+
11
+ class Season
12
+ ##
13
+ ## todo: add (optional) start_date and end_date - why? why not?
14
+ ## add next
15
+
16
+
17
+ attr_reader :start_year,
18
+ :end_year
19
+
20
+ def year?() @end_year.nil?; end ## single-year season e.g. 2011 if no end_year present
21
+
22
+
23
+ def initialize( str ) ## assume only string / line gets passed in for now
24
+ @start_year, @end_year = parse( str )
25
+ end
26
+
27
+
28
+ YYYY_YYYY_RE = %r{^ ## e.g. 2011-2012 or 2011/2012
29
+ (\d{4})
30
+ [/-]
31
+ (\d{4})
32
+ $
33
+ }x
34
+ YYYY_YY_RE = %r{^ ## e.g. 2011-12 or 2011/12
35
+ (\d{4})
36
+ [/-]
37
+ (\d{2})
38
+ $
39
+ }x
40
+ YYYY_Y_RE = %r{^ ## e.g. 2011-2 or 2011/2
41
+ (\d{4})
42
+ [/-]
43
+ (\d{1})
44
+ $
45
+ }x
46
+ YYYY_RE = %r{^ ## e.g. 2011
47
+ (\d{4})
48
+ $
49
+ }x
50
+
51
+ def parse( str )
52
+ if str =~ YYYY_YYYY_RE ## e.g. 2011/2012
53
+ [$1.to_i, $2.to_i]
54
+ elsif str =~ YYYY_YY_RE ## e.g. 2011/12
55
+ fst = $1.to_i
56
+ snd = $2.to_i
57
+ snd_exp = '%02d' % [(fst+1) % 100] ## double check: e.g 00 == 00, 01==01 etc.
58
+ raise ArgumentError.new( "[Season#parse] invalid year in season >>#{str}<<; expected #{snd_exp} but got #{$2}") if snd_exp != $2
59
+ [fst, fst+1]
60
+ elsif str =~ YYYY_Y_RE ## e.g. 2011/2
61
+ fst = $1.to_i
62
+ snd = $2.to_i
63
+ snd_exp = '%d' % [(fst+1) % 10] ## double check: e.g 0 == 0, 1==1 etc.
64
+ raise ArgumentError.new( "[Season#parse] invalid year in season >>#{str}<<; expected #{snd_exp} but got #{$2}") if snd_exp != $2
65
+ [fst, fst+1]
66
+ elsif str =~ YYYY_RE ## e.g. 2011
67
+ [$1.to_i]
68
+ else
69
+ raise ArgumentError.new( "[Season#parse] unkown season format >>#{str}<<; sorry cannot parse")
70
+ end
71
+ end
72
+
73
+
74
+
75
+ def prev
76
+ if year?
77
+ Season.new( "#{@start_year-1}" )
78
+ else
79
+ Season.new( "#{@start_year-1}/#{@start_year}" )
80
+ end
81
+ end
82
+
83
+ def key
84
+ if year?
85
+ '%d' % @start_year
86
+ else
87
+ '%d/%02d' % [@start_year, @end_year % 100]
88
+ end
89
+ end
90
+ alias_method :to_key, :key
91
+ alias_method :to_s, :key
92
+
93
+
94
+ def path( format: nil )
95
+ ## todo: find better names for formats - why? why not?:
96
+ ## long | archive | decade(?) => 1980s/1988-89, 2010s/2017-18, ...
97
+ ## short | std(?) => 1988-89, 2017-18, ...
98
+
99
+ ## convert season name to "standard" season name for directory
100
+
101
+ if ['l', 'long', 'archive' ].include?( format.to_s ) ## note: allow passing in of symbol to e.g. 'long' or :long
102
+ if year? ## e.g. 2000s/2001
103
+ "%3d0s/%4d" % [@start_year / 10, @start_year]
104
+ else ## e.g. 2000s/2001-02
105
+ "%3d0s/%4d-%02d" % [@start_year / 10, @start_year, @end_year % 100]
106
+ end
107
+ else ## default 'short' format / fallback
108
+ if year? ## e.g. 2001
109
+ "%4d" % @start_year
110
+ else ## e.g. 2001-02
111
+ "%4d-%02d" % [@start_year, @end_year % 100]
112
+ end
113
+ end
114
+ end # method path
115
+ alias_method :directory, :path ## keep "legacy" directory alias - why? why not?
116
+ alias_method :to_path, :path
117
+
118
+
119
+ end # class Season
120
+
121
+
122
+ end # module Import
123
+ end # module SportDb
@@ -0,0 +1,250 @@
1
+ # encoding: utf-8
2
+
3
+ ##########
4
+ # todo/fix:
5
+ ## reuse standings helper/calculator from sportdb
6
+ ## do NOT duplicate
7
+
8
+
9
+ ##
10
+ # note: add all "former" structs to the SportDb::Import module / namespace
11
+
12
+ module SportDb
13
+ module Import
14
+
15
+
16
+ class Standings
17
+
18
+ class StandingsLine ## nested class StandinsLine
19
+ attr_accessor :rank, :team,
20
+ :played, :won, :lost, :drawn, ## -- total
21
+ :goals_for, :goals_against, :pts,
22
+ :home_played, :home_won, :home_lost, :home_drawn, ## -- home
23
+ :home_goals_for, :home_goals_against, :home_pts,
24
+ :away_played, :away_won, :away_lost, :away_drawn, ## -- away
25
+ :away_goals_for, :away_goals_against, :away_pts
26
+
27
+ def initialize( team )
28
+ @rank = nil # use 0? why? why not?
29
+ @team = team
30
+ @played = @home_played = @away_played = 0
31
+ @won = @home_won = @away_won = 0
32
+ @lost = @home_lost = @away_lost = 0
33
+ @drawn = @home_drawn = @away_drawn = 0
34
+ @goals_for = @home_goals_for = @away_goals_for = 0
35
+ @goals_against = @home_goals_against = @away_goals_against = 0
36
+ @pts = @home_pts = @away_pts = 0
37
+ end
38
+ end # (nested) class StandingsLine
39
+
40
+
41
+ def initialize( opts={} )
42
+ ## fix:
43
+ # passing in e.g. pts for win (3? 2? etc.)
44
+ # default to 3 for now
45
+
46
+ ## lets you pass in 2 as an alterantive, for example
47
+ @pts_won = opts[:pts_won] || 3
48
+
49
+ @lines = {} # StandingsLines cached by team name/key
50
+ end
51
+
52
+
53
+ def update( match_or_matches )
54
+ ## convenience - update all matches at once
55
+ matches = match_or_matches.is_a?(Array) ? match_or_matches : [match_or_matches]
56
+
57
+ matches.each_with_index do |match,i| # note: index(i) starts w/ zero (0)
58
+ update_match( match )
59
+ end
60
+ self # note: return self to allow chaining
61
+ end
62
+
63
+
64
+ def to_a
65
+ ## return lines; sort and add rank
66
+ ## note: will update rank!!!! (side effect)
67
+
68
+ #############################
69
+ ### calc ranking position (rank)
70
+ ## fix/allow same rank e.g. all 1 or more than one team 3rd etc.
71
+
72
+ # build array from hash
73
+ ary = []
74
+ @lines.each do |k,v|
75
+ ary << v
76
+ end
77
+
78
+ ary.sort! do |l,r|
79
+ ## note: reverse order (thus, change l,r to r,l)
80
+ value = r.pts <=> l.pts
81
+ if value == 0 # same pts try goal diff
82
+ value = (r.goals_for-r.goals_against) <=> (l.goals_for-l.goals_against)
83
+ if value == 0 # same goal diff too; try assume more goals better for now
84
+ value = r.goals_for <=> l.goals_for
85
+ end
86
+ end
87
+ value
88
+ end
89
+
90
+ ## update rank using ordered array
91
+ ary.each_with_index do |line,i|
92
+ line.rank = i+1 ## add ranking (e.g. 1,2,3 etc.) - note: i starts w/ zero (0)
93
+ end
94
+
95
+ ary
96
+ end # to_a
97
+
98
+
99
+
100
+ #####
101
+ ###
102
+ ## fix: move build to StandingsPart/Report !!!!
103
+ def build( source: nil ) ## build / pretty print standings table in string buffer
104
+ ## keep pretty printer in struct - why? why not?
105
+
106
+
107
+ ## add standings table in markdown to buffer (buf)
108
+
109
+ ## todo: use different styles/formats (simple/ etc ???)
110
+
111
+ ## simple table (only totals - no home/away)
112
+ ## standings.to_a.each do |l|
113
+ ## buf << '%2d. ' % l.rank
114
+ ## buf << '%-28s ' % l.team
115
+ ## buf << '%2d ' % l.played
116
+ ## buf << '%3d ' % l.won
117
+ ## buf << '%3d ' % l.drawn
118
+ ## buf << '%3d ' % l.lost
119
+ ## buf << '%3d:%-3d ' % [l.goals_for,l.goals_against]
120
+ ## buf << '%3d' % l.pts
121
+ ## buf << "\n"
122
+ ## end
123
+
124
+ buf = ''
125
+ buf << "\n"
126
+ buf << "```\n"
127
+ buf << " - Home - - Away - - Total -\n"
128
+ buf << " Pld W D L F:A W D L F:A F:A +/- Pts\n"
129
+
130
+ to_a.each do |l|
131
+ buf << '%2d. ' % l.rank
132
+ buf << '%-28s ' % l.team
133
+ buf << '%2d ' % l.played
134
+
135
+ buf << '%2d ' % l.home_won
136
+ buf << '%2d ' % l.home_drawn
137
+ buf << '%2d ' % l.home_lost
138
+ buf << '%3d:%-3d ' % [l.home_goals_for,l.home_goals_against]
139
+
140
+ buf << '%2d ' % l.away_won
141
+ buf << '%2d ' % l.away_drawn
142
+ buf << '%2d ' % l.away_lost
143
+ buf << '%3d:%-3d ' % [l.away_goals_for,l.away_goals_against]
144
+
145
+ buf << '%3d:%-3d ' % [l.goals_for,l.goals_against]
146
+
147
+ goals_diff = l.goals_for-l.goals_against
148
+ if goals_diff > 0
149
+ buf << '%3s ' % "+#{goals_diff}"
150
+ elsif goals_diff < 0
151
+ buf << '%3s ' % "#{goals_diff}"
152
+ else ## assume 0
153
+ buf << ' '
154
+ end
155
+
156
+ buf << '%3d' % l.pts
157
+ buf << "\n"
158
+ end
159
+
160
+ buf << "```\n"
161
+ buf << "\n"
162
+
163
+ ## optinal: add data source if known / present
164
+ ## assume (relative) markdown link for now in README.md
165
+ if source
166
+ buf << "(Source: [`#{source}`](#{source}))\n"
167
+ buf << "\n"
168
+ end
169
+
170
+ buf
171
+ end
172
+
173
+
174
+ private
175
+ def update_match( m ) ## add a match
176
+
177
+ ## puts " #{m.team1} - #{m.team2} #{m.score_str}"
178
+ unless m.over?
179
+ puts " !!!! skipping match - not yet over (play_at date in the future)"
180
+ return
181
+ end
182
+
183
+ unless m.complete?
184
+ puts "!!! [calc_standings] skipping match #{m.team1} - #{m.team2} - scores incomplete #{m.score_str}"
185
+ return
186
+ end
187
+
188
+ line1 = @lines[ m.team1 ] || StandingsLine.new( m.team1 )
189
+ line2 = @lines[ m.team2 ] || StandingsLine.new( m.team2 )
190
+
191
+ line1.played += 1
192
+ line1.home_played += 1
193
+
194
+ line2.played += 1
195
+ line2.away_played += 1
196
+
197
+ if m.winner == 1
198
+ line1.won += 1
199
+ line1.home_won += 1
200
+
201
+ line2.lost += 1
202
+ line2.away_lost += 1
203
+
204
+ line1.pts += @pts_won
205
+ line1.home_pts += @pts_won
206
+ elsif m.winner == 2
207
+ line1.lost += 1
208
+ line1.home_lost += 1
209
+
210
+ line2.won += 1
211
+ line2.away_won += 1
212
+
213
+ line2.pts += @pts_won
214
+ line2.away_pts += @pts_won
215
+ else ## assume drawn/tie (that is, 0)
216
+ line1.drawn += 1
217
+ line1.home_drawn += 1
218
+
219
+ line2.drawn += 1
220
+ line2.away_drawn += 1
221
+
222
+ line1.pts += 1
223
+ line1.home_pts += 1
224
+ line2.pts += 1
225
+ line2.away_pts += 1
226
+ end
227
+
228
+ if m.score1 && m.score2
229
+ line1.goals_for += m.score1
230
+ line1.home_goals_for += m.score1
231
+ line1.goals_against += m.score2
232
+ line1.home_goals_against += m.score2
233
+
234
+ line2.goals_for += m.score2
235
+ line2.away_goals_for += m.score2
236
+ line2.goals_against += m.score1
237
+ line2.away_goals_against += m.score1
238
+ else
239
+ puts "*** warn: [standings] skipping match with missing scores: #{m.inspect}"
240
+ end
241
+
242
+ @lines[ m.team1 ] = line1
243
+ @lines[ m.team2 ] = line2
244
+ end # method update_match
245
+
246
+ end # class Standings
247
+
248
+
249
+ end # module Import
250
+ end # module SportDb
@@ -0,0 +1,91 @@
1
+ # encoding: utf-8
2
+
3
+
4
+ ##
5
+ # note: add all "former" structs to the SportDb::Import module / namespace
6
+
7
+ module SportDb
8
+ module Import
9
+
10
+
11
+ class TeamUsage
12
+
13
+ class TeamUsageLine ## nested class
14
+ attr_accessor :team,
15
+ :matches, ## number of matches (played),
16
+ :seasons, ## (optianl) array of seasons, use seasons.size for count
17
+ :levels ## (optional) hash of levels (holds mapping level to TeamUsageLine)
18
+
19
+ def initialize( team )
20
+ @team = team
21
+
22
+ @matches = 0
23
+ @seasons = []
24
+ @levels = {}
25
+ end
26
+ end # (nested) class TeamUsageLine
27
+
28
+
29
+
30
+ def initialize( opts={} )
31
+ @lines = {} # StandingsLines cached by team name/key
32
+ end
33
+
34
+
35
+ def update( matches, season: '?', level: nil )
36
+ ## convenience - update all matches at once
37
+ matches.each_with_index do |match,i| # note: index(i) starts w/ zero (0)
38
+ update_match( match, season: season, level: level )
39
+ end
40
+ self # note: return self to allow chaining
41
+ end
42
+
43
+ def to_a
44
+ ## return lines; sort
45
+
46
+ # build array from hash
47
+ ary = []
48
+ @lines.each do |k,v|
49
+ ary << v
50
+ end
51
+
52
+ ## for now sort just by name (a-z)
53
+ ary.sort! do |l,r|
54
+ ## note: reverse order (thus, change l,r to r,l)
55
+ l.team <=> r.team
56
+ end
57
+
58
+ ary
59
+ end # to_a
60
+
61
+
62
+ private
63
+ def update_match( m, season: '?', level: nil ) ## add a match
64
+
65
+ line1 = @lines[ m.team1 ] ||= TeamUsageLine.new( m.team1 )
66
+ line2 = @lines[ m.team2 ] ||= TeamUsageLine.new( m.team2 )
67
+
68
+ line1.matches +=1
69
+ line2.matches +=1
70
+
71
+ ## include season if not seen before (allow season in multiple files!!!)
72
+ line1.seasons << season unless line1.seasons.include?( season )
73
+ line2.seasons << season unless line2.seasons.include?( season )
74
+
75
+ if level
76
+ line1_level = line1.levels[ level ] ||= TeamUsageLine.new( m.team1 )
77
+ line2_level = line2.levels[ level ] ||= TeamUsageLine.new( m.team2 )
78
+
79
+ line1_level.matches +=1
80
+ line2_level.matches +=1
81
+
82
+ line1_level.seasons << season unless line1_level.seasons.include?( season )
83
+ line2_level.seasons << season unless line2_level.seasons.include?( season )
84
+ end
85
+ end # method update_match
86
+
87
+
88
+ end # class TeamUsage
89
+
90
+ end # module Import
91
+ end # module SportDb
@@ -5,8 +5,8 @@ module SportDb
5
5
  module Formats
6
6
 
7
7
  MAJOR = 0 ## todo: namespace inside version or something - why? why not??
8
- MINOR = 2
9
- PATCH = 1
8
+ MINOR = 3
9
+ PATCH = 0
10
10
  VERSION = [MAJOR,MINOR,PATCH].join('.')
11
11
 
12
12
  def self.version
@@ -31,6 +31,13 @@ require 'sportdb/formats/datafile'
31
31
  require 'sportdb/formats/package'
32
32
  require 'sportdb/formats/season_utils'
33
33
 
34
+ require 'sportdb/formats/structs/season'
35
+ require 'sportdb/formats/structs/club'
36
+ require 'sportdb/formats/structs/match'
37
+ require 'sportdb/formats/structs/matchlist'
38
+ require 'sportdb/formats/structs/standings'
39
+ require 'sportdb/formats/structs/team_usage'
40
+
34
41
 
35
42
  require 'sportdb/formats/scores'
36
43
  require 'sportdb/formats/goals'