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,87 @@
1
+ module SportDb
2
+ class MatchTree
3
+
4
+
5
+ def on_goal_line( node )
6
+ logger.debug "on goal line: >#{node}<"
7
+
8
+ goals1 = node.goals1
9
+ goals2 = node.goals2
10
+
11
+
12
+ pp [goals1,goals2] if debug?
13
+
14
+
15
+ ## special rule
16
+ ## if goals 2 empty check if score for team 1 is zero
17
+ ## and team 2 is NOT zero than
18
+ ## make goals1 goald2!!
19
+ ## e.g. Norway 0-1 Austria
20
+ ## (Hof 32)
21
+
22
+ if goals2.empty? && !goals1.empty?
23
+
24
+ match = @matches[-1]
25
+
26
+ ##
27
+ ## todo/fix
28
+ ## move upstream
29
+ ## use score1_zero? or such - why? why not?
30
+ if (match.score.is_a?(Array) && match.score[0] == 0 ) ||
31
+ (match.score.is_a?(Hash) && match.score[:et] && match.score[:et][0] == 0) ||
32
+ (match.score.is_a?(Hash) && match.score[:et].nil? &&
33
+ match.score[:ft] && match.score[:ft][0] == 0)
34
+ ## "parallel assignment (or multiple assignment") - swap values in single line
35
+ goals2, goals1 = goals1, goals2
36
+ end
37
+ end
38
+
39
+
40
+
41
+ goals = []
42
+
43
+ goals1.each do |rec|
44
+ rec.minutes.each do |minute|
45
+ goal = Goal.new(
46
+ player: rec.player,
47
+ team: 1,
48
+ minute: minute.m,
49
+ offset: minute.offset,
50
+ penalty: minute.pen || false, # note: pass along/use false NOT nil
51
+ owngoal: minute.og || false
52
+ )
53
+ goals << goal
54
+ end
55
+ end
56
+
57
+ goals2.each do |rec|
58
+ rec.minutes.each do |minute|
59
+ goal = Goal.new(
60
+ player: rec.player,
61
+ team: 2,
62
+ minute: minute.m,
63
+ offset: minute.offset,
64
+ penalty: minute.pen || false, # note: pass along/use false NOT nil
65
+ owngoal: minute.og || false
66
+ )
67
+ goals << goal
68
+ end
69
+ end
70
+
71
+ pp goals if debug?
72
+
73
+ ## quick & dirty - auto add goals to last match
74
+ ## note - for hacky (quick& dirty) multi-line support
75
+ ## always append for now
76
+ match = @matches[-1]
77
+ match.goals ||= []
78
+ match.goals += goals
79
+
80
+ ## todo/fix
81
+ ## sort by minute
82
+ ## PLUS auto-fill score1,score2 - why? why not?
83
+ end
84
+
85
+
86
+ end ## class MatchTree
87
+ end ## module SportDb
@@ -0,0 +1,27 @@
1
+ module SportDb
2
+ class MatchTree
3
+
4
+
5
+ def on_group_def( node )
6
+ logger.debug "on group def: >#{node}<"
7
+
8
+ ## e.g
9
+ ## [:group_def, "Group A"],
10
+ ## [:team, "Germany"],
11
+ ## [:team, "Scotland"],
12
+ ## [:team, "Hungary"],
13
+ ## [:team, "Switzerland"]
14
+
15
+ node.teams.each do |team|
16
+ @teams[ team ] += 1
17
+ end
18
+
19
+ group = Group.new( name: node.name,
20
+ teams: node.teams )
21
+
22
+ @groups[ node.name ] = group
23
+ end
24
+
25
+
26
+ end ## class MatchTree
27
+ end ## module SportDb
@@ -0,0 +1,195 @@
1
+ module SportDb
2
+ class MatchTree
3
+
4
+
5
+
6
+ def on_match_line( node )
7
+ logger.debug( "on match: >#{node}<" )
8
+
9
+ ## collect (possible) nodes by type
10
+ num = nil
11
+ num = node.num if node.num
12
+
13
+ date = nil
14
+ date = _build_date( m: node.date[:m],
15
+ d: node.date[:d],
16
+ y: node.date[:y],
17
+ yy: node.date[:yy],
18
+ wday: node.date[:wday],
19
+ start: @start,
20
+ last_year: true ) if node.date
21
+
22
+ ## note - there's no time (-only) type in ruby
23
+ ## use string (e.g. '14:56', '1:44')
24
+ ## use 01:44 or 1:44 ?
25
+ ## check for 0:00 or 24:00 possible?
26
+ time = nil
27
+ if node.time
28
+ time = ('%d:%02d' % [node.time[:h], node.time[:m]])
29
+ ## check for timezone
30
+ time += " #{node.time[:timezone]}" if node.time[:timezone]
31
+ end
32
+
33
+
34
+
35
+ ## todo/fix -
36
+ ## keep time & time_local as pairs for @last_time/@last_time_local
37
+ ## - check for timezone
38
+ ## incl. timezone in time (string) - why? why not?
39
+ time_local = nil
40
+ if node.time_local
41
+ time_local = ('%d:%02d' % [node.time_local[:h], node.time_local[:m]])
42
+ time_local += " #{node.time_local[:timezone]}" if node.time_local[:timezone]
43
+ end
44
+
45
+
46
+ ### todo/fix
47
+ ## add keywords (e.g. ht, ft or such) to Score.new - why? why not?
48
+ ## or use new Score.build( ht:, ft:, ) or such - why? why not?
49
+ ## pp score
50
+ score = nil
51
+ score = node.score if node.score
52
+
53
+ ## if node.score.is_a?(Array)
54
+ ## ## assume "undefined" score
55
+ ## score = node.score
56
+ ## else ## (default) assume Hash
57
+ ## # ht = node.score[:ht] || [nil,nil]
58
+ ## # ft = node.score[:ft] || [nil,nil]
59
+ ## # et = node.score[:et] || [nil,nil]
60
+ ## # p = node.score[:p] || [nil,nil]
61
+ ## # values = [*ht, *ft, *et, *p]
62
+ ## # pp values
63
+ ## ## pp node.score
64
+ ## score = node.score
65
+ ## end
66
+ ## end
67
+
68
+
69
+ status = nil
70
+ status = node.status if node.status ### assume text for now
71
+ ## if node_type == :status # e.g. awarded, canceled, postponed, etc.
72
+ ## status = node[1]
73
+ #
74
+ ## todo - add ## find (optional) match status e.g. [abandoned] or [replay] or [awarded]
75
+ ## or [cancelled] or [postponed] etc.
76
+ ## status = find_status!( line ) ## todo/check: allow match status also in geo part (e.g. after @) - why? why not?
77
+
78
+
79
+
80
+ team1 = node.team1
81
+ team2 = node.team2
82
+
83
+ @teams[ team1 ] += 1
84
+ @teams[ team2 ] += 1
85
+
86
+
87
+ if node.header ## note - date/time for matches (w/ header) CANNOT get inherited!!
88
+ @last_date = nil
89
+ @last_time = nil
90
+ else ## no (match header), use date/time inheritance rules
91
+ ###
92
+ # check if date found?
93
+ # note: ruby falsey is nil & false only (not 0 or empty array etc.)
94
+ if date
95
+ ### check: use date_v2 if present? why? why not?
96
+ @last_date = date # keep a reference for later use
97
+ @last_time = nil
98
+ # @last_time = nil
99
+ else
100
+ date = @last_date # no date found; (re)use last seen date
101
+ end
102
+
103
+ if time
104
+ @last_time = time
105
+ else
106
+ time = @last_time
107
+ end
108
+ end
109
+
110
+
111
+
112
+ group = nil
113
+ group = @last_group if @last_group
114
+
115
+ round = nil
116
+ round = @last_round if @last_round
117
+
118
+
119
+ ### try auto-fill round
120
+ ## find (first) matching round by date if rounds / matchdays defined
121
+ ## if not rounds / matchdays defined - YES, allow matches WITHOUT rounds!!!
122
+ if date && round.nil?
123
+ if @rounds.size > 0
124
+ @rounds.values.each do |round_rec|
125
+ ## note: convert date to date only (no time) with to_date!!!
126
+ if (round_rec.start_date && round_rec.end_date) &&
127
+ (date.to_date >= round_rec.start_date &&
128
+ date.to_date <= round_rec.end_date)
129
+ round = round_rec
130
+ break
131
+ end
132
+ end
133
+ if round.nil?
134
+ ## todo/fix - issue a warning (do NOT stop)
135
+ puts "!! PARSE ERROR - no matching round found for match date:"
136
+ pp date
137
+ exit 1
138
+ end
139
+ end
140
+ end
141
+
142
+
143
+ ## todo/check: scores are integers or strings?
144
+
145
+ ## todo/check: pass along round and group refs or just string (canonical names) - why? why not?
146
+
147
+ ## split date in date & time if DateTime
148
+ =begin
149
+ time_str = nil
150
+ date_str = nil
151
+ if date.is_a?( DateTime )
152
+ date_str = date.strftime('%Y-%m-%d')
153
+ time_str = date.strftime('%H:%M')
154
+ elsif date.is_a?( Date )
155
+ date_str = date.strftime('%Y-%m-%d')
156
+ else # assume date is nil
157
+ end
158
+ =end
159
+
160
+ time_str = nil
161
+ time_local_str = nil
162
+ date_str = nil
163
+
164
+ date_str = date.strftime('%Y-%m-%d') if date
165
+ time_str = time if date && time
166
+ time_local_str = time_local if date && time_local
167
+
168
+
169
+
170
+ ground = nil
171
+ ground = node.geo if node.geo
172
+
173
+ ## attendance
174
+ att = nil
175
+ att = node.att if node.att
176
+
177
+
178
+ @matches << Match.new( num: num,
179
+ date: date_str,
180
+ time: time_str,
181
+ time_local: time_local_str,
182
+ team1: team1, ## note: for now always use mapping value e.g. rec (NOT string e.g. team1.name)
183
+ team2: team2, ## note: for now always use mapping value e.g. rec (NOT string e.g. team2.name)
184
+ score: score,
185
+ round: round ? round.name : nil, ## note: for now always use string (assume unique canonical name for event)
186
+ group: group ? group.name : nil, ## note: for now always use string (assume unique canonical name for event)
187
+ status: status,
188
+ ground: ground,
189
+ att: att )
190
+ ### todo: cache team lookups in hash?
191
+ end
192
+
193
+
194
+ end ## class MatchTree
195
+ end ## module SportDb
@@ -0,0 +1,85 @@
1
+ module SportDb
2
+ class MatchTree
3
+
4
+
5
+ def on_round_def( node )
6
+ logger.debug "on round def: >#{node}<"
7
+
8
+ ## e.g. [[:round_def, "Matchday 1"], [:duration, "Fri Jun 14 - Tue Jun 18"]]
9
+ ## [[:round_def, "Matchday 2"], [:duration, "Wed Jun 19 - Sat Jun 22"]]
10
+ ## [[:round_def, "Matchday 3"], [:duration, "Sun Jun 23 - Wed Jun 26"]]
11
+
12
+ name = node.name
13
+ # NB: use extracted round name for knockout check
14
+ # knockout_flag = is_knockout_round?( name )
15
+
16
+
17
+ ##
18
+ ## note - do NOT update @last_year on round def dates!!
19
+ ## only update for running dates in matches!!
20
+
21
+ if node.date
22
+ start_date = end_date = _build_date( m: node.date[:m],
23
+ d: node.date[:d],
24
+ y: node.date[:y],
25
+ yy: node.date[:yy],
26
+ wday: node.date[:wday],
27
+ start: @start,
28
+ last_year: false )
29
+ elsif node.duration
30
+ ## reuse year in start date e.g. July 26-27 1930
31
+ ## July 26 [1930], [July] 27 1930
32
+ start_date = _build_date( m: node.duration[:start][:m],
33
+ d: node.duration[:start][:d],
34
+ y: node.duration[:start][:y] || node.duration[:end][:y],
35
+ yy: node.duration[:start][:yy] || node.duration[:end][:yy],
36
+ wday: node.duration[:start][:wday],
37
+ start: @start,
38
+ last_year: false )
39
+
40
+ ## reuse month in end date e.g. July 26-27
41
+ ## July 26, [July] 27
42
+ end_date = _build_date( m: node.duration[:end][:m] || node.duration[:start][:m],
43
+ d: node.duration[:end][:d],
44
+ y: node.duration[:end][:y],
45
+ yy: node.duration[:end][:yy],
46
+ wday: node.duration[:end][:wday],
47
+ start: @start,
48
+ last_year: false )
49
+ else
50
+ puts "!! PARSE ERROR - expected date or duration for round def; got:"
51
+ pp node
52
+ exit 1
53
+ end
54
+
55
+ # note: - NOT needed; start_at and end_at are saved as date only (NOT datetime)
56
+ # set hours,minutes,secs to beginning and end of day (do NOT use default 12.00)
57
+ # e.g. use 00.00 and 23.59
58
+ # start_at = start_at.beginning_of_day
59
+ # end_at = end_at.end_of_day
60
+
61
+ # note: make sure start_at/end_at is date only (e.g. use start_at.to_date)
62
+ # sqlite3 saves datetime in date field as datetime, for example (will break date compares later!)
63
+
64
+ # note - _build_date always returns Date for now - no longer needed!!
65
+ # start_date = start_date.to_date
66
+ # end_date = end_date.to_date
67
+
68
+
69
+
70
+
71
+ logger.debug " start_date: #{start_date}"
72
+ logger.debug " end_date: #{end_date}"
73
+ logger.debug " name: >#{name}<"
74
+
75
+ round = Round.new( name: name,
76
+ start_date: start_date,
77
+ end_date: end_date,
78
+ auto: false )
79
+
80
+ @rounds[ name ] = round
81
+ end
82
+
83
+
84
+ end ## class MatchTree
85
+ end ## module SportDb
@@ -0,0 +1,104 @@
1
+ module SportDb
2
+ class MatchTree
3
+
4
+
5
+
6
+ def on_round_outline( node )
7
+ logger.debug "on round outline: >#{node}<"
8
+
9
+ ## always reset dates - why? why not?
10
+ ## note - needs last_date for year
11
+ ## track last_year with extra variable
12
+
13
+ name = node.outline
14
+ level = node.level
15
+
16
+
17
+ ####
18
+ # check for "old" group header in ("automagic") round outline for now
19
+ ##
20
+ ## todo/fix - use only names from group def for lookup/is_group match!!!
21
+ ## do NOT use (generic) regex!!
22
+ if level == 1 && _is_group?( name )
23
+ logger.debug "on group header: >#{node}<"
24
+
25
+ group = @groups[ name ]
26
+
27
+ if group
28
+ # set group for matches
29
+ @last_group = group
30
+ # note: group header resets (last) round (allows, for example):
31
+ # e.g.
32
+ # Group Playoffs/Replays -- round header
33
+ # team1 team2 -- match
34
+ # Group B -- group header
35
+ # team1 team2 - match (will get new auto-matchday! not last round)
36
+ @last_round = nil
37
+ return ## note - return here; do NOT fall through to std round processing!
38
+ else
39
+ puts "!! WARN - no group def found for >#{name}<; will use a (plain) round"
40
+ end
41
+ end ## is_group?
42
+
43
+
44
+ ##
45
+ ## todo/fix - also reset round name levels on heading 1/2/3 etc.
46
+ ## why? why not?
47
+
48
+ if level == 1
49
+ @last_round_name1 = name
50
+ @last_round_name2 = nil
51
+ @last_round_name3 = nil
52
+ elsif level == 2
53
+ @last_round_name2 = name
54
+ @last_round_name3 = nil
55
+ name = [@last_round_name1, name].join( ', ' )
56
+ elsif level == 3
57
+ @last_round_name3 == name
58
+ name = [@last_round_name1, @last_round_name2, name].join( ', ')
59
+ else
60
+ puts "!! ERROR - unsupported round outline level #{level}; use 1-3 - sorry"
61
+ exit 1
62
+ end
63
+
64
+
65
+ round = @rounds[ name ]
66
+ if round.nil? ## auto-add / create if missing
67
+ round = Round.new( name: name )
68
+ @rounds[ name ] = round
69
+ end
70
+
71
+ @last_round = round
72
+ @last_group = nil # note: always reset group to no group - why? why not?
73
+
74
+ ## todo/fix/check
75
+ ## make round a scope for date(time) - why? why not?
76
+ ## reset date/time e.g. @last_date = nil !!!!
77
+ end
78
+
79
+
80
+
81
+ ###
82
+ ## helpers
83
+
84
+ ##
85
+ ## note - do NOT match group phase or group playoff or such
86
+ ## for now only works for group a,b,c or group 1, group 2, etc.
87
+
88
+ GROUP_RE = %r{\A
89
+ Group [ ] (?:
90
+ [a-z]
91
+ | \d+
92
+ )
93
+ \z}ix
94
+
95
+
96
+ def _is_group?( text )
97
+ ## use regex for match
98
+ GROUP_RE.match?( text )
99
+ end
100
+
101
+
102
+
103
+ end ## class MatchTree
104
+ end ## module SportDb
@@ -24,7 +24,10 @@ class QuickMatchReader
24
24
 
