sportdb-formats 0.2.1 → 0.3.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,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'