footballdata-api 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 16f099d030a007993b7289956e920fd89baff6abc506feeca3dad792453efe76
4
- data.tar.gz: 8f3d2df27a3dc12cc15fa2a8ee0dcd42a51ed793261a2ab2795020147596e7f6
3
+ metadata.gz: a0fe438df785fdc33e14a57346b2ec720029a97e34ae4a5eaeddf4e7fa4d8cda
4
+ data.tar.gz: e4eaf71567d20a666525139d94dbe4323fb2c8773e245b67454281e6740a9cb4
5
5
  SHA512:
6
- metadata.gz: cca1d63da2e1823a3c9cc2636953da2f691101c32b63f5004c5349338d9dd75099ac993786e038cf3e2ea8a41f3ac1ce7510cdf148f1f13a98d360203613cf99
7
- data.tar.gz: 6030f680eb2e5dadd269ee78bd778c1b465914702e20fe98e55e0967346feb67a6b74d8a965dea73ce62b9c8f457ba1de811d02e26a603b90ff4d3a160b8ee99
6
+ metadata.gz: 4081619b75253f9ad57b77ef6232e262b37af91a6e154e3832274520d7363a150a38ddf5a6596d12c5e28646419e530c569a902a74b807f78c987de02104e92f
7
+ data.tar.gz: 2639c457fea6f4c4a7cae24196f8133e72ccfa80c433963726b17b498f7e2fcb34dfbe7ae64afb78c76ebc5ff120905cc2f71b3ceec05deb5a535140b6761faa
data/CHANGELOG.md CHANGED
@@ -1,4 +1,4 @@
1
- ### 0.2.0
1
+ ### 0.3.0
2
2
 
3
3
  ### 0.0.1 / 2024-07-03
4
4
 
data/Manifest.txt CHANGED
@@ -6,7 +6,6 @@ bin/fbdat
6
6
  lib/footballdata.rb
7
7
  lib/footballdata/convert.rb
8
8
  lib/footballdata/download.rb
9
- lib/footballdata/generator.rb
10
9
  lib/footballdata/leagues.rb
11
10
  lib/footballdata/mods.rb
12
11
  lib/footballdata/prettyprint.rb
data/Rakefile CHANGED
@@ -19,15 +19,15 @@ Hoe.spec 'footballdata-api' do
19
19
 
20
20
  self.extra_deps = [
21
21
  ['tzinfo'],
22
- ['season-formats'],
23
- ['webget'],
22
+ ['season-formats'],
23
+ ['webget'],
24
24
  ['cocos'], ## later pull in with sportsdb-writers
25
25
  ]
26
26
 
27
27
  self.licenses = ['Public Domain']
28
28
 
29
29
  self.spec_extras = {
30
- required_ruby_version: '>= 2.2.2'
30
+ required_ruby_version: '>= 3.1.0'
31
31
  }
32
32
 
33
33
  end
data/bin/fbdat CHANGED
@@ -12,17 +12,20 @@ load_env ## use dotenv (.env)
12
12
  Webcache.root = if File.exist?( '/sports/cache' )
13
13
  puts " setting web cache to >/sports/cache<"
14
14
  '/sports/cache'
15
- else
15
+ else
16
16
  './cache'
17
17
  end
18
18
 
19
- ## note - free tier (tier one) plan - 10 requests/minute
19
+ ## note - free tier (tier one) plan - 10 requests/minute
20
20
  ## (one request every 6 seconds 6*10=60 secs)
21
21
  ## 10 API calls per minute max.
22
22
  ## note - default sleep (delay in secs) is 3 sec(s)
23
23
  Webget.config.sleep = 10
24
24
 
25
25
 
26
+ Footballdata.config.convert.out_dir = '/sports/cache.api.fbdat' if File.exist?( '/sports/cache.api.fbdat' )
27
+
28
+
26
29
 
27
30
 
28
31
  require 'optparse'
@@ -32,17 +35,28 @@ require 'optparse'
32
35
  module Footballdata
33
36
  def self.main( args=ARGV )
34
37
 
35
- opts = {}
38
+ opts = {
39
+ cached: false,
40
+ convert: true,
41
+ }
42
+
36
43
  parser = OptionParser.new do |parser|
37
- parser.banner = "Usage: #{$PROGRAM_NAME} [options]"
44
+ parser.banner = "Usage: #{$PROGRAM_NAME} [options] [args]"
38
45
 
39
46
  parser.on( "--cache", "--cached", "--offline",
40
- "use cached data in #{Webcache.root}" ) do |cached|
47
+ "use cached data in #{Webcache.root} - default is (#{opts[:cached]})" ) do |cached|
41
48
  opts[:cached] = cached
42
49
  end
50
+
51
+ parser.on( "--no-convert",
52
+ "turn off conversion to .csv in #{Footballdata.config.convert.out_dir} - default is (#{!opts[:convert]})" ) do |convert|
53
+ opts[:convert] = !convert
54
+ end
43
55
  end
44
56
  parser.parse!( args )
45
57
 
58
+
59
+
46
60
  puts "OPTS:"
47
61
  p opts
48
62
  puts "ARGV:"
@@ -51,23 +65,23 @@ p args
51
65
 
52
66
  ## try special args
53
67
 
54
- if ['plan', 'plans',
55
- 'comp', 'comps'].include?(args[0])
68
+ if ['plan', 'plans',
69
+ 'comp', 'comps'].include?(args[0])
56
70
 