25
25
  def initialize( txt )
26
26
  @errors = []
27
- @outline = QuickLeagueOutline.parse( txt )
27
+ @txt = txt
28
+
29
+ @league_name = ''
30
+ @matches = []
28
31
  end
29
32
 
30
33
  attr_reader :errors
@@ -35,59 +38,82 @@ class QuickMatchReader
35
38
  # helpers get matches & more after parse; all stored in data
36
39
  #
37
40
  ## change/rename to event - why? why not?
38
- def league_name
39
- league = @data.keys[0]
40
- season = @data[ league ].keys[0]
41
-
42
- "#{league} #{season}"
43
- end
44
-
45
- def matches
46
- league = @data.keys[0]
47
- season = @data[ league ].keys[0]
48
- @data[league][season]
49
- end
50
-
41
+ ## note - may or may not include season
42
+ def league_name() @league_name; end
43
+ def matches() @matches; end
44
+
45
+
46
+ ## try to find season in heading
47
+ ## e.g. Österr. Bundesliga 2015/16 or 2015-16
48
+ ## World Cup 2018 or 2018 World Cup etc.
49
+ SEASON_IN_HEADING_RE = %r{
50
+ \b
51
+ (?<season>\d{4}
52
+ (?:[\/-]\d{1,4})? ## optional 2nd year in season
53
+ )\b
54
+ }x
51
55
 
