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 +4 -4
- data/CHANGELOG.md +1 -1
- data/Manifest.txt +0 -1
- data/Rakefile +3 -3
- data/bin/fbdat +61 -32
- data/lib/footballdata/convert.rb +263 -75
- data/lib/footballdata/mods.rb +2 -0
- data/lib/footballdata/prettyprint.rb +29 -47
- data/lib/footballdata/version.rb +1 -1
- data/lib/footballdata.rb +1 -8
- metadata +3 -4
- data/lib/footballdata/generator.rb +0 -33
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a0fe438df785fdc33e14a57346b2ec720029a97e34ae4a5eaeddf4e7fa4d8cda
|
4
|
+
data.tar.gz: e4eaf71567d20a666525139d94dbe4323fb2c8773e245b67454281e6740a9cb4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4081619b75253f9ad57b77ef6232e262b37af91a6e154e3832274520d7363a150a38ddf5a6596d12c5e28646419e530c569a902a74b807f78c987de02104e92f
|
7
|
+
data.tar.gz: 2639c457fea6f4c4a7cae24196f8133e72ccfa80c433963726b17b498f7e2fcb34dfbe7ae64afb78c76ebc5ff120905cc2f71b3ceec05deb5a535140b6761faa
|
data/CHANGELOG.md
CHANGED
data/Manifest.txt
CHANGED
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: '>=
|
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] || '
|
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
|
157
|
-
|
174
|
+
if LEAGUES.has_key?( league_code )
|
175
|
+
metal_league_code = LEAGUES[ league_code ]
|
158
176
|
else
|
159
|
-
|
160
|
-
|
161
|
-
|
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
|
-
(
|
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
|
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
|
-
|
178
|
-
|
179
|
-
|
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(
|
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
|
|
data/lib/footballdata/convert.rb
CHANGED
@@ -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
|
-
|
85
|
-
|
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
|
-
|
93
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 << [
|
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}
|
231
|
-
|
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 "
|
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[
|
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
|
-
|
323
|
-
|
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
|
|
data/lib/footballdata/mods.rb
CHANGED
@@ -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
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
-
|
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
|
|
data/lib/footballdata/version.rb
CHANGED
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.
|
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-
|
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:
|
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
|