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.
@@ -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
 
@@ -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 = LEAGUES[league.downcase]
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
+
@@ -1,8 +1,8 @@
1
1
 
2
2
  module FootballdataApi
3
3
  MAJOR = 0 ## todo: namespace inside version or something - why? why not??
4
- MINOR = 2
5
- PATCH = 0
4
+ MINOR = 3
5
+ PATCH = 1
6
6
  VERSION = [MAJOR,MINOR,PATCH].join('.')
7
7
 
8
8
  def self.version
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.2.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-07-07 00:00:00.000000000 Z
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: 2.2.2
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