52
56
 
53
57
  def parse
54
58
  ## note: every (new) read call - resets errors list to empty
55
59
  @errors = []
60
+
61
+ @league_name = ''
62
+ @matches = []
63
+
64
+
65
+ ## note - source file MUST always start with heading 1 for now
66
+ tree = []
67
+ parser = RaccMatchParser.new( @txt, debug: debug? ) ## use own parser instance (not shared) - why? why not?
68
+ tree = parser.parse
69
+
70
+
71
+ ##
72
+ ## !! (QUICK) PARSE ERROR - source MUST start with Heading1; got 34 nodes:
73
+ ## [<BlankLine>,
74
+ ## <BlankLine>,
75
+ ## <Heading1 World Cup 1930>,
76
+
77
+ ## remove leading BlankLines if any!!
78
+ while tree[0].is_a? RaccMatchParser::BlankLine
79
+ tree.shift ## remove (leading) blank line from parse tree
80
+ end
56
81
 
57
- @data = {} # return data hash with leagues
58
- # and seasons
59
- # for now merge stage into matches
60
82
 
61
- @outline.each_sec do |sec| ## sec(tion)s
62
- ### move season parse into outline upstream - why? why not?
63
- season = Season.parse( sec.season ) ## convert (str) to season obj!!!
64
- league = sec.league
65
- stage = sec.stage
66
- lines = sec.lines
83
+ if tree[0].is_a? RaccMatchParser::Heading1
84
+ ## try to get league and season
85
+ @league_name = tree[0].text
86
+ else
87
+ ### report error - source MUST start with heading 1
88
+ puts "!! (QUICK) PARSE ERROR - source MUST start with Heading1; got #{tree.size} nodes:"
89
+ pp tree
90
+ exit
91
+ end
67
92
 
