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 +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
|