footballdata-api 0.2.0 → 0.3.1

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.
@@ -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