sportdb-quick 0.5.3 → 0.7.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,67 @@
1
+ module SportDb
2
+ class MatchTree
3
+
4
+
5
+
6
+ class Goal ### nested (non-freestanding) inside match (match is parent)
7
+ attr_reader :team, ## note - 1|2 expected
8
+ :player,
9
+ :minute,
10
+ :offset,
11
+ :owngoal, ## true|false
12
+ :penalty ## true|false
13
+
14
+ ## add alias for player => name - why? why not?
15
+ alias_method :name, :player
16
+
17
+
18
+ def owngoal?() @owngoal==true; end
19
+ def penalty?() @penalty==true; end
20
+ def team1?() @team == 1; end
21
+ def team2?() @team == 2; end
22
+
23
+
24
+ ## note: make score1,score2 optional for now !!!!
25
+ def initialize( team:,
26
+ player:,
27
+ minute:,
28
+ offset: nil,
29
+ owngoal: false,
30
+ penalty: false
31
+ )
32
+ @team = team # 1|2
33
+ @player = player
34
+ @minute = minute
35
+ @offset = offset
36
+ @owngoal = owngoal
37
+ @penalty = penalty
38
+ end
39
+
40
+ def state
41
+ [@team,
42
+ @player, @minute, @offset, @owngoal, @penalty
43
+ ]
44
+ end
45
+
46
+ def ==(o)
47
+ o.class == self.class && o.state == state
48
+ end
49
+
50
+ def pretty_print( printer )
51
+ buf = String.new
52
+ buf << "<Goal"
53
+ buf << " #{@player} #{@minute}"
54
+ buf << "+#{@offset}" if @offset && @offset > 0
55
+ buf << "'"
56
+ buf << " (og)" if @owngoal
57
+ buf << " (p)" if @penalty
58
+ buf << " for #{@team}" ### team 1 or 2 - use home/away
59
+ buf << ">"
60
+
61
+ printer.text( buf )
62
+ end
63
+ end # class Goal
64
+
65
+
66
+ end # class MatchTree
67
+ end # module SportDb
@@ -0,0 +1,25 @@
1
+ module SportDb
2
+ class MatchTree
3
+
4
+ class Group
5
+ attr_reader :name, :teams
6
+
7
+ def initialize( name:,
8
+ teams: )
9
+ @name = name
10
+ @teams = teams
11
+ end
12
+
13
+ def pretty_print( printer )
14
+ buf = String.new
15
+ buf << "<Group #{@name} "
16
+ buf << @teams.pretty_print_inspect
17
+ buf << ">"
18
+
19
+ printer.text( buf )
20
+ end
21
+ end # class Group
22
+
23
+
24
+ end # class MatchTree
25
+ end # module SportDb
@@ -0,0 +1,247 @@
1
+ ##
2
+ # move (simpler) struct version inline to MatchTree for now
3
+ #
4
+ module SportDb
5
+ class MatchTree
6
+
7
+
8
+
9
+ class Match
10
+
11
+ ### note - use inline Score class Match::Score - why? why not?
12
+ ## note - score might internally be an array [2,3]
13
+ ## or hash { ft:, } etc.
14
+
15
+ ## note - score for now might be
16
+ ## 1) array e.g. [1,0] or []
17
+ ## 2) hash e.g. { ft: [1,0] } etc.
18
+
19
+ attr_reader :num,
20
+ :date,
21
+ :time,
22
+ :time_local,
23
+ :team1, :team2, ## todo/fix: use team1_name, team2_name or similar - for compat with db activerecord version? why? why not?
24
+ :score,
25
+ :round, ## todo/fix: use round_num or similar - for compat with db activerecord version? why? why not?
26
+ :group,
27
+ :status, ## e.g. replay, cancelled, awarded, abadoned, postponed, etc.
28
+ :ground, ## (optional) add as text line for now (incl. city, timezone etc.)
29
+ :att ## (optional) attendance as (integer) number
30
+
31
+
32
+ attr_accessor :goals ## todo/fix: make goals like all other attribs!!
33
+
34
+ def initialize( **kwargs )
35
+ @score = []
36
+ ## @score1, @score2 = [nil,nil] ## full time
37
+ ## @score1i, @score2i = [nil,nil] ## half time (first (i) part)
38
+ ## @score1et, @score2et = [nil,nil] ## extra time
39
+ ## @score1p, @score2p = [nil,nil] ## penalty
40
+ ## @score1agg, @score2agg = [nil,nil] ## full time (all legs) aggregated
41
+
42
+
43
+ update( **kwargs ) unless kwargs.empty?
44
+ end
45
+
46
+
47
+ def update( **kwargs )
48
+ @num = kwargs[:num] if kwargs.has_key?( :num )
49
+
50
+ ## note: check with has_key? because value might be nil!!!
51
+ @date = kwargs[:date] if kwargs.has_key?( :date )
52
+ @time = kwargs[:time] if kwargs.has_key?( :time )
53
+ @time_local = kwargs[:time_local] if kwargs.has_key?( :time_local )
54
+
55
+ ## todo/fix: use team1_name, team2_name or similar - for compat with db activerecord version? why? why not?
56
+ @team1 = kwargs[:team1] if kwargs.has_key?( :team1 )
57
+ @team2 = kwargs[:team2] if kwargs.has_key?( :team2 )
58
+
59
+ ## note: round is a string!!! e.g. '1', '2' for matchday or 'Final', 'Semi-final', etc.
60
+ ## todo: use to_s - why? why not?
61
+ @round = kwargs[:round] if kwargs.has_key?( :round )
62
+ @group = kwargs[:group] if kwargs.has_key?( :group )
63
+ @status = kwargs[:status] if kwargs.has_key?( :status )
64
+
65
+ @ground = kwargs[:ground] if kwargs.has_key?( :ground )
66
+ @att = kwargs[:att] if kwargs.has_key?( :att )
67
+
68
+
69
+ if kwargs.has_key?( :score ) ## check all-in-one score struct for convenience!!!
70
+ score = kwargs[:score]
71
+
72
+ if score.nil? ## reset all score attribs to nil!!
73
+ @score = [] ## [nil,nil]
74
+ else
75
+ ## check if is array - assume "generic" score e.g. 3-2
76
+ ## that is, not known if full-time, after extra-time etc.
77
+ if score.is_a?( Array )
78
+ @score = score ## e.g. [3,2]
79
+ else ## assume hash
80
+ @score = score
81
+ # @score1, @score2 = score[:ft] || []
82
+ # @score1i, @score2i = score[:ht] || []
83
+ # @score1et, @score2et = score[:et] || []
84
+ # @score1p, @score2p = score[:p] || score[:pen] || []
85
+ # @score1agg, @score2agg = score[:agg] || []
86
+ end
87
+ end
88
+ end
89
+ # @score[:ht] = kwargs[:score_ht] if kwargs.has_key?( :score_ht )
90
+ # @score[:et] = kwargs[:score_et] if kwargs.has_key?( :score_et )
91
+ # @score[:p] = kwargs[:score_p] if kwargs.has_key?( :score_p )
92
+ # @score[:agg] = kwargs[:score_agg] if kwargs.has_key?( :score_agg )
93
+
94
+ ## note: (always) (auto-)convert scores to integers
95
+ # @score1 = @score1.to_i(10) if @score1
96
+ # @score1i = @score1i.to_i(10) if @score1i
97
+ # @score1et = @score1et.to_i(10) if @score1et
98
+ # @score1p = @score1p.to_i(10) if @score1p
99
+ # @score1agg = @score1agg.to_i(10) if @score1agg
100
+
101
+ # @score2 = @score2.to_i(10) if @score2
102
+ # @score2i = @score2i.to_i(10) if @score2i
103
+ # @score2et = @score2et.to_i(10) if @score2et
104
+ # @score2p = @score2p.to_i(10) if @score2p
105
+ # @score2agg = @score2agg.to_i(10) if @score2agg
106
+
107
+ ## todo/fix:
108
+ ## gr-greece/2014-15/G1.csv:
109
+ ## G1,10/05/15,Niki Volos,OFI,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
110
+ ##
111
+
112
+ ## for now score1 and score2 must be present
113
+ ## if @score1.nil? || @score2.nil?
114
+ ## puts "** WARN: missing scores for match:"
115
+ ## pp kwargs
116
+ ## ## exit 1
117
+ ## end
118
+
119
+
120
+ self ## note - MUST return self for chaining
121
+ end
122
+
123
+
124
+ ####
125
+ ## deprecated - use score.to_s and friends - why? why not?
126
+ # def score_str # pretty print (full time) scores; convenience method
127
+ # "#{@score1}-#{@score2}"
128
+ # end
129
+
130
+ # def scorei_str # pretty print (half time) scores; convenience method
131
+ # "#{@score1i}-#{@score2i}"
132
+ # end
133
+
134
+
135
+ def as_json
136
+ #####
137
+ ## note - use string keys (NOT symbol for data keys)
138
+ ## for easier json compatibility
139
+ data = {}
140
+
141
+ ## check round
142
+ if @round
143
+ data['round'] = if round.is_a?( Integer )
144
+ "Matchday #{@round}"
145
+ else ## assume string
146
+ @round
147
+ end
148
+ end
149
+
150
+
151
+ data['num'] = @num if @num
152
+ if @date
153
+ ## assume 2020-09-19 date format!!
154
+ data['date'] = @date.is_a?( String ) ? @date : @date.strftime('%Y-%m-%d')
155
+
156
+ data['time'] = @time if @time
157
+ data['time_local'] = @time_local if @time_local
158
+ end
159
+
160
+
161
+ data['team1'] = @team1.is_a?( String ) ? @team1 : @team1.name
162
+
163
+ ## note - for match status bye team2 is nil!!!
164
+ ## e.g. Queen's Park bye
165
+ ## Wanderers bye
166
+ ## todo/check - keep bye as a match - why? why not?
167
+ ## has no date/time & venue & score etc.
168
+ if @team2
169
+ data['team2'] = @team2.is_a?( String ) ? @team2 : @team2.name
170
+ end
171
+
172
+ ## note - score might be
173
+ ## 1) array e.g. [0,1]
174
+ ## 2) hash e.g. { ft: [0,1] } etc.
175
+ ## note - w/o (walkout) do NOT add empty score
176
+ if @score.is_a?(Hash)
177
+ # note: make sure hash keys are always strings
178
+ data['score'] = @score.transform_keys(&:to_s)
179
+ elsif @score.is_a?(Array)
180
+ ## note:
181
+ ## for now always assume full-time (ft)
182
+ ## in future check for score note or such
183
+ ## to use "plain" array or such - why? why not?
184
+ ## data['score'] = { 'ft' => @score } if !@score.empty?
185
+
186
+ data['score'] = @score if !@score.empty?
187
+ end
188
+
189
+
190
+ ## data['score']['ht'] = [@score1i, @score2i] if @score1i && @score2i
191
+ ## data['score']['ft'] = [@score1, @score2] if @score1 && @score2
192
+ ## data['score']['et'] = [@score1et, @score2et] if @score1et && @score2et
193
+ ## data['score']['p'] = [@score1p, @score2p] if @score1p && @score2p
194
+
195
+
196
+
197
+ ### check for goals
198
+ if @goals && @goals.size > 0
199
+ data['goals1'] = []
200
+ data['goals2'] = []
201
+
202
+ @goals.each do |goal|
203
+ node = {}
204
+ node['name'] = goal.player
205
+
206
+ ## note - use a string for minutes for now
207
+ ## allows e.g. 45+2 etc. too
208
+ minute_str = "#{goal.minute}"
209
+ minute_str += "+#{goal.offset}" if goal.offset
210
+
211
+ node['minute'] = minute_str
212
+
213
+ node['owngoal'] = true if goal.owngoal
214
+ node['penalty'] = true if goal.penalty
215
+
216
+ if goal.team == 1
217
+ data['goals1'] << node
218
+ else ## assume 2
219
+ data['goals2'] << node
220
+ end
221
+ end # each goal
222
+ end
223
+
224
+
225
+ data['status'] = @status if @status
226
+
227
+ data['group'] = @group if @group
228
+
229
+ if @ground
230
+ ## note: might be array of string e.g. ['Wembley', 'London']
231
+ ##
232
+ ## todo/check - auto-join to string - why? why not?
233
+ ## e.g. ['Wembley', 'London']
234
+ ## to 'Wembley, London'
235
+ ## note - auto-join geo tree for now
236
+ data['ground'] = @ground.join(', ')
237
+ end
238
+
239
+ data['attendance'] = @att if @att
240
+ data
241
+ end
242
+ end # class Match
243
+
244
+
245
+
246
+ end # class MatchTree
247
+ end # module SportDb
@@ -0,0 +1,36 @@
1
+
2
+ module SportDb
3
+ class MatchTree
4
+
5
+
6
+ class Round
7
+ attr_reader :name, :start_date, :end_date
8
+
9
+ def initialize( name:,
10
+ start_date: nil,
11
+ end_date: nil,
12
+ auto: true )
13
+ @name = name
14
+ @start_date = start_date
15
+ @end_date = end_date
16
+ @auto = auto # auto-created (inline reference/header without proper definition before)
17
+ end
18
+
19
+ def pretty_print( printer )
20
+ ## todo/check - how to display/format key - use () or not - why? why not?
21
+ buf = String.new
22
+ buf << "<Round"
23
+ buf << " AUTO" if @auto
24
+ buf << ": "
25
+ buf << "#{@name}, "
26
+ buf << "#{@start_date}"
27
+ buf << " - #{@end_date}" if @start_date != @end_date
28
+ buf << ">"
29
+
30
+ printer.text( buf )
31
+ end
32
+ end # class Round
33
+
34
+
35
+ end # class MatchTree
36
+ end # module SportDb
@@ -0,0 +1,81 @@
1
+
2
+ module SportDb
3
+ class MatchTree
4
+
5
+ def log( msg )
6
+ ## append msg to ./logs.txt
7
+ ## use ./errors.txt - why? why not?
8
+ File.open( './logs.txt', 'a:utf-8' ) do |f|
9
+ f.write( msg )
10
+ f.write( "\n" )
11
+ end
12
+ end
13
+
14
+
15
+ ### check - rename last_year to running_last_year to make intent clearer - why? why not?
16
+ def _build_date( m:, d:, y:, yy:, wday:,
17
+ start:,
18
+ last_year: )
19
+
20
+ if m.nil? || d.nil?
21
+ puts "[debug] !! ERROR - _build_date required month or day missing:"
22
+ pp [m,d,y,yy,wday,start]
23
+ exit 1
24
+ end
25
+
26
+
27
+ ## quick debug hack
28
+ if m == 2 && d == 29
29
+ puts "quick check feb/29 dates"
30
+ pp [d,m,y]
31
+ pp start
32
+ end
33
+
34
+
35
+ ####
36
+ ## support two digit shortcut for year
37
+ if yy
38
+ ###
39
+ ## for now assume 00,01 to 30 is 2000,2001 to 2030
40
+ ## and 31 to 99 is 1931 to 1999
41
+ y = yy <= 30 ? 2000+yy : 1900+yy
42
+ end
43
+
44
+
45
+ if y.nil? ## try to calculate year
46
+ if last_year && @last_year ## use new formula
47
+ y = @last_year
48
+ elsif start.nil?
49
+ puts "!! ERROR - _build_date - year expected for (first) date; cannot infer/guess; sorry"
50
+ exit 1
51
+ else ## fallback to "old" formula - FIX/FIX remove later
52
+ ## puts "[deprecated] WARN - do NOT use old year (date) auto-complete; add year to first date"
53
+ y = if m > start.month ||
54
+ (m == start.month && d >= start.day)
55
+ # assume same year as start_at event (e.g. 2013 for 2013/14 season)
56
+ start.year
57
+ else
58
+ # assume year+1 as start_at event (e.g. 2014 for 2013/14 season)
59
+ start.year+1
60
+ end
61
+ end
62
+ else
63
+ ### note - reset @start to new date
64
+ ## use @last_year
65
+ if last_year
66
+ @last_year = y
67
+ puts " [debug] _build_date - set running last_year to #{y}"
68
+ end
69
+ end
70
+
71
+
72
+ date = Date.new( y,m,d ) ## y,m,d
73
+
74
+ ### todo/fix
75
+ ### check/validate wday here
76
+
77
+ date
78
+ end
79
+
80
+ end ## class MatchTree
81
+ end ## module SportDb
@@ -0,0 +1,162 @@
1
+ module SportDb
2
+
3
+
4
+ ##############################
5
+ ## simple (match) parse tree to structs walker/handler/converter
6
+ class MatchTree
7
+ def self.debug=(value) @@debug = value; end
8
+ def self.debug?() @@debug ||= false; end ## note: default is FALSE
9
+ def debug?() self.class.debug?; end
10
+
11
+ include Logging ## e.g. logger#debug, logger#info, etc.
12
+
13
+
14
+
15
+ ##
16
+ ## note: allow start(_date) nil
17
+ ## if in use (start: nil) years expected on first date!!!
18
+
19
+ def initialize( tree, start: nil )
20
+ @tree = tree
21
+ @start = start
22
+
23
+ @errors = []
24
+ end
25
+
26
+ attr_reader :errors
27
+ def errors?() @errors.size > 0; end
28
+
29
+
30
+ def convert
31
+ ## note: every (new) read call - resets errors list to empty
32
+ @errors = []
33
+ @warns = [] ## track list of warnings (unmatched lines) too - why? why not?
34
+
35
+ ### todo/fix - FIX/FIX
36
+ ## check start year from first date
37
+ ## for now (auto-)update - @start with every date that incl. a year!!!
38
+ @last_year = nil
39
+ @last_date = nil
40
+ @last_time = nil
41
+
42
+ ## todo/fix - use stack push/pop in the future - why? why not?
43
+ @last_round = nil ## merge - "top-level" - Round struct
44
+ @last_round_name1 = nil ## level 1 - string
45
+ @last_round_name2 = nil ## level 2 - string
46
+ @last_round_name3 = nil ## level 3 - string
47
+
48
+ @last_group = nil
49
+
50
+
51
+ @teams = Hash.new(0) ## track counts (only) for now for (interal) team stats - why? why not?
52
+ @rounds = {}
53
+ @groups = {}
54
+ @matches = []
55
+
56
+
57
+ @tree.each do |node|
58
+ if node.is_a? RaccMatchParser::RoundDef
59
+ ## todo/fix: add round definition (w begin n end date)
60
+ ## todo: do not patch rounds with definition (already assume begin/end date is good)
61
+ ## -- how to deal with matches that get rescheduled/postponed?
62
+ on_round_def( node )
63
+ elsif node.is_a? RaccMatchParser::GroupDef ## NB: group goes after round (round may contain group marker too)
64
+ ### todo: add pipe (|) marker (required)
65
+ on_group_def( node )
66
+ elsif node.is_a? RaccMatchParser::RoundOutline
67
+ on_round_outline( node )
68
+ elsif node.is_a? RaccMatchParser::DateHeader
69
+ on_date_header( node )
70
+ elsif node.is_a? RaccMatchParser::MatchLine
71
+ on_match_line( node )
72
+ elsif node.is_a? RaccMatchParser::MatchLineWalkover
73
+ on_match_line_walkover( node )
74
+ elsif node.is_a? RaccMatchParser::MatchLineBye
75
+ on_match_line_bye( node )
76
+ elsif node.is_a? RaccMatchParser::GoalLine
77
+ on_goal_line( node )
78
+ elsif node.is_a?( RaccMatchParser::LineupLine ) ||
79
+ node.is_a?( RaccMatchParser::RefereeLine )
80
+ ## skip lineup, referee props for now
81
+ elsif node.is_a?( RaccMatchParser::Heading1 ) ||
82
+ node.is_a?( RaccMatchParser::Heading2 ) ||
83
+ node.is_a?( RaccMatchParser::Heading3 )
84
+ ### skip headings (1/2/3) for now
85
+ elsif node.is_a?( RaccMatchParser::BlankLine )
86
+ ### skip for now; do nothing
87
+ else
88
+ ## report error
89
+ msg = "!! WARN - unknown node (parse tree type) - #{node.class.name}"
90
+ puts msg
91
+ pp node
92
+
93
+ log( msg )
94
+ log( node.pretty_inspect )
95
+ end
96
+ end # tree.each
97
+
98
+ ## note - team keys are names and values are "internal" stats e.g. usage count!!
99
+ ## and NOT team/club/nat_team structs!!
100
+ [@teams.keys, @matches, @rounds.values, @groups.values]
101
+ end # method convert
102
+
103
+
104
+
105
+
106
+ def on_match_line_walkover( node )
107
+ logger.debug( "on match (w/o): >#{node}<" )
108
+
109
+ ## note - w/o (walkover) records NO date/time or ground (or score etc.)
110
+ ## for now only team1/team2 and match status!!
111
+ ## plus inherited round/group
112
+
113
+ status = 'walkover' ## use w/o - why? why not?
114
+
115
+ team1 = node.team1
116
+ team2 = node.team2
117
+
118
+ @teams[ team1 ] += 1
119
+ @teams[ team2 ] += 1
120
+
121
+
122
+ group = nil
123
+ group = @last_group if @last_group
124
+
125
+ round = nil
126
+ round = @last_round if @last_round
127
+
128
+ @matches << Match.new( team1: team1, ## note: for now always use mapping value e.g. rec (NOT string e.g. team1.name)
129
+ team2: team2, ## note: for now always use mapping value e.g. rec (NOT string e.g. team2.name)
130
+ round: round ? round.name : nil, ## note: for now always use string (assume unique canonical name for event)
131
+ group: group ? group.name : nil, ## note: for now always use string (assume unique canonical name for event)
132
+ status: status )
133
+ ### todo: cache team lookups in hash?
134
+ end
135
+
136
+ def on_match_line_bye( node )
137
+ logger.debug( "on match (bye): >#{node}<" )
138
+
139
+ ## note - bye records NO date/time or ground (or score etc.)
140
+ ## for now only team1/team2 and match status!!
141
+ ## plus inherited round/group
142
+
143
+ status = 'bye'
144
+
145
+ team = node.team
146
+
147
+ @teams[ team ] += 1
148
+
149
+ group = nil
150
+ group = @last_group if @last_group
151
+
152
+ round = nil
153
+ round = @last_round if @last_round
154
+
155
+ @matches << Match.new( team1: team, ## note: for now always use mapping value e.g. rec (NOT string e.g. team1.name)
156
+ round: round ? round.name : nil, ## note: for now always use string (assume unique canonical name for event)
157
+ group: group ? group.name : nil, ## note: for now always use string (assume unique canonical name for event)
158
+ status: status )
159
+ ### todo: cache team lookups in hash?
160
+ end
161
+ end # class MatchTree
162
+ end # module SportDb
@@ -0,0 +1,45 @@
1
+ module SportDb
2
+ class MatchTree
3
+
4
+
5
+
6
+
7
+ def on_date_header( node )
8
+ logger.debug( "date header: >#{node}<")
9
+
10
+ date = _build_date( m: node.date[:m],
11
+ d: node.date[:d],
12
+ y: node.date[:y],
13
+ yy: node.date[:yy],
14
+ wday: node.date[:wday],
15
+ start: @start,
16
+ last_year: true )
17
+
18
+ logger.debug( " date: #{date} with start: #{@start}")
19
+
20
+ @last_date = date # keep a reference for later use
21
+ @last_time = nil
22
+
23
+ ### quick "corona" hack - support seasons going beyond 12 month (see swiss league 2019/20 and others!!)
24
+ ## find a better way??
25
+ ## set @start date to full year (e.g. 1.1.) if date.year is @start.year+1
26
+ ## todo/fix: add to linter to check for chronological dates!! - warn if NOT chronological
27
+ ### todo/check: just turn on for 2019/20 season or always? why? why not?
28
+
29
+ ## todo/fix: add switch back to old @start_org
30
+ ## if year is date.year == @start.year-1 -- possible when full date with year set!!!
31
+ =begin
32
+ if @start.month != 1
33
+ if date.year == @start.year+1
34
+ logger.debug( "!! hack - extending start date to full (next/end) year; assumes all dates are chronologigal - always moving forward" )
35
+ @start_org = @start ## keep a copy of the original (old) start date - why? why not? - not used for now
36
+ @start = Date.new( @start.year+1, 1, 1 )
37
+ end
38
+ end
39
+ =end
40
+ end
41
+
42
+
43
+
44
+ end ## class MatchTree
45
+ end ## module SportDb