93
+ ## todo/fix
94
+ ## make season optional
95
+ m = SEASON_IN_HEADING_RE.match( @league_name )
96
+ if m.nil?
97
+ puts "!! (QUICK) PARSE ERROR - no season found in Heading1 >#{@league_name}; sorry"
98
+ exit
99
+ end
100
+ season = Season.parse( m[:season] ) ## convert (str) to season obj!!!
68
101
  start = if season.year?
69
102
  Date.new( season.start_year, 1, 1 )
70
103
  else
71
104
  Date.new( season.start_year, 7, 1 )
72
105
  end
73
106
 
74
- # if debug?
75
- # puts " (sec) lines:"
76
- # pp lines
77
- # end
78
-
79
- ### note - skip section if no lines !!!!!
80
- next if lines.empty? ## or use lines.size == 0
81
-
82
107
 
83
- parser = MatchParser.new( lines,
84
- start ) ## note: keep season start_at date for now (no need for more specific stage date need for now)
108
+ ############
109
+ ### "walk" tree to get structs (matches/teams/etc.)
110
+ conv = MatchTree.new( tree, start: start )
111
+
112
+ auto_conf_teams, matches, rounds, groups = conv.convert
85
113
 
86
- auto_conf_teams, matches, rounds, groups = parser.parse
87
114
 
