footballdata-api 0.2.0 → 0.3.1
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 +3 -1
- data/Rakefile +3 -3
- data/bin/fbdat +56 -34
- data/config/leagues.csv +54 -0
- data/config/timezones.csv +27 -0
- data/lib/footballdata/convert.rb +263 -100
- data/lib/footballdata/download.rb +11 -11
- data/lib/footballdata/leagues.rb +20 -55
- data/lib/footballdata/mods.rb +2 -0
- data/lib/footballdata/prettyprint.rb +29 -47
- data/lib/footballdata/teams.rb +13 -14
- data/lib/footballdata/timezones.rb +97 -0
- data/lib/footballdata/version.rb +2 -2
- data/lib/footballdata.rb +3 -8
- metadata +6 -4
- data/lib/footballdata/generator.rb +0 -33
@@ -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/teams.rb
CHANGED
@@ -8,9 +8,9 @@ module Footballdata
|
|
8
8
|
def self.export_teams( league:, season: )
|
9
9
|
|
10
10
|
season = Season( season ) ## cast (ensure) season class (NOT string, integer, etc.)
|
11
|
-
league_code =
|
11
|
+
league_code = find_league!( league )
|
12
12
|
|
13
|
-
teams_url = Metal.competition_teams_url( league_code,
|
13
|
+
teams_url = Metal.competition_teams_url( league_code,
|
14
14
|
season.start_year )
|
15
15
|
data_teams = Webcache.read_json( teams_url )
|
16
16
|
|
@@ -22,12 +22,12 @@ def self.export_teams( league:, season: )
|
|
22
22
|
clubs = {} ## by country
|
23
23
|
|
24
24
|
data_teams['teams'].each do |rec|
|
25
|
-
|
26
|
-
buf = String.new ### use for string buffer (or String.new('') - why? why not?
|
25
|
+
|
26
|
+
buf = String.new ### use for string buffer (or String.new('') - why? why not?
|
27
27
|
buf << "#{rec['name']}"
|
28
28
|
buf << ", #{rec['founded']}" if rec['founded']
|
29
|
-
if rec['venue']
|
30
|
-
buf << ", @ #{rec['venue']}"
|
29
|
+
if rec['venue']
|
30
|
+
buf << ", @ #{rec['venue']}"
|
31
31
|
# buf << " # #{rec['area']['name']}"
|
32
32
|
end
|
33
33
|
buf << "\n"
|
@@ -39,7 +39,7 @@ def self.export_teams( league:, season: )
|
|
39
39
|
|
40
40
|
alt_names << " | #{rec['tla']}" if rec['tla'] &&
|
41
41
|
rec['tla'] != rec['name']
|
42
|
-
|
42
|
+
|
43
43
|
if alt_names.size > 0
|
44
44
|
buf << " "
|
45
45
|
buf << alt_names
|
@@ -48,7 +48,7 @@ def self.export_teams( league:, season: )
|
|
48
48
|
|
49
49
|
## clean null in address (or keep nulls) - why? why not?
|
50
50
|
## e.g. null Rionegro null
|
51
|
-
## Calle 104 No. 13a - 32 Bogotá null
|
51
|
+
## Calle 104 No. 13a - 32 Bogotá null
|
52
52
|
|
53
53
|
buf << " address: #{rec['address'].gsub( /\bnull\b/, '')}"
|
54
54
|
buf << "\n"
|
@@ -57,21 +57,21 @@ def self.export_teams( league:, season: )
|
|
57
57
|
buf << " colors: #{rec['clubColors']}"
|
58
58
|
buf << "\n"
|
59
59
|
|
60
|
-
country = rec['area']['name']
|
60
|
+
country = rec['area']['name']
|
61
61
|
ary = clubs[ country] ||= []
|
62
62
|
ary << buf
|
63
63
|
end
|
64
64
|
# puts buf
|
65
|
-
|
65
|
+
|
66
66
|
## pp clubs
|
67
67
|
|
68
|
-
|
68
|
+
|
69
69
|
buf = String.new
|
70
70
|
|
71
71
|
if clubs.size > 1
|
72
72
|
clubs.each do |country, ary|
|
73
73
|
buf << "# #{country} - #{ary.size} clubs\n"
|
74
|
-
end
|
74
|
+
end
|
75
75
|
buf << "\n"
|
76
76
|
end
|
77
77
|
|
@@ -82,9 +82,8 @@ def self.export_teams( league:, season: )
|
|
82
82
|
end
|
83
83
|
|
84
84
|
path = "#{config.convert.out_dir}/#{season.to_path}/#{league.downcase}.clubs.txt"
|
85
|
-
write_text( path, buf )
|
85
|
+
write_text( path, buf )
|
86
86
|
end
|
87
87
|
|
88
88
|
|
89
89
|
end # module Footballdata
|
90
|
-
|
@@ -0,0 +1,97 @@
|
|
1
|
+
|
2
|
+
class UTC
|
3
|
+
def self.now() Time.now.utc; end
|
4
|
+
def self.today() now.to_date; end
|
5
|
+
|
6
|
+
## -- todo - make sure / assert it's always utc - how???
|
7
|
+
## utc = ## tz_utc.strptime( m['utcDate'], '%Y-%m-%dT%H:%M:%SZ' )
|
8
|
+
## note: DateTime.strptime is supposed to be unaware of timezones!!!
|
9
|
+
## use to parse utc
|
10
|
+
## quick hack -
|
11
|
+
## use to_time.getutc instead of utc ???
|
12
|
+
def self.strptime( str, format )
|
13
|
+
DateTime.strptime( str, format ).to_time.utc
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.find_zone( name )
|
17
|
+
zone = TZInfo::Timezone.get( name )
|
18
|
+
## wrap tzinfo timezone in our own - for adding more (auto)checks etc.
|
19
|
+
zone ? Timezone.new( zone ) : nil
|
20
|
+
end
|
21
|
+
|
22
|
+
class Timezone ## nested inside UTC
|
23
|
+
## todo/fix
|
24
|
+
## cache timezone - why? why not?
|
25
|
+
def initialize( zone )
|
26
|
+
@zone = zone
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_local( time )
|
30
|
+
## assert time is Time (not Date or DateTIme)
|
31
|
+
## and assert utc!!!
|
32
|
+
assert( time.is_a?( Time ), "time #{time} is NOT of class Time; got #{time.class.name}" )
|
33
|
+
assert( time.utc?, "time #{time} is NOT utc; utc? returns #{time.utc?}" )
|
34
|
+
local = @zone.to_local( time )
|
35
|
+
local
|
36
|
+
end
|
37
|
+
|
38
|
+
def assert( cond, msg )
|
39
|
+
if cond
|
40
|
+
# do nothing
|
41
|
+
else
|
42
|
+
puts "!!! assert failed - #{msg}"
|
43
|
+
exit 1
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end # class Timezone
|
47
|
+
end # class UTC
|
48
|
+
|
49
|
+
|
50
|
+
|
51
|
+
module Footballdata
|
52
|
+
def self.find_zone!( league:, season: )
|
53
|
+
@zones ||= begin
|
54
|
+
recs = read_csv( "#{FootballdataApi.root}/config/timezones.csv" )
|
55
|
+
zones = {}
|
56
|
+
recs.each do |rec|
|
57
|
+
zone = UTC.find_zone( rec['zone'] )
|
58
|
+
if zone.nil?
|
59
|
+
## raise ArgumentError - invalid zone
|
60
|
+
puts "!! ERROR - cannot find timezone in timezone db:"
|
61
|
+
pp rec
|
62
|
+
exit 1
|
63
|
+
end
|
64
|
+
zones[ rec['key']] = zone
|
65
|
+
end
|
66
|
+
zones
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
## lookup first try by league+season
|
71
|
+
league_code = league.downcase
|
72
|
+
season = Season( season )
|
73
|
+
|
74
|
+
## e.g. world+2022, etc.
|
75
|
+
key = "#{league_code}+#{season}"
|
76
|
+
zone = @zones[key]
|
77
|
+
|
78
|
+
## try league e.g. eng.1 etc.
|
79
|
+
zone = @zones[league_code] if zone.nil?
|
80
|
+
|
81
|
+
## try first code only (country code )
|
82
|
+
if zone.nil?
|
83
|
+
code, _ = league_code.split( '.', 2 )
|
84
|
+
zone = @zones[code]
|
85
|
+
end
|
86
|
+
|
87
|
+
if zone.nil? ## still not found; report error
|
88
|
+
puts "!! ERROR: no timezone found for #{league} #{season}"
|
89
|
+
exit 1
|
90
|
+
end
|
91
|
+
|
92
|
+
zone
|
93
|
+
end
|
94
|
+
end # module Footballdata
|
95
|
+
|
96
|
+
|
97
|
+
|
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
|
|
@@ -36,6 +36,8 @@ end # module Footballdata
|
|
36
36
|
# our own code
|
37
37
|
require_relative 'footballdata/version'
|
38
38
|
require_relative 'footballdata/leagues'
|
39
|
+
require_relative 'footballdata/timezones'
|
40
|
+
|
39
41
|
require_relative 'footballdata/download'
|
40
42
|
require_relative 'footballdata/prettyprint'
|
41
43
|
|
@@ -44,13 +46,6 @@ require_relative 'footballdata/convert'
|
|
44
46
|
require_relative 'footballdata/teams'
|
45
47
|
|
46
48
|
|
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
49
|
|
55
50
|
|
56
51
|
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.1
|
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-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: tzinfo
|
@@ -116,15 +116,17 @@ files:
|
|
116
116
|
- README.md
|
117
117
|
- Rakefile
|
118
118
|
- bin/fbdat
|
119
|
+
- config/leagues.csv
|
120
|
+
- config/timezones.csv
|
119
121
|
- lib/footballdata.rb
|
120
122
|
- lib/footballdata/convert.rb
|
121
123
|
- lib/footballdata/download.rb
|
122
|
-
- lib/footballdata/generator.rb
|
123
124
|
- lib/footballdata/leagues.rb
|
124
125
|
- lib/footballdata/mods.rb
|
125
126
|
- lib/footballdata/prettyprint.rb
|
126
127
|
- lib/footballdata/stat.rb
|
127
128
|
- lib/footballdata/teams.rb
|
129
|
+
- lib/footballdata/timezones.rb
|
128
130
|
- lib/footballdata/version.rb
|
129
131
|
homepage: https://github.com/sportdb/sport.db
|
130
132
|
licenses:
|
@@ -140,7 +142,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
140
142
|
requirements:
|
141
143
|
- - ">="
|
142
144
|
- !ruby/object:Gem::Version
|
143
|
-
version:
|
145
|
+
version: 3.1.0
|
144
146
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
145
147
|
requirements:
|
146
148
|
- - ">="
|
@@ -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
|