57
71
  if opts[:cached]
58
72
  ## do nothing
59
- else
73
+ else
60
74
  Metal.competitions( auth: true ) ## get free tier (TIER_ONE) with auth (token)
61
75
  end
62
76
 
63
77
  url = Metal.competitions_url
64
78
  pp url
65
79
  #=> "http://api.football-data.org/v4/competitions"
66
-
80
+
67
81
  data = Webcache.read_json( url )
68
82
  pp data
69
-
70
- comps = data['competitions']
83
+
84
+ comps = data['competitions']
71
85
  comps.each do |rec|
72
86
  print "==> "
73
87
  print "#{rec['area']['name']} (#{rec['area']['code']}) - "
@@ -75,12 +89,12 @@ if ['plan', 'plans',
75
89
  print "#{rec['plan']} #{rec['type']}, "
76
90
  print "#{rec['numberOfAvailableSeasons']} season(s)"
77
91
  print "\n"
78
-
79
- print " #{rec['currentSeason']['startDate']} - #{rec['currentSeason']['endDate']} "
92
+
93
+ print " #{rec['currentSeason']['startDate']} - #{rec['currentSeason']['endDate']} "
80
94
  print "@ #{rec['currentSeason']['currentMatchday']}"
81
95
  print "\n"
82
96
  end
83
-
97
+
84
98
  puts " #{comps.size} competition(s)"
85
99
  exit 0
86
100
  end
@@ -100,9 +114,9 @@ end
100
114
  ## todo - add more date offsets - t+2,t+3,t+4, etc.
101
115
 
102
116
  date = if ['y', 'yesterday', 't-1', '-1'].include?( args[0] )
103
- Date.today-1
117
+ Date.today-1
104
118
  elsif ['t', 'tomorrow', 't+1', '1', '+1'].include?( args[0] )
105
- Date.today+1
119
+ Date.today+1
106
120
  elsif ['m', 'match', 'matches', 'today'].include?( args[0] || 'today' ) ## make default - why? why not?
107
121
  Date.today
108
122
  else
@@ -110,7 +124,7 @@ date = if ['y', 'yesterday', 't-1', '-1'].include?( args[0] )
110
124
  end
111
125
 
112
126
  if date
113
- if opts[:cached]
127
+ if opts[:cached]
114
128
  ## do nothing
115
129
  else
116
130
  Metal.todays_matches( date )
@@ -124,7 +138,7 @@ if date
124
138
  ## only print competition header if different from last match
125
139
  comp = fmt_competition( rec )
126
140
  puts comp if comp != last_comp
127
-
141
+
128
142
  puts fmt_match( rec )
129
143
 
130
144
  last_comp = comp
@@ -145,40 +159,48 @@ end
145
159
  ## club cups intl
146
160
  ## CL 2023 - (uefa/european) champions league
147
161
  ## CLI 2204 - (south american) copa libertadores
148
- ## club leagues
162
+ ## club leagues
149
163
  ## PL 2024 - england premiere league
150
164
 
165
+ ##
166
+ ## note - only use "generic" uniform league codes for now!!
151
167
 
152
- league_code = args[0] || 'PL'
168
+ league_code = (args[0] || 'eng.1').downcase
169
+ metal_league_code = nil ## todo - find a better name
170
+ ## use internal_league_code or such - why? why not?
153
171
 
154
172
  ### convenience helpers - lets you use eng.1, euro, etc.
155
173
  ## check if mapping for league_code
156
- if LEAGUES.has_key?( league_code.downcase )
157
- league_code = LEAGUES[ league_code.downcase ]
174
+ if LEAGUES.has_key?( league_code )
175
+ metal_league_code = LEAGUES[ league_code ]
158
176
  else
159
- ## assume "native" code
160
- ## always upcase e.g. pl => PL etc.
161
- league_code = league_code.upcase
177
+ puts "!! ERROR - no code/mapping found for league >#{league_code}<"
178
+ puts " mappings include:"
179
+ pp LEAGUES
180
+ exit 1
162
181
  end
163
182
 
164
183
 
165
- season = Season( args[1] ||
166
- (league_code == 'EC' ? '2024' : '2024/25'))
184
+ season = Season( args[1] ||
185
+ (['euro',
186
+ 'copa.l',
187
+ 'br.1'].include?(league_code) ? '2024' : '2024/25'))
167
188
 
168
189
  season_start_year = season.start_year ## use year - why? why not?
169
190
 
170
- pp league_code, season_start_year
191
+ pp [metal_league_code, season_start_year]
171
192
 
172
193
  if opts[:cached]
173
194
  ## do nothing
174
195
  else
175
196
  ## download dataset(s)
176
197
  ## try download
177
- Metal.matches( league_code,
178
- season_start_year )
179
- end
198
+ ## note: include teams (for convert) for now too!!
199
+ Metal.teams( metal_league_code, season_start_year )
200
+ Metal.matches( metal_league_code, season_start_year )
201
+ end
180
202
 
181
- url = Metal.competition_matches_url( league_code,
203
+ url = Metal.competition_matches_url( metal_league_code,
182
204
  season_start_year )
183
205
  pp url
184
206
  #=> "http://api.football-data.org/v4/competitions/EC/matches?season=2024"
@@ -188,6 +210,13 @@ data = Webcache.read_json( url )
188
210
 
189
211
  pp_matches( data )
190
212
 
213
+
214
+ if opts[:convert]
215
+ puts "==> converting to .csv"
216
+ convert( league: league_code, season: season )
217
+ end
218
+
219
+
191
220
  end # def self.main
192
221
  end # module Footballdata
193
222
 
@@ -5,30 +5,113 @@ module Footballdata
5
5
 
6
6
 
7
7
  TIMEZONES = {
8
- 'eng.1' => 'Europe/London',
8
+ 'eng.1' => 'Europe/London',
9
9
  'eng.2' => 'Europe/London',
10
-
10
+
11
11
  'es.1' => 'Europe/Madrid',
12
-
12
+
13
13
  'de.1' => 'Europe/Berlin',
14
- 'fr.1' => 'Europe/Paris',
14
+ 'fr.1' => 'Europe/Paris',
15
15
  'it.1' => 'Europe/Rome',
16
16
  'nl.1' => 'Europe/Amsterdam',
17
-
18
- 'pt.1' => 'Europe/Lisbon',
17
+
18
+ 'pt.1' => 'Europe/Lisbon',
19
+
20
+ ## for champs default for not to cet (central european time) - why? why not?
21
+ 'uefa.cl' => 'Europe/Paris',
22
+ 'euro' => 'Europe/Paris',
19
23
 
20
24
  ## todo/fix - pt.1
21
25
  ## one team in madeira!!! check for different timezone??
22
- ## CD Nacional da Madeira
26
+ ## CD Nacional da Madeira
23
27
 
24
28
  'br.1' => 'America/Sao_Paulo',
25
29
  ## todo/fix - brazil has 4 timezones
26
30
  ## really only two in use for clubs
27
31
  ## west and east (amazonas et al)
28
32
  ## for now use west for all - why? why not?
33
+ 'copa.l' => 'America/Sao_Paulo',
29
34
  }
30
35
 
31
36
 
37
+
38
+
39
+ def self.convert_score( score )
40
+ ## duration: REGULAR · PENALTY_SHOOTOUT · EXTRA_TIME
41
+ ft, ht, et, pen = ["","","",""]
42
+
43
+ if score['duration'] == 'REGULAR'
44
+ ft = "#{score['fullTime']['home']}-#{score['fullTime']['away']}"
45
+ ht = "#{score['halfTime']['home']}-#{score['halfTime']['away']}"
46
+ elsif score['duration'] == 'EXTRA_TIME'
47
+ et = "#{score['regularTime']['home']+score['extraTime']['home']}"
48
+ et << "-"
49
+ et << "#{score['regularTime']['away']+score['extraTime']['away']}"
50
+
51
+ ft = "#{score['regularTime']['home']}-#{score['regularTime']['away']}"
52
+ ht = "#{score['halfTime']['home']}-#{score['halfTime']['away']}"
53
+ elsif score['duration'] == 'PENALTY_SHOOTOUT'
54
+ if score['extraTime']
55
+ ## quick & dirty hack - calc et via regulartime+extratime
56
+ pen = "#{score['penalties']['home']}-#{score['penalties']['away']}"
57
+ et = "#{score['regularTime']['home']+score['extraTime']['home']}"
58
+ et << "-"
59
+ et << "#{score['regularTime']['away']+score['extraTime']['away']}"
60
+
61
+ ft = "#{score['regularTime']['home']}-#{score['regularTime']['away']}"
62
+ ht = "#{score['halfTime']['home']}-#{score['halfTime']['away']}"
63
+ else ### south american-style (no extra time)
64
+ ## quick & dirty hacke - calc ft via fullTime-penalties
65
+ pen = "#{score['penalties']['home']}-#{score['penalties']['away']}"
66
+ ft = "#{score['fullTime']['home']-score['penalties']['home']}"
67
+ ft << "-"
68
+ ft << "#{score['fullTime']['away']-score['penalties']['away']}"
69
+ ht = "#{score['halfTime']['home']}-#{score['halfTime']['away']}"
70
+ end
71
+ else
72
+ puts "!! unknown score duration:"
73
+ pp score
74
+ exit 1
75
+ end
76
+
77
+ [ft,ht,et,pen]
78
+ end
79
+
80
+
81
+ #######
82
+ ## map round-like to higher-level stages
83
+ STAGES = {
84
+ 'REGULAR_SEASON' => ['Regular'],
85
+
86
+ 'PRELIMINARY_ROUND' => ['Qualifying', 'Preliminary Round' ],
87
+ 'PRELIMINARY_SEMI_FINALS' => ['Qualifying', 'Preliminary Semifinals' ],
88
+ 'PRELIMINARY_FINAL' => ['Qualifying', 'Preliminary Final' ],
89
+ '1ST_QUALIFYING_ROUND' => ['Qualifying', 'Qual. Round 1' ],
90
+ '2ND_QUALIFYING_ROUND' => ['Qualifying', 'Qual. Round 2' ],
91
+ '3RD_QUALIFYING_ROUND' => ['Qualifying', 'Qual. Round 3' ],
92
+ 'QUALIFICATION_ROUND_1' => ['Qualifying', 'Qual. Round 1' ],
93
+ 'QUALIFICATION_ROUND_2' => ['Qualifying', 'Qual. Round 2' ],
94
+ 'QUALIFICATION_ROUND_3' => ['Qualifying', 'Qual. Round 3' ],
95
+ 'ROUND_1' => ['Qualifying', 'Round 1'], ## use Qual. Round 1 - why? why not?
96
+ 'ROUND_2' => ['Qualifying', 'Round 2'],
97
+ 'ROUND_3' => ['Qualifying', 'Round 3'],
98
+ 'PLAY_OFF_ROUND' => ['Qualifying', 'Playoff Round'],
99
+ 'PLAYOFF_ROUND_1' => ['Qualifying', 'Playoff Round 1'],
100
+
101
+ 'LEAGUE_STAGE' => ['League'],
102
+ 'GROUP_STAGE' => ['Group'],
103
+ 'PLAYOFFS' => ['Playoffs'],
104
+
105
+ 'ROUND_OF_16' => ['Finals', 'Round of 16'],
106
+ 'LAST_16' => ['Finals', 'Round of 16'], ## use Last 16 - why? why not?
107
+ 'QUARTER_FINALS' => ['Finals', 'Quarterfinals'],
108
+ 'SEMI_FINALS' => ['Finals', 'Semifinals'],
109
+ 'FINAL' => ['Finals', 'Final'],
110
+ }
111
+
112
+
113
+
114
+
32
115
  def self.convert( league:, season: )
33
116
 
34
117
  ### note/fix: cl (champions league for now is a "special" case)
@@ -48,14 +131,14 @@ def self.convert( league:, season: )
48
131
  data = Webcache.read_json( matches_url )
49
132
  data_teams = Webcache.read_json( teams_url )
50
133
 
51
-
134
+
52
135
  ## check for time zone
53
136
  tz_name = TIMEZONES[ league.downcase ]
54
137
  if tz_name.nil?
55
138
  puts "!! ERROR - sorry no timezone configured for league #{league}"
56
139
  exit 1
57
140
  end
58
-
141
+
59
142
  tz = TZInfo::Timezone.get( tz_name )
60
143
  pp tz
61
144
 
@@ -67,9 +150,9 @@ def self.convert( league:, season: )
67
150
  h
68
151
  end
69
152
 
70
- pp teams_by_name.keys
153
+ ## pp teams_by_name.keys
154
+
71
155
 
72
-
73
156
 
74
157
  mods = MODS[ league.downcase ] || {}
75
158
 
@@ -81,22 +164,62 @@ teams = Hash.new( 0 )
81
164
 
82
165
  # stat = Stat.new
83
166
 
84
- # track stati counts
85
- stati = Hash.new(0)
167
+ ## track stage, match status et
168
+ stats = { 'status' => Hash.new(0),
169
+ 'stage' => Hash.new(0),
170
+ }
171
+
172
+
86
173
 
87
174
 
88
175
  matches = data[ 'matches']
89
176
  matches.each do |m|
90
177
  # stat.update( m )
91
178
 
92
- team1 = m['homeTeam']['name']
93
- team2 = m['awayTeam']['name']
179
+ ## use ? or N.N. or ? for nil - why? why not?
180
+ team1 = m['homeTeam']['name'] || 'N.N.'
181
+ team2 = m['awayTeam']['name'] || 'N.N.'
94
182
 
95
183
  score = m['score']
96
184
 
97
185
 
186
+ stage_key = m['stage']
187
+
188
+ stats['stage'][ stage_key ] += 1 ## track stage counts
189
+
190
+ ## map stage to stage + round
191
+ stage, stage_round = STAGES[ stage_key ]
192
+
193
+ if stage.nil?
194
+ puts "!! ERROR - no stage mapping found for stage >#{stage_key}<"
195
+ exit 1
196
+ end
197
+
198
+ matchday_num = m['matchday']
199
+ matchday_num = nil if matchday_num == 0 ## change 0 to nil (empty) too
200
+
201
+ if stage_round.nil? ## e.g. Regular, League, Group, Playoffs
202
+ ## keep/assume matchday number is matchday .e.g
203
+ ## matchday 1, 2 etc.
204
+ matchday = matchday_num.to_s
205
+ else
206
+ ## note - if matchday defined; assume leg e.g. 1|2
207
+ ## skip if different than one or two for now
208
+ matchday = String.new
209
+ matchday << stage_round
210
+ matchday << " | Leg #{matchday_num}" if matchday_num &&
211
+ (matchday_num == 1 || matchday_num == 2)
212
+ end
213
+
214
+
215
+
216
+ group = m['group'] || ''
217
+ ## GROUP_A
218
+ ## shorten group to A|B|C etc.
219
+ group = group.sub( /^GROUP_/, '' )
220
+
221
+
98
222
 
99
- if m['stage'] == 'REGULAR_SEASON'
100
223
  teams[ team1 ] += 1
101
224
  teams[ team2 ] += 1
102
225
 
@@ -107,31 +230,36 @@ matches.each do |m|
107
230
  end
108
231
 
109
232
 
233
+ ## auto-fix copa.l 2024
234
+ ## !! ERROR: unsupported match status >IN_PLAY< - sorry:
235
+ if m['status'] == 'IN_PLAY' &&
236
+ team1 == 'Club Aurora' && team2 == 'FBC Melgar'
237
+ m['status'] = 'FINISHED'
238
+ end
239
+
110
240
 
111
241
  comments = ''
112
242
  ft = ''
113
243
  ht = ''
244
+ et = ''
245
+ pen = ''
114
246
 
115
- stati[m['status']] += 1 ## track stati counts for logs
247
+ stats['status'][m['status']] += 1 ## track status counts
116
248
 
117
249
  case m['status']
118
250
  when 'SCHEDULED', 'TIMED' ## , 'IN_PLAY'
119
251
  ft = ''
120
252
  ht = ''
121
253
  when 'FINISHED'
122
- ## todo/fix: assert duration == "REGULAR"
123
- assert( score['duration'] == 'REGULAR', 'score.duration REGULAR expected' )
124
- ft = "#{score['fullTime']['home']}-#{score['fullTime']['away']}"
125
- ht = "#{score['halfTime']['home']}-#{score['halfTime']['away']}"
254
+ ft, ht, et, pen = convert_score( score )
126
255
  when 'AWARDED'
127
- ## todo/fix: assert duration == "REGULAR"
128
- assert( score['duration'] == 'REGULAR', 'score.duration REGULAR expected' )
256
+ assert( score['duration'] == 'REGULAR', 'score.duration REGULAR expected' )
129
257
  ft = "#{score['fullTime']['home']}-#{score['fullTime']['away']}"
130
258
  ft << ' (*)'
131
259
  ht = ''
132
260
  comments = 'awarded'
133
261
  when 'CANCELLED'
134
- ## note cancelled might have scores!!
262
+ ## note cancelled might have scores!! -- add/fix later!!!
135
263
  ## ht only or ft+ht!!! (see fr 2021/22)
136
264
  ft = '(*)'
137
265
  ht = ''
@@ -157,55 +285,36 @@ matches.each do |m|
157
285
  ## utc = ## tz_utc.strptime( m['utcDate'], '%Y-%m-%dT%H:%M:%SZ' )
158
286
  ## note: DateTime.strptime is supposed to be unaware of timezones!!!
159
287
  ## use to parse utc
160
- utc = DateTime.strptime( m['utcDate'], '%Y-%m-%dT%H:%M:%SZ' ).to_time.utc
288
+ utc = DateTime.strptime( m['utcDate'], '%Y-%m-%dT%H:%M:%SZ' ).to_time.utc
161
289
  assert( utc.strftime( '%Y-%m-%dT%H:%M:%SZ' ) == m['utcDate'], 'utc time mismatch' )
162
-
290
+
163
291
  local = tz.to_local( utc )
164
-
292
+
165
293
 
166
294
  ## do NOT add time if status is SCHEDULED
167
295
  ## or POSTPONED for now
168
296
  ## otherwise assume time always present - why? why not?
169
-
297
+
298
+
170
299
 
171
300
  ## todo/fix: assert matchday is a number e.g. 1,2,3, etc.!!!
172
- recs << [m['matchday'].to_s, ## note: convert integer to string!!!
301
+ recs << [stage,
302
+ group,
303
+ matchday,
173
304
  local.strftime( '%Y-%m-%d' ),
174
305
  ['SCHEDULED','POSTPONED'].include?( m['status'] ) ? '' : local.strftime( '%H:%M' ),
175
- local.strftime( '%Z / UTC%z' ),
306
+ local.strftime( '%Z / UTC%z' ),
176
307
  team1,
177
308
  ft,
178
309
  ht,
179
310
  team2,
311
+ et,
312
+ pen,
180
313
  comments,
181
314
  ## add more columns e.g. utc date, status
182
315
  m['status'], # e.g. FINISHED, TIMED, etc.
183
316
  m['utcDate'],
184
317
  ]
185
-
186
-
187
- print '%2s' % m['matchday']
188
- print ' - '
189
- print '%-26s' % team1
190
- print ' '
191
- print ft
192
- print ' '
193
- print "(#{ht})" unless ht.empty?
194
- print ' '
195
- print '%-26s' % team2
196
- print ' '
197
- print comments
198
- print ' | '
199
- ## print date.to_date ## strip time
200
- print utc.strftime( '%a %b %-d %Y' )
201
- print ' -- '
202
- print utc
203
- print "\n"
204
- else
205
- puts "!!! unexpected stage:"
206
- puts "-- skipping #{m['stage']}"
207
- # exit 1
208
- end
209
318
  end # each match
210
319
 
211
320
 
@@ -227,8 +336,8 @@ dates = "#{start_date.strftime('%b %-d')} - #{end_date.strftime('%b %-d')}"
227
336
 
228
337
  buf = ''
229
338
  buf << "#{season.key} (#{dates}) - "
230
- buf << "#{teams.keys.size} clubs, "
231
- # buf << "#{stat[:regular_season][:matches]} matches, "
339
+ buf << "#{teams.keys.size} teams, "
340
+ buf << "#{recs.size} matches"
232
341
  # buf << "#{stat[:regular_season][:goals]} goals"
233
342
  buf << "\n"
234
343
 
@@ -248,7 +357,7 @@ puts buf
248
357
 
249
358
  File.open( './logs.txt', 'a:utf-8' ) do |f|
250
359
  f.write "==== #{league} #{season.key} =============\n"
251
- f.write " match stati: #{stati.inspect}\n"
360
+ f.write " #{stats.inspect}\n"
252
361
  end
253
362
 
254
363
  =begin
@@ -283,26 +392,30 @@ puts buf
283
392
 
284
393
 
285
394
  ## reformat date / beautify e.g. Sat Aug 7 1993
286
- recs = recs.map do |rec|
287
- rec[1] = Date.strptime( rec[1], '%Y-%m-%d' ).strftime( '%a %b %-d %Y' )
395
+ recs = recs.map do |rec|
396
+ rec[3] = Date.strptime( rec[3], '%Y-%m-%d' ).strftime( '%a %b %-d %Y' )
288
397
  rec
289
398
  end
290
399
 
400
+ ## pp recs
401
+
402
+ ## check if all status colums
403
+ ### are FINISHED
404
+ ### if yes, set all to empty (for vacuum)
405
+
406
+ if stats['status'].keys.size == 1 && stats['status'].keys[0] == 'FINISHED'
407
+ recs = recs.map { |rec| rec[-2] = ''; rec }
408
+ end
409
+
410
+ if stats['stage'].keys.size == 1 && stats['stage'].keys[0] == 'REGULAR_SEASON'
411
+ recs = recs.map { |rec| rec[0] = ''; rec }
412
+ end
413
+
414
+
415
+ recs, headers = vacuum( recs )
416
+
417
+
291
418
 
292
- headers = [
293
- 'Matchday',
294
- 'Date',
295
- 'Time',
296
- 'Timezone', ## move back column - why? why not?
297
- 'Team 1',
298
- 'FT',
299
- 'HT',
300
- 'Team 2',
301
- 'Comments',
302
- ##
303
- 'Status', # e.g.
304
- 'UTC', # date utc
305
- ]
306
419
 
307
420
  ## note: change season_key from 2019/20 to 2019-20 (for path/directory!!!!)
308
421
  write_csv( "#{config.convert.out_dir}/#{season.to_path}/#{league.downcase}.csv",
@@ -319,14 +432,89 @@ teams.each do |name, count|
319
432
  print " › #{rec['area']['name']}"
320
433
  print " - #{rec['address']}"
321
434
  else
322
- puts "!! ERROR - no team record found in teams.json for #{name}"
323
- exit 1
435
+ if name == 'N.N.'
436
+ ## ignore missing record
437
+ else
438
+ puts "!! ERROR - no team record found in teams.json for #{name}"
439
+ exit 1
440
+ end
324
441
  end
325
442
  print "\n"
326
443
  end
327
444
 
328
445
  ## pp stat
329
446
  end # method convert
447
+
448
+
449
+
450
+ MAX_HEADERS = [
451
+ 'Stage', # 0
452
+ 'Group', # 1
453
+ 'Matchday', # 2
454
+ 'Date', # 3
455
+ 'Time', # 4
456
+ 'Timezone', # 5 ## move back column - why? why not?
457
+ 'Team 1', # 6
458
+ 'FT', # 7
459
+ 'HT', # 8
460
+ 'Team 2', # 9
461
+ 'ET', # 10 # extra: incl. extra time
462
+ 'P', # 11 # extra: incl. penalties
463
+ 'Comments', # 12
464
+ 'Status', # 13 / -2 # e.g.
465
+ 'UTC', # 14 / -1 # date utc
466
+ ]
467
+
468
+ MIN_HEADERS = [ ## always keep even if all empty
469
+ 'Date',
470
+ 'Team 1',
471
+ 'FT',
472
+ 'Team 2'
473
+ ]
474
+
475
+
476
+
477
+ def self.vacuum( rows, headers: MAX_HEADERS, fixed_headers: MIN_HEADERS )
478
+ ## check for unused columns and strip/remove
479
+ counter = Array.new( MAX_HEADERS.size, 0 )
480
+ rows.each do |row|
481
+ row.each_with_index do |col, idx|
482
+ counter[idx] += 1 unless col.nil? || col.empty?
483
+ end
484
+ end
485
+
486
+ ## pp counter
487
+
488
+ ## check empty columns
489
+ headers = []
490
+ indices = []
491
+ empty_headers = []
492
+ empty_indices = []
493
+
494
+ counter.each_with_index do |num, idx|
495
+ header = MAX_HEADERS[ idx ]
496
+ if num > 0 || (num == 0 && fixed_headers.include?( header ))
497
+ headers << header
498
+ indices << idx
499
+ else
500
+ empty_headers << header
501
+ empty_indices << idx
502
+ end
503
+ end
504
+
505
+ if empty_indices.size > 0
506
+ rows = rows.map do |row|
507
+ row_vacuumed = []
508
+ row.each_with_index do |col, idx|
509
+ ## todo/fix: use values or such??
510
+ row_vacuumed << col unless empty_indices.include?( idx )
511
+ end
512
+ row_vacuumed
513
+ end
514
+ end
515
+
516
+ [rows, headers]
517
+ end
330
518
  end # module Footballdata
331
519
 
332
520
 
@@ -6,6 +6,8 @@ module Footballdata
6
6
  # Cardiff City FC | Cardiff › Wales - Cardiff City Stadium, Leckwith Road Cardiff CF11 8AZ
7
7
  # AS Monaco FC | Monaco › Monaco - Avenue des Castellans Monaco 98000
8
8
 
9
+
10
+
9
11
  MODS = {
10
12
  'br.1' => {
11
13
  'América FC' => 'América Mineiro', # in year 2018 ??
@@ -17,7 +17,7 @@ def self.fmt_competition( rec )
17
17
  buf << "#{rec['competition']['name']} (#{rec['competition']['code']}) -- "
18
18
  buf << "#{rec['area']['name']} (#{rec['area']['code']}) "
19
19
  buf << "#{rec['competition']['type']} "
20
- buf << "#{rec['season']['startDate']} - #{rec['season']['endDate']} "
20
+ buf << "#{rec['season']['startDate']} - #{rec['season']['endDate']} "
21
21
  buf << "@ #{rec['season']['currentMatchday']}"
22
22
  buf << "\n"
23
23
 
@@ -26,12 +26,12 @@ end
26
26
 
27
27
  def self.fmt_match( rec )
28
28
  buf = String.new
29
-
29
+
30
30
  ## -- todo - make sure / assert it's always utc - how???
31
31
  ## utc = ## tz_utc.strptime( m['utcDate'], '%Y-%m-%dT%H:%M:%SZ' )
32
32
  ## note: DateTime.strptime is supposed to be unaware of timezones!!!
33
33
  ## use to parse utc
34
- utc = DateTime.strptime( rec['utcDate'], '%Y-%m-%dT%H:%M:%SZ' ).to_time.utc
34
+ utc = DateTime.strptime( rec['utcDate'], '%Y-%m-%dT%H:%M:%SZ' ).to_time.utc
35
35
  assert( utc.strftime( '%Y-%m-%dT%H:%M:%SZ' ) == rec['utcDate'], 'utc time mismatch' )
36
36
 
37
37
  status = rec['status']
@@ -50,7 +50,7 @@ def self.fmt_match( rec )
50
50
  team1 = rec['homeTeam']['name'] ?
51
51
  "#{rec['homeTeam']['name']} (#{rec['homeTeam']['tla']})" : '?'
52
52
  team2 = rec['awayTeam']['name'] ?
53
- "#{rec['awayTeam']['name']} (#{rec['awayTeam']['tla']})" : '?'
53
+ "#{rec['awayTeam']['name']} (#{rec['awayTeam']['tla']})" : '?'
54
54
  buf << '%22s' % team1
55
55
  buf << " - "
56
56
  buf << '%-22s' % team2
@@ -58,7 +58,7 @@ def self.fmt_match( rec )
58
58
 
59
59
  stage = rec['stage']
60
60
  group = rec['group']
61
-
61
+
62
62
  buf << "#{rec['matchday']} - #{stage} "
63
63
  buf << "/ #{group} " if group
64
64
  buf << "\n"
@@ -66,49 +66,31 @@ def self.fmt_match( rec )
66
66
  buf << " "
67
67
  buf << '%-20s' % rec['score']['duration']
68
68
  buf << ' '*24
69
-
69
+
70
70
  duration = rec['score']['duration']
71
71
  assert( %w[REGULAR
72
72
  EXTRA_TIME
73
73
  PENALTY_SHOOTOUT
74
74
  ].include?( duration ), "unknown duration - #{duration}" )
75
75
 
76
- score = String.new
77
-
78
- if duration == 'PENALTY_SHOOTOUT'
79
- if rec['score']['extraTime']
80
- ## quick & dirty hack - calc et via regulartime+extratime
81
- score << "#{rec['score']['penalties']['home']}-#{rec['score']['penalties']['away']} pen. "
82
- score << "#{rec['score']['regularTime']['home']+rec['score']['extraTime']['home']}"
83
- score << "-"
84
- score << "#{rec['score']['regularTime']['away']+rec['score']['extraTime']['away']}"
85
- score << " a.e.t. "
86
- score << "(#{rec['score']['regularTime']['home']}-#{rec['score']['regularTime']['away']},"
87
- score << "#{rec['score']['halfTime']['home']}-#{rec['score']['halfTime']['away']})"
88
- else ### south american-style (no extra time)
89
- ## quick & dirty hacke - calc ft via fullTime-penalties
90
- score << "#{rec['score']['penalties']['home']}-#{rec['score']['penalties']['away']} pen. "
91
- score << "(#{rec['score']['fullTime']['home']-rec['score']['penalties']['home']}"
92
- score << "-"
93
- score << "#{rec['score']['fullTime']['away']-rec['score']['penalties']['away']},"
94
- score << "#{rec['score']['halfTime']['home']}-#{rec['score']['halfTime']['away']})"
95
- end
96
- elsif duration == 'EXTRA_TIME'
97
- score << "#{rec['score']['regularTime']['home']+rec['score']['extraTime']['home']}"
98
- score << "-"
99
- score << "#{rec['score']['regularTime']['away']+rec['score']['extraTime']['away']}"
100
- score << " a.e.t. "
101
- score << "(#{rec['score']['regularTime']['home']}-#{rec['score']['regularTime']['away']},"
102
- score << "#{rec['score']['halfTime']['home']}-#{rec['score']['halfTime']['away']})"
103
- elsif duration == 'REGULAR'
104
- if rec['score']['fullTime']['home'] && rec['score']['fullTime']['away']
105
- score << "#{rec['score']['fullTime']['home']}-#{rec['score']['fullTime']['away']} "
106
- score << "(#{rec['score']['halfTime']['home']}-#{rec['score']['halfTime']['away']})"
107
- end
76
+
77
+ ft, ht, et, pen = convert_score( rec['score'] )
78
+ score = String.new
79
+ if !pen.empty?
80
+ if et.empty? ### south american-style (no extra time)
81
+ score << "#{pen} pen. "
82
+ score << "(#{ft}, #{ht})"
83
+ else
84
+ score << "#{pen} pen. "
85
+ score << "#{et} a.e.t. "
86
+ score << "(#{ft}, #{ht})"
87
+ end
88
+ elsif !et.empty?
89
+ score << "#{et} a.e.t. "
90
+ score << "(#{ft}, #{ht})"
108
91
  else
109
- raise ArgumentError, "unexpected/unknown score duration #{rec['score']['duration']}"
92
+ score << "#{ft} (#{ht})"
110
93
  end
111
-
112
94
 
113
95
  buf << score
114
96
  buf << "\n"
@@ -118,14 +100,14 @@ end
118
100
 
119
101
  def self.pp_matches( data )
120
102
 
121
- ## track match status and score duration
103
+ ## track match status and score duration
122
104
  stats = { 'status' => Hash.new(0),
123
105
  'duration' => Hash.new(0),
124
106
  'stage' => Hash.new(0),
125
107
  'group' => Hash.new(0),
126
108
  }
127
109
 
128
- first = Date.strptime( data['resultSet']['first'], '%Y-%m-%d' )
110
+ first = Date.strptime( data['resultSet']['first'], '%Y-%m-%d' )
129
111
  last = Date.strptime( data['resultSet']['last'], '%Y-%m-%d' )
130
112
 
131
113
  diff = (last - first).to_i # note - returns rational number (e.g. 30/1)
@@ -142,16 +124,16 @@ def self.pp_matches( data )
142
124
 
143
125
  ## track stats
144
126
  status = rec['status']
145
- stats['status'][status] += 1
127
+ stats['status'][status] += 1
146
128
 
147
129
  stage = rec['stage']
148
- stats['stage'][stage] += 1
130
+ stats['stage'][stage] += 1
149
131
 
150
132
  group = rec['group']
151
- stats['group'][group] += 1 if group
133
+ stats['group'][group] += 1 if group
152
134
 
153
135
  duration = rec['score']['duration']
154
- stats['duration'][duration] += 1
136
+ stats['duration'][duration] += 1
155
137
  end
156
138
 
157
139
  print " #{data['resultSet']['played']}/#{data['resultSet']['count']} matches"
@@ -182,7 +164,7 @@ def self.fmt_count( h, sort: false )
182
164
  end
183
165
  end
184
166
  pairs = pairs.map { |name,count| "#{name} (#{count})" }
185
- pairs.join( ' · ' )
167
+ pairs.join( ' · ' )
186
168
  end
187
169
 
188
170
 
@@ -1,7 +1,7 @@
1
1
 
2
2
  module FootballdataApi
3
3
  MAJOR = 0 ## todo: namespace inside version or something - why? why not??
4
- MINOR = 2
4
+ MINOR = 3
5
5
  PATCH = 0
6
6
  VERSION = [MAJOR,MINOR,PATCH].join('.')
7
7
 
data/lib/footballdata.rb CHANGED
@@ -17,7 +17,7 @@ module Footballdata
17
17
  def out_dir() @out_dir || './o'; end
18
18
  def out_dir=(value) @out_dir = value; end
19
19
  end
20
-
20
+
21
21
  def convert() @convert ||= Convert.new; end
22
22
  end # class Configuration
23
23
 
@@ -44,13 +44,6 @@ require_relative 'footballdata/convert'
44
44
  require_relative 'footballdata/teams'
45
45
 
46
46
 
47
- require_relative 'footballdata/generator'
48
-
49
-
50
-
51
- ### for processing tool
52
- ## (auto-)add sportdb/writer (pulls in sportdb/catalogs and gitti)
53
- ## require 'sportdb/writers'
54
47
 
55
48
 
56
49
  puts FootballdataApi.banner ## say hello
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: footballdata-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gerald Bauer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-07-07 00:00:00.000000000 Z
11
+ date: 2024-09-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: tzinfo
@@ -119,7 +119,6 @@ files:
119
119
  - lib/footballdata.rb
120
120
  - lib/footballdata/convert.rb
121
121
  - lib/footballdata/download.rb
122
- - lib/footballdata/generator.rb
123
122
  - lib/footballdata/leagues.rb
124
123
  - lib/footballdata/mods.rb
125
124
  - lib/footballdata/prettyprint.rb
@@ -140,7 +139,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
140
139
  requirements:
141
140
  - - ">="
142
141
  - !ruby/object:Gem::Version
143
- version: 2.2.2
142
+ version: 3.1.0
144
143
  required_rubygems_version: !ruby/object:Gem::Requirement
145
144
  requirements:
146
145
  - - ">="
@@ -1,33 +0,0 @@
1
-
2
- ##
3
- ### check - change Generator to Writer
4
- ## and write( league:, season: ) - why? why not?
5
-
6
- module Footballdata
7
- class Generator
8
-
9
-
10
-
11
- ###########
12
- ## always download for now - why? why not?
13
- ## support cache - why? why not?
14
- def generate( league:, season: )
15
-
16
- ## for testing use cached version always - why? why not?
17
- ## step 1 - download
18
- Footballdata.schedule( league: league, season: season )
19
-
20
- ## step 2 - convert (to .csv)
21
-
22
- ## todo/fix - convert in-memory and return matches
23
- Footballdata.convert( league: league, season: season )
24
-
25
- source_dir = Footballdata.config.convert.out_dir
26
-
27
- Writer.write( league: league,
28
- season: season,
29
- source: source_dir )
30
- end # def generate
31
-
32
- end # class Generator
33
- end # module Footballdata