footballdata-api 0.2.0 → 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.
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