88
115
  ## auto-add "upstream" errors from parser
89
- @errors += parser.errors if parser.errors?
90
-
116
+ ## @errors += parser.errors if parser.errors?
91
117
 
92
118
  if debug?
93
119
  puts ">>> #{auto_conf_teams.size} teams:"
@@ -100,40 +126,11 @@ class QuickMatchReader
100
126
  pp groups
101
127
  end
102
128
 
103
- ## note: pass along stage (if present): stage - optional from heading!!!!
104
- if stage
105
- matches.each do |match|
106
- match = match.update( stage: stage )
107
- end
108
- end
109
-
110
- @data[ league ] ||= {}
111
- @data[ league ][ season.key ] ||= []
112
129
 
113
- @data[ league ][ season.key ] += matches
130
+ @matches = matches
114
131
  ## note - skip teams, rounds, and groups for now
115
- end
116
-
117
- ## check - only one league and one season
118
- ## allowed in quick style
119
-
120
-
121
- leagues = @data.keys
122
- if leagues.size != 1
123
- puts "!! (QUICK) PARSE ERROR - expected one league only; got #{leagues.size}:"
124
- pp leagues
125
- exit 1
126
- end
127
-
128
- seasons = @data[ leagues[0] ].keys
129
- if seasons.size != 1
130
- puts "!! (QUICK) PARSE ERROR - expected one #{leagues[0]} season only; got #{seasons.size}:"
131
- pp seasons
132
- exit 1
133
- end
134
-
135
- @data[ leagues[0] ][ seasons[0] ]
136
- end # method parse
132
+ @matches
133
+ end # method parse
137
134
 
138
135
  end # class QuickMatchReader
139
136
  end # module SportDb
@@ -3,8 +3,8 @@ module SportDb
3
3
  module Module
4
4
  module Quick
5
5
  MAJOR = 0 ## todo: namespace inside version or something - why? why not??
6
- MINOR = 5
7
- PATCH = 3
6
+ MINOR = 7
7
+ PATCH = 0
8
8
  VERSION = [MAJOR,MINOR,PATCH].join('.')
9
9
 
10
10
  def self.version