football-sources 0.1.0 → 0.1.1

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,239 +0,0 @@
1
-
2
-
3
- module Footballdata
4
-
5
-
6
- def self.convert( league:, season: )
7
-
8
- ### note/fix: cl (champions league for now is a "special" case)
9
- if league.downcase == 'cl'
10
- convert_cl( league: league,
11
- season: season )
12
- return
13
- end
14
-
15
-
16
-
17
- season = Season( season ) ## cast (ensure) season class (NOT string, integer, etc.)
18
-
19
- data = Webcache.read_json( Metal.competition_matches_url( LEAGUES[league.downcase], season.start_year ))
20
- data_teams = Webcache.read_json( Metal.competition_teams_url( LEAGUES[league.downcase], season.start_year ))
21
-
22
-
23
- ## build a (reverse) team lookup by name
24
- puts "#{data_teams['teams'].size} teams"
25
-
26
- teams_by_name = data_teams['teams'].reduce( {} ) do |h,rec|
27
- h[ rec['name'] ] = rec
28
- h
29
- end
30
-
31
- pp teams_by_name.keys
32
-
33
-
34
-
35
- mods = MODS[ league.downcase ] || {}
36
-
37
-
38
- recs = []
39
-
40
- teams = Hash.new( 0 )
41
-
42
- stat = Stat.new
43
-
44
- matches = data[ 'matches']
45
- matches.each do |m|
46
- stat.update( m )
47
-
48
- team1 = m['homeTeam']['name']
49
- team2 = m['awayTeam']['name']
50
-
51
- score = m['score']
52
-
53
-
54
-
55
- if m['stage'] == 'REGULAR_SEASON'
56
- teams[ team1 ] += 1
57
- teams[ team2 ] += 1
58
-
59
- ### mods - rename club names
60
- unless mods.nil? || mods.empty?
61
- team1 = mods[ team1 ] if mods[ team1 ]
62
- team2 = mods[ team2 ] if mods[ team2 ]
63
- end
64
-
65
-
66
- ## e.g. "utcDate": "2020-05-09T00:00:00Z",
67
- date_str = m['utcDate']
68
- date = DateTime.strptime( date_str, '%Y-%m-%dT%H:%M:%SZ' )
69
-
70
-
71
- comments = ''
72
- ft = ''
73
- ht = ''
74
-
75
- case m['status']
76
- when 'SCHEDULED', 'IN_PLAY'
77
- ft = ''
78
- ht = ''
79
- when 'FINISHED'
80
- ## todo/fix: assert duration == "REGULAR"
81
- ft = "#{score['fullTime']['homeTeam']}-#{score['fullTime']['awayTeam']}"
82
- ht = "#{score['halfTime']['homeTeam']}-#{score['halfTime']['awayTeam']}"
83
- when 'AWARDED'
84
- ## todo/fix: assert duration == "REGULAR"
85
- ft = "#{score['fullTime']['homeTeam']}-#{score['fullTime']['awayTeam']}"
86
- ft << ' (*)'
87
- ht = ''
88
- comments = 'awarded'
89
- when 'CANCELLED'
90
- ft = '(*)'
91
- ht = ''
92
- comments = 'canceled' ## us eng ? -> canceled, british eng. cancelled ?
93
- when 'POSTPONED'
94
- ft = '(*)'
95
- ht = ''
96
- comments = 'postponed'
97
- else
98
- puts "!! ERROR: unsupported match status >#{m['status']}< - sorry:"
99
- pp m
100
- exit 1
101
- end
102
-
103
-
104
- ## todo/fix: assert matchday is a number e.g. 1,2,3, etc.!!!
105
- recs << [m['matchday'],
106
- date.to_date.strftime( '%Y-%m-%d' ),
107
- team1,
108
- ft,
109
- ht,
110
- team2,
111
- comments
112
- ]
113
-
114
-
115
- print '%2s' % m['matchday']
116
- print ' - '
117
- print '%-24s' % team1
118
- print ' '
119
- print ft
120
- print ' '
121
- print "(#{ht})" unless ht.empty?
122
- print ' '
123
- print '%-24s' % team2
124
- print ' '
125
- print comments
126
- print ' | '
127
- ## print date.to_date ## strip time
128
- print date.to_date.strftime( '%a %b %-d %Y' )
129
- print ' -- '
130
- print date
131
- print "\n"
132
- else
133
- puts "-- skipping #{m['stage']}"
134
- end
135
- end # each match
136
-
137
-
138
-
139
- ## note: get season from first match
140
- ## assert - all other matches include the same season
141
- ## e.g.
142
- # "season": {
143
- # "id": 154,
144
- # "startDate": "2018-08-03",
145
- # "endDate": "2019-05-05",
146
- # "currentMatchday": 46
147
- # }
148
-
149
- start_date = Date.strptime( matches[0]['season']['startDate'], '%Y-%m-%d' )
150
- end_date = Date.strptime( matches[0]['season']['endDate'], '%Y-%m-%d' )
151
-
152
- dates = "#{start_date.strftime('%b %-d')} - #{end_date.strftime('%b %-d')}"
153
-
154
- buf = ''
155
- buf << "#{season.key} (#{dates}) - "
156
- buf << "#{teams.keys.size} clubs, "
157
- buf << "#{stat[:regular_season][:matches]} matches, "
158
- buf << "#{stat[:regular_season][:goals]} goals"
159
- buf << "\n"
160
-
161
- puts buf
162
-
163
-
164
-
165
- ## note: warn if stage is greater one and not regular season!!
166
- File.open( './errors.txt' , 'a:utf-8' ) do |f|
167
- if stat[:all][:stage].keys != ['REGULAR_SEASON']
168
- f.write "!! WARN - league: #{league}, season: #{season.key} includes non-regular stage(s):\n"
169
- f.write " #{stat[:all][:stage].keys.inspect}\n"
170
- end
171
- end
172
-
173
-
174
- File.open( './logs.txt', 'a:utf-8' ) do |f|
175
- f.write "\n================================\n"
176
- f.write "==== #{league} =============\n"
177
- f.write buf
178
- f.write " match status: #{stat[:regular_season][:status].inspect}\n"
179
- f.write " match duration: #{stat[:regular_season][:duration].inspect}\n"
180
-
181
- f.write "#{teams.keys.size} teams:\n"
182
- teams.each do |name, count|
183
- rec = teams_by_name[ name ]
184
- f.write " #{count}x #{name}"
185
- if rec
186
- f.write " | #{rec['shortName']} " if name != rec['shortName']
187
- f.write " › #{rec['area']['name']}"
188
- f.write " - #{rec['address']}"
189
- else
190
- puts "!! ERROR - no team record found in teams.json for >#{name}<"
191
- exit 1
192
- end
193
- f.write "\n"
194
- end
195
- end
196
-
197
-
198
-
199
-
200
- # recs = recs.sort { |l,r| l[1] <=> r[1] }
201
- ## reformat date / beautify e.g. Sat Aug 7 1993
202
- recs.each { |rec| rec[1] = Date.strptime( rec[1], '%Y-%m-%d' ).strftime( '%a %b %-d %Y' ) }
203
-
204
- headers = [
205
- 'Matchday',
206
- 'Date',
207
- 'Team 1',
208
- 'FT',
209
- 'HT',
210
- 'Team 2',
211
- 'Comments'
212
- ]
213
-
214
- ## note: change season_key from 2019/20 to 2019-20 (for path/directory!!!!)
215
- Cache::CsvMatchWriter.write( "#{config.convert.out_dir}/#{season.to_path}/#{league.downcase}.csv",
216
- recs,
217
- headers: headers )
218
-
219
-
220
- teams.each do |name, count|
221
- rec = teams_by_name[ name ]
222
- print " #{count}x "
223
- print name
224
- if rec
225
- print " | #{rec['shortName']} " if name != rec['shortName']
226
- print " › #{rec['area']['name']}"
227
- print " - #{rec['address']}"
228
- else
229
- puts "!! ERROR - no team record found in teams.json for #{name}"
230
- exit 1
231
- end
232
- print "\n"
233
- end
234
-
235
- pp stat
236
- end # method convert
237
- end # module Footballdata
238
-
239
-
@@ -1,267 +0,0 @@
1
- ##########
2
- ## todo/fix:
3
- ## use a more generic name?
4
- ## might also works for euro or worldcup - check?
5
-
6
-
7
- module Footballdata
8
-
9
-
10
- STAGE_TO_STAGE = {
11
- 'PRELIMINARY_SEMI_FINALS' => 'Qualifying',
12
- 'PRELIMINARY_FINAL' => 'Qualifying',
13
- '1ST_QUALIFYING_ROUND' => 'Qualifying',
14
- '2ND_QUALIFYING_ROUND' => 'Qualifying',
15
- '3RD_QUALIFYING_ROUND' => 'Qualifying',
16
- 'PLAY_OFF_ROUND' => 'Qualifying',
17
- 'ROUND_OF_16' => 'Knockout',
18
- 'QUARTER_FINALS' => 'Knockout',
19
- 'SEMI_FINALS' => 'Knockout',
20
- 'FINAL' => 'Knockout',
21
- }
22
-
23
- STAGE_TO_ROUND = {
24
- 'PRELIMINARY_SEMI_FINALS' => 'Preliminary Semifinals',
25
- 'PRELIMINARY_FINAL' => 'Preliminary Final',
26
- '1ST_QUALIFYING_ROUND' => 'Qual. Round 1',
27
- '2ND_QUALIFYING_ROUND' => 'Qual. Round 2',
28
- '3RD_QUALIFYING_ROUND' => 'Qual. Round 3',
29
- 'PLAY_OFF_ROUND' => 'Playoff Round',
30
- 'ROUND_OF_16' => 'Round of 16',
31
- 'QUARTER_FINALS' => 'Quarterfinals',
32
- 'SEMI_FINALS' => 'Semifinals',
33
- 'FINAL' => 'Final',
34
- }
35
-
36
-
37
-
38
-
39
- def self.convert_cl( league:, season: )
40
- season = Season( season ) ## cast (ensure) season class (NOT string, integer, etc.)
41
-
42
- data = Webcache.read_json( Metal.competition_matches_url( LEAGUES[league.downcase], season.start_year ))
43
- data_teams = Webcache.read_json( Metal.competition_teams_url( LEAGUES[league.downcase], season.start_year ))
44
-
45
-
46
- ## build a (reverse) team lookup by name
47
- puts "#{data_teams['teams'].size} teams"
48
-
49
- teams_by_name = data_teams['teams'].reduce( {} ) do |h,rec|
50
- h[ rec['name'] ] = rec
51
- h
52
- end
53
-
54
- pp teams_by_name.keys
55
-
56
-
57
-
58
- mods = MODS[ league.downcase ] || {}
59
-
60
-
61
- recs = []
62
-
63
- teams = Hash.new( 0 )
64
- stat = Stat.new
65
-
66
- matches = data[ 'matches' ]
67
- matches.each do |m|
68
- stat.update( m )
69
-
70
- team1 = m['homeTeam']['name']
71
- team2 = m['awayTeam']['name']
72
-
73
-
74
- score = m['score']
75
-
76
-
77
-
78
- if m['stage'] == 'GROUP_STAGE'
79
- stage = 'Group'
80
- round = "Matchday #{m['matchday']}"
81
- if m['group'] =~ /Group ([A-Z])/
82
- group = $1
83
- else
84
- puts "!! ERROR - no group name found for group >#{m['group']}<"
85
- exit 1
86
- end
87
- else
88
- stage = STAGE_TO_STAGE[ m['stage'] ]
89
- if stage.nil?
90
- puts "!! ERROR - no stage mapping found for stage >#{m['stage']}<"
91
- exit 1
92
- end
93
- round = STAGE_TO_ROUND[ m['stage'] ]
94
- if round.nil?
95
- puts "!! ERROR - no round mapping found for stage >#{m['stage']}<"
96
- exit 1
97
- end
98
- group = ''
99
- end
100
-
101
-
102
- teams[ team1 ] += 1
103
- teams[ team2 ] += 1
104
-
105
- ### mods - rename club names
106
- unless mods.nil? || mods.empty?
107
- team1 = mods[ team1 ] if mods[ team1 ]
108
- team2 = mods[ team2 ] if mods[ team2 ]
109
- end
110
-
111
-
112
- ## e.g. "utcDate": "2020-05-09T00:00:00Z",
113
- date_str = m['utcDate']
114
- date = DateTime.strptime( date_str, '%Y-%m-%dT%H:%M:%SZ' )
115
-
116
-
117
- comments = ''
118
- ft = ''
119
- ht = ''
120
- et = ''
121
- pen = ''
122
-
123
- case m['status']
124
- when 'SCHEDULED', 'IN_PLAY'
125
- ft = ''
126
- ht = ''
127
- et = ''
128
- pen = ''
129
- when 'AWARDED'
130
- ## todo/fix: assert duration == "REGULAR"
131
- ft = "#{score['fullTime']['homeTeam']}-#{score['fullTime']['awayTeam']}"
132
- ft << ' (*)'
133
- ht = ''
134
- comments = 'awarded'
135
- when 'FINISHED'
136
- ## note: if extraTime present
137
- ## than fullTime is extraTime score!!
138
- ## AND fullTime - extraTime is fullTime score!!
139
- ## double check in other season too??
140
- ## - checked in cl 2018/19
141
-
142
- if score['extraTime']['homeTeam'] && score['extraTime']['awayTeam']
143
- et = "#{score['fullTime']['homeTeam']}-#{score['fullTime']['awayTeam']}"
144
- ft = "#{score['fullTime']['homeTeam']-score['extraTime']['homeTeam']}-#{score['fullTime']['awayTeam']-score['extraTime']['awayTeam']}"
145
- else
146
- ft = "#{score['fullTime']['homeTeam']}-#{score['fullTime']['awayTeam']}"
147
- end
148
-
149
- ht = "#{score['halfTime']['homeTeam']}-#{score['halfTime']['awayTeam']}"
150
-
151
- pen = if score['penalties']['homeTeam'] && score['penalties']['awayTeam']
152
- "#{score['penalties']['homeTeam']}-#{score['penalties']['awayTeam']}"
153
- else
154
- ''
155
- end
156
- else
157
- puts "!! ERROR: unsupported match status >#{m['status']}< - sorry:"
158
- pp m
159
- exit 1
160
- end
161
-
162
-
163
- recs << [stage,
164
- round,
165
- group,
166
- date.to_date.strftime( '%Y-%m-%d' ),
167
- team1,
168
- ft,
169
- ht,
170
- team2,
171
- et,
172
- pen,
173
- comments
174
- ]
175
-
176
-
177
- print '%2s' % m['matchday']
178
- print ' - '
179
- print '%-24s' % team1
180
- print ' '
181
- print ft
182
- print ' '
183
- print "(#{ht})" unless ht.empty?
184
- print ' '
185
- print '%-24s' % team2
186
- print ' '
187
- print comments
188
- print ' | '
189
- ## print date.to_date ## strip time
190
- print date.to_date.strftime( '%a %b %-d %Y' )
191
- print ' -- '
192
- print date
193
- print "\n"
194
-
195
- end # each match
196
-
197
-
198
- ## note: get season from first match
199
- ## assert - all other matches include the same season
200
- ## e.g.
201
- # "season": {
202
- # "id": 154,
203
- # "startDate": "2018-08-03",
204
- # "endDate": "2019-05-05",
205
- # "currentMatchday": 46
206
- # }
207
-
208
- start_date = Date.strptime( matches[0]['season']['startDate'], '%Y-%m-%d' )
209
- end_date = Date.strptime( matches[0]['season']['endDate'], '%Y-%m-%d' )
210
-
211
- dates = "#{start_date.strftime('%b %-d')} - #{end_date.strftime('%b %-d')}"
212
-
213
- buf = ''
214
- buf << "#{season.key} (#{dates}) - "
215
- buf << "#{teams.keys.size} clubs, "
216
- buf << "#{stat[:all][:matches]} matches, "
217
- buf << "#{stat[:all][:goals]} goals"
218
- buf << "\n"
219
-
220
- puts buf
221
-
222
-
223
-
224
- # recs = recs.sort { |l,r| l[1] <=> r[1] }
225
- ## reformat date / beautify e.g. Sat Aug 7 1993
226
- recs.each { |rec| rec[3] = Date.strptime( rec[3], '%Y-%m-%d' ).strftime( '%a %b %-d %Y' ) }
227
-
228
- headers = [
229
- 'Stage',
230
- 'Round',
231
- 'Group',
232
- 'Date',
233
- 'Team 1',
234
- 'FT',
235
- 'HT',
236
- 'Team 2',
237
- 'ET',
238
- 'P',
239
- 'Comments'
240
- ]
241
-
242
- ## note: change season_key from 2019/20 to 2019-20 (for path/directory!!!!)
243
- Cache::CsvMatchWriter.write( "#{config.convert.out_dir}/#{season.to_path}/#{league.downcase}.csv",
244
- recs,
245
- headers: headers )
246
-
247
-
248
- teams.each do |name, count|
249
- rec = teams_by_name[ name ]
250
- print " #{count}x "
251
- print name
252
- if rec
253
- print " | #{rec['shortName']} " if name != rec['shortName']
254
- print " › #{rec['area']['name']}"
255
- print " - #{rec['address']}"
256
- else
257
- print "!! ERROR - no team record found in teams.json for #{name}"
258
- exit 1
259
- end
260
- print "\n"
261
- end
262
-
263
- pp stat
264
- end # method convert_cl
265
- end # module Footballdata
266
-
267
-
@@ -1,20 +0,0 @@
1
- module Footballdata
2
-
3
- #########
4
- ## Mods
5
- # e.g.
6
- # Cardiff City FC | Cardiff › Wales - Cardiff City Stadium, Leckwith Road Cardiff CF11 8AZ
7
- # AS Monaco FC | Monaco › Monaco - Avenue des Castellans Monaco 98000
8
-
9
-
10
- MODS = {
11
- 'br.1' => {
12
- 'América FC' => 'América MG', # in year 2018
13
- },
14
- 'pt.1' => {
15
- 'Vitória SC' => 'Vitória Guimarães', ## avoid easy confusion with Vitória SC <=> Vitória FC
16
- 'Vitória FC' => 'Vitória Setúbal',
17
- },
18
- }
19
-
20
- end # module Footballdata
@@ -1,59 +0,0 @@
1
- module Footballdata
2
-
3
- class Stat ## rename to match stat or something why? why not?
4
- def initialize
5
- @data = {}
6
- end
7
-
8
- def [](key) @data[ key ]; end
9
-
10
- def update( match )
11
- ## keep track of some statistics
12
- stat = @data[:all] ||= { stage: Hash.new( 0 ),
13
- duration: Hash.new( 0 ),
14
- status: Hash.new( 0 ),
15
- group: Hash.new( 0 ),
16
- matchday: Hash.new( 0 ),
17
-
18
- matches: 0,
19
- goals: 0,
20
- }
21
-
22
- stat[:stage][ match['stage'] ] += 1
23
- stat[:group][ match['group'] ] += 1
24
- stat[:status][ match['status'] ] += 1
25
- stat[:matchday][ match['matchday'] ] += 1
26
-
27
- score = match['score']
28
-
29
- stat[:duration][ score['duration'] ] += 1 ## track - assert always REGULAR
30
-
31
- stat[:matches] += 1
32
- stat[:goals] += score['fullTime']['homeTeam'].to_i if score['fullTime']['homeTeam']
33
- stat[:goals] += score['fullTime']['awayTeam'].to_i if score['fullTime']['awayTeam']
34
-
35
-
36
- stage_key = match['stage'].downcase.to_sym # e.g. :regular_season
37
- stat = @data[ stage_key ] ||= { duration: Hash.new( 0 ),
38
- status: Hash.new( 0 ),
39
- group: Hash.new( 0 ),
40
- matchday: Hash.new( 0 ),
41
-
42
- matches: 0,
43
- goals: 0,
44
- }
45
- stat[:group][ match['group'] ] += 1
46
- stat[:status][ match['status'] ] += 1
47
- stat[:matchday][ match['matchday'] ] += 1
48
-
49
- stat[:duration][ score['duration'] ] += 1 ## track - assert always REGULAR
50
-
51
- stat[:matches] += 1
52
- stat[:goals] += score['fullTime']['homeTeam'].to_i if score['fullTime']['homeTeam']
53
- stat[:goals] += score['fullTime']['awayTeam'].to_i if score['fullTime']['awayTeam']
54
- end
55
- end # class Stat
56
- end # module Footballdata
57
-
58
-
59
-
@@ -1,10 +0,0 @@
1
-
2
-
3
- ###########################
4
- ## our own code
5
- require_relative 'apis/config'
6
- require_relative 'apis/mods'
7
- require_relative 'apis/stat'
8
- require_relative 'apis/convert'
9
- require_relative 'apis/convert_cl'
10
-
@@ -1,96 +0,0 @@
1
-
2
- module Fbref
3
-
4
- def self.build( rows, league:, season: )
5
- season = Season( season ) ## cast (ensure) season class (NOT string, integer, etc.)
6
-
7
- raise ArgumentError, "league key as string expected" unless league.is_a?(String) ## note: do NOT pass in league struct! pass in key (string)
8
-
9
- print " #{rows.size} rows - build #{league} #{season}"
10
- print "\n"
11
-
12
-
13
- recs = []
14
- rows.each do |row|
15
-
16
- stage = row[:stage] || ''
17
-
18
- ## todo/check: assert that only matchweek or round can be present NOT both!!
19
- round = if row[:matchweek] && row[:matchweek].size > 0
20
- row[:matchweek]
21
- elsif row[:round] && row[:round].size > 0
22
- row[:round]
23
- else
24
- ''
25
- end
26
-
27
- date_str = row[:date]
28
- time_str = row[:time]
29
- team1_str = row[:team1]
30
- team2_str = row[:team2]
31
- score_str = row[:score]
32
-
33
- ## convert date from string e.g. 2019-25-10
34
- date = Date.strptime( date_str, '%Y-%m-%d' )
35
-
36
- comments = row[:comments]
37
- ht, ft, et, pen, comments = parse_score( score_str, comments )
38
-
39
-
40
- venue_str = row[:venue]
41
- attendance_str = row[:attendance]
42
-
43
-
44
- recs << [stage,
45
- round,
46
- date.strftime( '%Y-%m-%d' ),
47
- time_str,
48
- team1_str,
49
- ft,
50
- ht,
51
- team2_str,
52
- et, # extra: incl. extra time
53
- pen, # extra: incl. penalties
54
- venue_str,
55
- attendance_str,
56
- comments]
57
- end
58
-
59
- recs
60
- end
61
-
62
-
63
- def self.parse_score( score_str, comments )
64
-
65
- ## split score
66
- ft = ''
67
- ht = ''
68
- et = ''
69
- pen = ''
70
-
71
- if score_str.size > 0
72
- ## note: replace unicode "fancy" dash with ascii-dash
73
- # check other columns too - possible in teams?
74
- score_str = score_str.gsub( /[–]/, '-' ).strip
75
-
76
- if score_str =~ /^\(([0-9]+)\)
77
- [ ]+ ([0-9]+) - ([0-9+]) [ ]+
78
- \(([0-9]+)\)$/x
79
- ft = '?'
80
- et = "#{$2}-#{$3}"
81
- pen = "#{$1}-#{$4}"
82
- else ## assume "regular" score e.g. 0-0
83
- ## check if notes include extra time otherwise assume regular time
84
- if comments =~ /extra time/i
85
- ft = '?'
86
- et = score_str
87
- else
88
- ft = score_str
89
- end
90
- end
91
- end
92
-
93
- [ht, ft, et, pen, comments]
94
- end
95
-
96
- end # module Fbref
@@ -1,16 +0,0 @@
1
- module Fbref
2
-
3
- ### add some more config options / settings
4
- class Configuration
5
- #########
6
- ## nested configuration classes - use - why? why not?
7
- class Convert
8
- def out_dir() @out_dir || './o'; end
9
- def out_dir=(value) @out_dir = value; end
10
- end
11
-
12
- def convert() @convert ||= Convert.new; end
13
- end # class Configuration
14
-
15
-
16
- end # module Fbref