leagues 0.1.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.
@@ -0,0 +1,9 @@
1
+ key, zone
2
+
3
+ jp, Asia/Tokyo
4
+ kr, Asia/Seoul
5
+ cn, Asia/Shanghai
6
+
7
+ kz, Asia/Almaty # see en.wikipedia.org/wiki/Time_in_Kazakhstan
8
+
9
+
@@ -0,0 +1,98 @@
1
+ key, zone
2
+
3
+ eng, Europe/London
4
+ sco, Europe/London ## check if entry or Edingburgh or such exits
5
+ wal, Europe/London
6
+ nir, Europe/London
7
+ ie, Europe/Dublin
8
+
9
+
10
+ es, Europe/Madrid
11
+ pt, Europe/Lisbon
12
+ gi, Europe/Gibraltar
13
+ ad, Europe/Andorra
14
+
15
+ fr, Europe/Paris
16
+ mc, Europe/Monaco
17
+
18
+ it, Europe/Rome
19
+ sm, Europe/San_Marino
20
+ va, Europe/Vatican
21
+
22
+
23
+
24
+ gr, Europe/Athens
25
+ tr, Europe/Istanbul
26
+ cy, Asia/Nicosia
27
+ mt, Europe/Malta
28
+
29
+
30
+ ro, Europe/Bucharest
31
+ bg, Europe/Sofia
32
+
33
+ ua, Europe/Kyiv
34
+ by, Europe/Minsk
35
+ ru, Europe/Moscow
36
+
37
+
38
+ de, Europe/Berlin
39
+ at, Europe/Vienna
40
+ ch, Europe/Zurich
41
+ li, Europe/Vaduz
42
+
43
+ hu, Europe/Budapest
44
+ cz, Europe/Prague
45
+ sk, Europe/Bratislava # link to Prague !!!
46
+ si, Europe/Ljubljana
47
+ pl, Europe/Warsaw
48
+
49
+
50
+ hr, Europe/Zagreb # link to Belgrade
51
+ ba, Europe/Sarajevo # link to Belgrade
52
+ rs, Europe/Belgrade # see en.wikipedia.org/wiki/Time_in_Serbia
53
+ al, Europe/Tirane
54
+ me, Europe/Podgorica
55
+ mk, Europe/Skopje
56
+ md, Europe/Chisinau
57
+
58
+ kos, Europe/Belgrade # en.wikipedia.org/wiki/Time_in_Kosovo
59
+ xk, Europe/Belgrade # note - add both codes for now eg. xk & kos
60
+
61
+
62
+
63
+ be, Europe/Brussels
64
+ lu, Europe/Luxembourg # link to Brussels !!!
65
+ nl, Europe/Amsterdam # link to Brussels !!!
66
+
67
+
68
+ dk, Europe/Copenhagen ## link to Berlin
69
+ se, Europe/Stockholm ## link to Berlin
70
+ no, Europe/Oslo # link to Berlin
71
+ fi, Europe/Helsinki
72
+
73
+ lv, Europe/Riga
74
+ ee, Europe/Tallinn
75
+ lt, Europe/Vilnius
76
+
77
+ is, Iceland
78
+ fo, Atlantic/Faroe
79
+
80
+
81
+
82
+ am, Asia/Yerevan # en.wikipedia.org/wiki/Armenia_Time
83
+ ge, Asia/Tbilisi # en.wikipedia.org/wiki/Georgia_Time
84
+ az, Asia/Baku
85
+
86
+
87
+
88
+ uefa, Europe/Paris
89
+ uefa.cl, Europe/Paris
90
+ uefa.champs, Europe/Paris ## for champs default for now to cet (central european time) - why? why not?
91
+ uefa.el, Europe/Paris
92
+ uefa.europa, Europe/Paris
93
+ uefa.conf, Europe/Paris
94
+ uefa.con, Europe/Paris
95
+ uefa.nl, Europe/Paris
96
+ uefa.nations, Europe/Paris
97
+ euro, Europe/Paris
98
+
@@ -0,0 +1,5 @@
1
+ key, zone
2
+
3
+ il, Asia/Jerusalem
4
+
5
+
@@ -0,0 +1,5 @@
1
+ key, zone
2
+
3
+
4
+ au, Australia/Sydney
5
+ nz, Pacific/Auckland
@@ -0,0 +1,45 @@
1
+ key, zone
2
+
3
+
4
+ ## todo/check - allow/use country ref
5
+ ## e.g. world+1998, fr
6
+ ## world+2006, de
7
+ ## and lookup timezone fr instead - why? why not?
8
+
9
+
10
+ world+2022, Asia/Qatar # Qatar
11
+ world+2018, Europe/Moscow # Russia
12
+ world+2014, America/Sao_Paulo # Brazil
13
+ world+2010, Africa/Johannesburg # South Africa
14
+ world+2006, Europe/Berlin # Germany
15
+ world+2002, Asia/Tokyo # Japan & South Korea
16
+ world+1998, Europe/Paris # France
17
+
18
+
19
+ friendlies, Europe/London ## use (generic) utc - why? why not?
20
+
21
+
22
+
23
+ ## add club world cups
24
+
25
+ world.clubs+2000, America/Sao_Paulo # Brazil
26
+ world.clubs+2005, Asia/Tokyo # Japan
27
+ world.clubs+2006, Asia/Tokyo # Japan
28
+ world.clubs+2007, Asia/Tokyo # Japan
29
+ world.clubs+2008, Asia/Tokyo # Japan
30
+ world.clubs+2009, Asia/Dubai # UAE
31
+ world.clubs+2010, Asia/Dubai # UAE
32
+ world.clubs+2011, Asia/Tokyo # Japan
33
+ world.clubs+2012, Asia/Tokyo # Japan
34
+ world.clubs+2013, Africa/Casablanca # Morocco
35
+ world.clubs+2014, Africa/Casablanca # Morocco
36
+ world.clubs+2015, Asia/Tokyo # Japan
37
+ world.clubs+2016, Asia/Tokyo # Japan
38
+ world.clubs+2017, Asia/Dubai # UAE
39
+ world.clubs+2018, Asia/Dubai # UAE
40
+ world.clubs+2019, Asia/Qatar # Qatar
41
+ world.clubs+2020, Asia/Qatar # Qatar
42
+ world.clubs+2021, Asia/Dubai # UAE
43
+ world.clubs+2022, Africa/Casablanca # Morocco
44
+ world.clubs+2023, Asia/Riyadh # Saudi Arabia
45
+ world.clubs+2025, America/New_York # United States
@@ -0,0 +1,169 @@
1
+ #####
2
+ #
3
+ # quick & dirty league code lookup (and mapping)
4
+
5
+
6
+ module SportDb
7
+ class LeagueCodes
8
+
9
+ ####
10
+ ## (public) api
11
+ def self.valid?( code )
12
+ ## check if code is valid
13
+ builtin.valid?( code )
14
+ end
15
+
16
+ def self.find_by( code:, season: )
17
+ ## return league code record/item or nil
18
+ builtin.find_by( code: code, season: season )
19
+ end
20
+
21
+
22
+ #####
23
+ ## (static) helpers
24
+ def self.builtin
25
+ ## get builtin league code index (build on demand)
26
+ @leagues ||= begin
27
+ leagues = SportDb::LeagueCodes.new
28
+ ['leagues',
29
+ 'leagues_more',
30
+ ].each do |name|
31
+ recs = read_csv( "#{SportDb::Module::Leagues.root}/config/#{name}.csv" )
32
+ leagues.add( recs )
33
+ end
34
+
35
+ ['codes_alt',
36
+ ].each do |name|
37
+ recs = read_csv( "#{SportDb::Module::Leagues.root}/config/#{name}.csv" )
38
+ leagues.add_alt( recs )
39
+ end
40
+ leagues
41
+ end
42
+ @leagues
43
+ end
44
+
45
+ def self.norm( code ) ## use norm_(league)code - why? why not?
46
+ ## norm league code
47
+ ## downcase
48
+ ## and remove all non-letters/digits e.g. at.1 => at1, at 1 => at1 etc.
49
+ ## ö.1 => ö1
50
+ ## note - allow unicode letters!!!
51
+ ## note - assume downcase works for unicode too e.g. Ö=>ö
52
+ ## for now no need to use our own downcase - why? why not?
53
+
54
+ code.downcase.gsub( /[^\p{Ll}0-9]/, '' )
55
+ end
56
+
57
+
58
+
59
+
60
+ def initialize
61
+ ## keep to separate (hash) table for now - why? why not?
62
+ @leagues = {}
63
+ @codes = {}
64
+ end
65
+
66
+
67
+ def add( recs )
68
+ recs.each do |rec|
69
+ key = LeagueCodes.norm( rec['code'] )
70
+ @leagues[ key ] ||= []
71
+
72
+ ## note: auto-change seasons to season object or nil
73
+ @leagues[ key ] << { 'code' => rec['code'],
74
+ 'name' => rec['name'],
75
+ 'basename' => rec['basename'],
76
+ 'start_season' => rec['start_season'].empty? ? nil : Season.parse( rec['start_season'] ),
77
+ 'end_season' => rec['end_season'].empty? ? nil : Season.parse( rec['end_season'] ),
78
+ }
79
+ end
80
+ end
81
+
82
+ ### step two - add alt(ernative) codes
83
+ def add_alt( recs )
84
+ recs.each do |rec|
85
+ key = LeagueCodes.norm( rec['alt'] )
86
+ @codes[ key ] ||= []
87
+
88
+ ### double check code reference
89
+ ## MUST be present for now!!
90
+ ref_key = LeagueCodes.norm( rec['code'] )
91
+ unless @leagues.has_key?( ref_key )
92
+ raise ArgumentError, "league code >#{rec['code']}< for alt code >#{rec['alt']}< not found; sorry"
93
+ end
94
+
95
+ ## note: auto-change seasons to season object or nil
96
+ @codes[ key ] << { 'code' => rec['code'],
97
+ 'alt' => rec['alt'],
98
+ 'start_season' => rec['start_season'].empty? ? nil : Season.parse( rec['start_season'] ),
99
+ 'end_season' => rec['end_season'].empty? ? nil : Season.parse( rec['end_season'] ),
100
+ }
101
+ end
102
+ end
103
+
104
+
105
+ def valid?( code )
106
+ ## check if code is valid
107
+ ## 1) canonical codes (check first - why? why not?)
108
+ ## 2) alt codes
109
+ raise ArgumentError, "league code as string|symbol expected" unless code.is_a?(String) || code.is_a?(Symbol)
110
+
111
+ key = LeagueCodes.norm( code )
112
+ found = @leagues.has_key?( key )
113
+ found = @codes.has_key?( key ) if found == false
114
+ found
115
+ end
116
+
117
+
118
+ def find_by( code:, season: )
119
+ raise ArgumentError, "league code as string|symbol expected" unless code.is_a?(String) || code.is_a?(Symbol)
120
+
121
+ ## return league code record/item or nil
122
+ ## check for alt code first
123
+ season = Season( season )
124
+ key = LeagueCodes.norm( code )
125
+ rec = nil
126
+
127
+ if !@leagues.has_key?( key ) ## try alt codes
128
+ key = _find_alt_code( key, season )
129
+ end
130
+
131
+ if key
132
+ recs = @leagues[ key ]
133
+ if recs
134
+ rec = _find_by_season( recs, season )
135
+ end
136
+ end
137
+
138
+ rec ## return nil if no code record/item found
139
+ end
140
+
141
+
142
+ def _find_alt_code( key, season )
143
+ ## check alt keys
144
+ ref_key = nil
145
+ recs = @codes[key]
146
+ if recs
147
+ rec = _find_by_season( recs, season )
148
+ ## norm code
149
+ ref_key = LeagueCodes.norm( rec['code'] ) if rec
150
+ end
151
+
152
+ ref_key ## return nil if no mapping found
153
+ end
154
+
155
+
156
+ def _find_by_season( recs, season )
157
+ recs.each do |rec|
158
+ start_season = rec['start_season']
159
+ end_season = rec['end_season']
160
+ return rec if (start_season.nil? || start_season <= season) &&
161
+ (end_season.nil? || end_season >= season)
162
+ end
163
+ nil
164
+ end
165
+
166
+
167
+ end # class LeagueCodes
168
+ end # module SportDb
169
+
@@ -0,0 +1,185 @@
1
+
2
+
3
+ ##
4
+ # use/find a better name
5
+ # League Set, League Sheet,
6
+ # Leagueset/LeagueSet, LeagueSheet/Leaguesheet
7
+ # or Leagues (only)??
8
+ # or League Book, League Setup, ??
9
+ # or Workset, Worksheet, Workbook, ...
10
+ #
11
+ # move league config over here from sportdb-writers too!!!!!
12
+ #
13
+ #
14
+ # find a better way to handle league codes
15
+ # always map to canoncial codes - why? why not?
16
+
17
+
18
+
19
+ module SportDb
20
+ class Leagueset
21
+
22
+ def self.parse_args( args )
23
+ ### split args in datasets with leagues and seasons
24
+ datasets = []
25
+ args.each do |arg|
26
+ if arg =~ %r{^[0-9/-]+$} ## season
27
+ if datasets.empty?
28
+ puts "!! ERROR - league required before season arg; sorry"
29
+ exit 1
30
+ end
31
+
32
+ season = Season.parse( arg ) ## check season
33
+ datasets[-1][1] << season
34
+ else ## assume league key
35
+ key = arg.downcase
36
+ datasets << [key, []]
37
+ end
38
+ end
39
+ new(datasets)
40
+ end
41
+
42
+
43
+ def self.parse( txt )
44
+ ### split args in datasets with leagues and seasons
45
+ datasets = []
46
+ recs = parse_csv( txt )
47
+ recs.each do |rec|
48
+ key = rec['league'].downcase
49
+ datasets << [key, []]
50
+
51
+ seasons_str = rec['seasons']
52
+ seasons = seasons_str.split( /[ ]+/ )
53
+
54
+ seasons.each do |season_str|
55
+ ## note - add support for ranges e.g. 2001/02..2010/11
56
+ if season_str.index( '..' )
57
+ fst,snd = season_str.split( '..' )
58
+ # pp [fst,snd]
59
+ fst = Season.parse( fst )
60
+ snd = Season.parse( snd )
61
+ if fst < snd && fst.year? == snd.year?
62
+ datasets[-1][1] += (fst..snd).to_a
63
+ else
64
+ raise ArgumentError, "parse error - invalid season range >#{str}<, 1) two seasons required, 2) first < second, 3) same (year/academic) type"
65
+ end
66
+ else
67
+ season = Season.parse( season_str ) ## check season
68
+ datasets[-1][1] << season
69
+ end
70
+ end
71
+ end
72
+ new(datasets)
73
+ end
74
+
75
+ def self.read( path ) parse( read_text( path )); end
76
+
77
+
78
+
79
+ def initialize( recs )
80
+ @recs = recs
81
+ end
82
+
83
+ def size() @recs.size; end
84
+
85
+ def each( &blk )
86
+ @recs.each do |league_key, seasons|
87
+ blk.call( league_key, seasons )
88
+ end
89
+ end
90
+
91
+
92
+ ### use a function for (re)use
93
+ ### note - may add seasons in place!! (if seasons is empty)
94
+ ##
95
+ ## todo/check - change source_path to (simply) path - why? why not?
96
+ def validate!( source_path: ['.'] )
97
+ each do |league_key, seasons|
98
+
99
+ unless LeagueCodes.valid?( league_key )
100
+ puts "!! ERROR - (leagueset) no league (config) found for code >#{league_key}<; sorry"
101
+ exit 1
102
+ end
103
+
104
+
105
+ if seasons.empty?
106
+ ## simple heuristic to find current season
107
+ [ Season( '2024/25'), Season( '2025') ].each do |season|
108
+ league_info = LeagueCodes.find_by( code: league_key, season: season )
109
+ filename = "#{season.to_path}/#{league_info['code']}.csv"
110
+ path = find_file( filename, path: source_path )
111
+ if path
112
+ seasons << season
113
+ break
114
+ end
115
+ end
116
+
117
+ if seasons.empty?
118
+ puts "!! ERROR - (leagueset) no latest auto-season via source found for #{league_key}; sorry"
119
+ exit 1
120
+ end
121
+ end
122
+
123
+ ## check source path too upfront - why? why not?
124
+ seasons.each do |season|
125
+ ## check league code config too - why? why not?
126
+ league_info = LeagueCodes.find_by( code: league_key, season: season )
127
+ if league_info.nil?
128
+ puts "!! ERROR - (leagueset) no league config found for code #{league_key} AND season #{season}; sorry"
129
+ exit 1
130
+ end
131
+
132
+ filename = "#{season.to_path}/#{league_info['code']}.csv"
133
+ path = find_file( filename, path: source_path )
134
+
135
+ if path.nil?
136
+ puts "!! ERROR - (leagueset) no source found for #{filename}; sorry"
137
+ exit 1
138
+ end
139
+ end
140
+ end # each record
141
+ end
142
+
143
+
144
+
145
+ ## todo/check: find a better name for helper?
146
+ ## find_all_datasets, filter_datatsets - add alias(es???
147
+ ## queries (lik ARGV) e.g. ['at'] or ['eng', 'de'] etc. list of strings
148
+ ##
149
+ ## todo/fix - check if used anywhere???
150
+ ## - check if works with new alt codes too (or needs update)???
151
+
152
+ def filter( queries=[] )
153
+ ## find all matching leagues (that is, league keys)
154
+ if queries.empty? ## no filter - get all league keys
155
+ self
156
+ else
157
+ recs = @recs.find_all do |league_key, seasons|
158
+ found = false
159
+ ## note: normalize league key
160
+ ## (remove dot and downcase)
161
+ norm_key = league_key.gsub( '.', '' )
162
+ queries.each do |query|
163
+ q = query.gsub( '.', '' ).downcase
164
+ if norm_key.start_with?( q )
165
+ found = true
166
+ break
167
+ end
168
+ end
169
+ found
170
+ end
171
+ ## return new typed leagueset
172
+ self.class.new( recs )
173
+ end
174
+ end
175
+
176
+
177
+ def pretty_print( printer )
178
+ printer.text( "<Leagueset: " )
179
+ printer.text( @recs )
180
+ printer.text( ">")
181
+ end
182
+
183
+ end # module Leagueset
184
+ end # module SportDb
185
+