footty 2025.5.2 → 2026.5.25

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 597423aa8445e6fd662c9a6541230726d1d97316e5f9a7ec7c8781dbf5f2f4e6
4
- data.tar.gz: c5bf0acffaa91bd215253eef1b8271348e9596499ff202ef131fa8ef002abd5a
3
+ metadata.gz: 75b4ca658d25b404bf5b4f1594da50c42564ad7ffc43e3190429a7c4627b8c94
4
+ data.tar.gz: 25eac464226fea4b31da270bc195c443e34bc4fa68ce9de1bd0646b8ec63639e
5
5
  SHA512:
6
- metadata.gz: fe813055addd774484ca21f19f4200c4cd567f242121df493192fbb5ea1b96a50a893d157c12d255b17743b1014eed13020914859bd23984aa969836e9302b20
7
- data.tar.gz: 140a848370b4d24ce43c9cc815fb2b28e2745eb68060dcae43415c1a54c770f9aaeb675d0b809ffbaf753b7b1353b9e98a7d021b1fb73fdc5d08d5adfe15b936
6
+ metadata.gz: a1762d1389a13f0380267a7b8a0621b6df1e176eb847a4cac454fcfdfb87ece7321a950e80ad2c1ffedbd38f2966def3c63cda9d159063e568b1854bac0349b9
7
+ data.tar.gz: e791218fe6679d8c03545114cb60a7ce588b8f8dce4f8d80764aaa1da1168f6ab7552af00098ad6165a59d97ecea5fc1292cbb979689835f029bb4d06a1bc5cb
data/CHANGELOG.md CHANGED
@@ -1,4 +1,4 @@
1
- ### 2025.5.2
1
+ ### 2026.5.25
2
2
  ### 1.0.0 / 2018-06-09
3
3
 
4
4
  * Everything is new (again). First release.
data/Manifest.txt CHANGED
@@ -5,7 +5,9 @@ Rakefile
5
5
  bin/footty
6
6
  bin/ftty
7
7
  lib/footty.rb
8
- lib/footty/dataset.rb
9
- lib/footty/openfootball.rb
10
- lib/footty/print.rb
8
+ lib/footty/dataset/dataset.rb
9
+ lib/footty/dataset/openfootball.rb
10
+ lib/footty/main.rb
11
+ lib/footty/pp_matches.rb
12
+ lib/footty/pp_week.rb
11
13
  lib/footty/version.rb
data/README.md CHANGED
@@ -16,22 +16,22 @@ The footty (or ftty) command line tool lets you query the online football.db via
16
16
  for upcoming or past matches. For example:
17
17
 
18
18
  $ footty # Defaults to today's matches of top leagues
19
-
19
+
20
20
 
21
21
  prints on Sep 27, 2024:
22
22
 
23
23
  ==> English Premier League 2024/25
24
24
  Upcoming matches:
25
- Sat Sep/28 12:30 (in 1d) Newcastle United FC vs Manchester City FC Matchday 6
26
- Sat Sep/28 15:00 (in 1d) Arsenal FC vs Leicester City FC Matchday 6
27
- Sat Sep/28 15:00 (in 1d) Brentford FC vs West Ham United FC Matchday 6
28
- Sat Sep/28 15:00 (in 1d) Chelsea FC vs Brighton & Hove Albion Matchday 6
29
- Sat Sep/28 15:00 (in 1d) Everton FC vs Crystal Palace FC Matchday 6
30
- Sat Sep/28 15:00 (in 1d) Nottingham Forest FC vs Fulham FC Matchday 6
31
- Sat Sep/28 17:30 (in 1d) Wolverhampton Wanderers FC vs Liverpool FC Matchday 6
32
- Sun Sep/29 14:00 (in 2d) Ipswich Town FC vs Aston Villa FC Matchday 6
33
- Sun Sep/29 16:30 (in 2d) Manchester United FC vs Tottenham Hotspur FC Matchday 6
34
- Mon Sep/30 20:00 (in 3d) AFC Bournemouth vs Southampton FC Matchday 6
25
+ Sat Sep 28 12:30 (in 1d) Newcastle United FC vs Manchester City FC Matchday 6
26
+ Sat Sep 28 15:00 (in 1d) Arsenal FC vs Leicester City FC Matchday 6
27
+ Sat Sep 28 15:00 (in 1d) Brentford FC vs West Ham United FC Matchday 6
28
+ Sat Sep 28 15:00 (in 1d) Chelsea FC vs Brighton & Hove AlbionMatchday 6
29
+ Sat Sep 28 15:00 (in 1d) Everton FC vs Crystal Palace FC Matchday 6
30
+ Sat Sep 28 15:00 (in 1d) Nottingham Forest FC vs Fulham FC Matchday 6
31
+ Sat Sep 28 17:30 (in 1d) Wolverhampton Wanderers FC vs Liverpool FCMatchday 6
32
+ Sun Sep 29 14:00 (in 2d) Ipswich Town FC vs Aston Villa FC Matchday 6
33
+ Sun Sep 29 16:30 (in 2d) Manchester United FC vs Tottenham Hotspur FC Matchday 6
34
+ Mon Sep 30 20:00 (in 3d) AFC Bournemouth vs Southampton FC Matchday 6
35
35
 
36
36
 
37
37
 
@@ -81,7 +81,7 @@ More
81
81
  - `world` => World Cup
82
82
  - `euro` => "Euro" - European Championship
83
83
 
84
- See [footty/openfootball](https://github.com/sportdb/footty/blob/master/footty/lib/footty/openfootball.rb) for the complete built-in list of data sources (and league codes).
84
+ See [footty/openfootball](https://github.com/sportdb/footty/blob/master/footty/lib/footty/dataset/openfootball.rb) for the complete built-in list of data sources (and league codes).
85
85
 
86
86
 
87
87
 
@@ -110,4 +110,3 @@ Use it as you please with no restrictions whatsoever.
110
110
 
111
111
  Yes, you can. More than welcome.
112
112
  See [Help & Support »](https://github.com/openfootball/help)
113
-
data/Rakefile CHANGED
@@ -17,7 +17,7 @@ Hoe.spec 'footty' do
17
17
  self.history_file = 'CHANGELOG.md'
18
18
 
19
19
  self.extra_deps = [
20
- ['sportdb-quick', '>= 0.2.0'],
20
+ ['sportdb-quick', '>= 0.7.0'],
21
21
  ['webget'],
22
22
  ]
23
23
 
@@ -1,25 +1,26 @@
1
1
  module Footty
2
2
 
3
3
 
4
- class Dataset
4
+ ##
5
+ # note - assume "upstream" date is always date object or nil (NOT string)!!!
5
6
 
7
+ class Dataset
6
8
 
7
9
  def matches
8
10
  raise ArgumentError, "method matches must be implemented by concrete class!!"
9
11
  end
10
12
 
11
13
  def league_name
12
- raise ArgumentError, "method league_name must be implemented by concrete class!!"
14
+ raise ArgumentError, "method league_name must be implemented by concrete class!!"
13
15
  end
14
16
 
15
17
 
18
+
16
19
  def end_date
17
20
  @end_date ||= begin
18
21
  end_date = nil
19
22
  matches.each do |match|
20
- date = Date.strptime(match['date'], '%Y-%m-%d' )
21
- end_date = date if end_date.nil? ||
22
- date > end_date
23
+ end_date = match['date'] if end_date.nil? || match['date'] > end_date
23
24
  end
24
25
  end_date
25
26
  end
@@ -29,58 +30,29 @@ module Footty
29
30
  @start_date ||= begin
30
31
  start_date = nil
31
32
  matches.each do |match|
32
- date = Date.strptime(match['date'], '%Y-%m-%d' )
33
- start_date = date if start_date.nil? ||
34
- date < start_date
33
+ start_date = date = match['date'] if start_date.nil? || date = match['date'] < start_date
35
34
  end
36
35
  start_date
37
36
  end
38
37
  end
39
38
 
40
- def todays_matches( date: Date.today ) matches_for( date ); end
41
- def tomorrows_matches( date: Date.today ) matches_for( date+1 ); end
42
- def yesterdays_matches( date: Date.today ) matches_for( date-1 ); end
43
39
 
44
- def matches_for( date )
45
- matches = select_matches do |match|
46
- date == Date.parse( match['date'] )
47
- end
48
- matches
49
- end
50
-
51
-
52
- def weeks_matches( week_start, week_end ) matches_within( week_start, week_end); end
53
-
54
- def matches_within( start_date, end_date )
55
- matches = select_matches do |match|
56
- date = Date.parse( match['date'] )
57
- date >= start_date && date <= end_date
58
- end
59
- matches
60
- end
40
+ def todays_matches( date: Date.today ) _select_matches { |match| date == match['date'] }; end
41
+ def tomorrows_matches( date: Date.today ) _select_matches { |match| date+1 == match['date'] }; end
42
+ def yesterdays_matches( date: Date.today ) _select_matches { |match| date-1 == match['date'] }; end
61
43
 
62
44
 
63
- def query( q )
64
- ## query/check for team name match for now
65
- rx = /#{Regexp.escape(q)}/i ## use case-insensitive regex match
66
-
67
- matches = select_matches do |match|
68
- if rx.match( match['team1'] ) ||
69
- rx.match( match['team2'] )
70
- true
71
- else
72
- false
73
- end
74
- end
75
- matches
45
+ def weeks_matches( start_week, end_week )
46
+ _select_matches do |match|
47
+ match['date'] >= start_week && match['date'] <= end_week
48
+ end
76
49
  end
77
50
 
78
51
 
79
-
80
52
  def upcoming_matches( date: Date.today,
81
53
  limit: nil )
82
54
  ## note: includes todays matches for now
83
- matches = select_matches { |match| date <= Date.parse( match['date'] ) }
55
+ matches = _select_matches { |match| date <= match['date'] }
84
56
 
85
57
  if limit
86
58
  matches[0, limit] ## cut-off
@@ -89,16 +61,34 @@ module Footty
89
61
  end
90
62
  end
91
63
 
64
+
92
65
  def past_matches( date: Date.today )
93
- matches = select_matches { |match| date > Date.parse( match['date'] ) }
66
+ matches = _select_matches { |match| date > match['date'] }
94
67
  ## note reveserve matches (chronological order/last first)
95
68
  ## matches.reverse
96
69
  matches
97
70
  end
98
71
 
99
72
 
100
- private
101
- def select_matches( &blk)
73
+
74
+ def query( q )
75
+ ## query/check for team name match for now
76
+ rx = /#{Regexp.escape(q)}/i ## use case-insensitive regex match
77
+
78
+ _select_matches do |match|
79
+ if rx.match( match['team1'] ) ||
80
+ rx.match( match['team2'] )
81
+ true
82
+ else
83
+ false
84
+ end
85
+ end
86
+ end
87
+
88
+
89
+
90
+
91
+ def _select_matches( &blk )
102
92
  selected = []
103
93
  matches.each do |match|
104
94
  selected << match if blk.call( match )
@@ -3,7 +3,9 @@ module Footty
3
3
 
4
4
  class OpenfootballDataset < Dataset
5
5
  SOURCES = {
6
- 'world' => { '2022' => [ 'worldcup/2022--qatar/cup.txt',
6
+ 'world' => { '2026' => [ 'worldcup/2026--usa/cup.txt',
7
+ 'worldcup/2026--usa/cup_finals.txt'],
8
+ '2022' => [ 'worldcup/2022--qatar/cup.txt',
7
9
  'worldcup/2022--qatar/cup_finals.txt'],
8
10
  '2018' => [ 'worldcup/2018--russia/cup.txt',
9
11
  'worldcup/2018--russia/cup_finals.txt']
@@ -15,7 +17,7 @@ module Footty
15
17
  'de2' => 'deutschland/$season$/2-bundesliga2.txt',
16
18
  'de3' => 'deutschland/$season$/3-liga3.txt',
17
19
  'decup' => 'deutschland/$season$/cup.txt',
18
-
20
+
19
21
 
20
22
  'en' => 'england/$season$/1-premierleague.txt',
21
23
  'en2' => 'england/$season$/2-championship.txt',
@@ -23,13 +25,13 @@ module Footty
23
25
  ## use eflcup, facup - why? why not?
24
26
  'eneflcup' => 'england/$season$/eflcup.txt',
25
27
  'enfacup' => 'england/$season$/facup.txt',
26
-
28
+
27
29
 
28
30
  'es' => 'espana/$season$/1-liga.txt',
29
31
  'escup' => 'espana/$season$/cup.txt',
30
32
 
31
33
  'it'=> 'italy/$season$/1-seriea.txt',
32
-
34
+
33
35
  'at'=> 'austria/$season$/1-bundesliga.txt',
34
36
  'at2' => 'austria/$season$/2-liga2.txt',
35
37
  'at3o' => 'austria/$season$/3-regionalliga-ost.txt',
@@ -48,7 +50,7 @@ module Footty
48
50
  'br' => 'south-america/brazil/$year$_br1.txt',
49
51
  'ar' => 'south-america/argentina/$year$_ar1.txt',
50
52
  'co' => 'south-america/colombia/$year$_co1.txt',
51
-
53
+
52
54
  ## use a different code for copa libertadores? why? why not?
53
55
  'copa' => 'south-america/copa-libertadores/$year$_copal.txt',
54
56
 
@@ -80,10 +82,10 @@ module Footty
80
82
 
81
83
  ## todo/fix - report error if no spec found
82
84
  season = if spec.is_a?( Hash ) ## assume lookup by year
83
- spec.keys[0]
85
+ spec.keys[0]
84
86
  else ## assume vanilla urls (no lookup by year)
85
- ## default to 2025 or 2024/25 for now
86
- spec.index( '$year$') ? '2025' : '2024/25'
87
+ ## default to 2026 or 2025/26 for now
88
+ spec.index( '$year$') ? '2026' : '2025/26'
87
89
  end
88
90
  season
89
91
  end
@@ -116,13 +118,27 @@ module Footty
116
118
  end
117
119
 
118
120
  matches = matches.map {|match| match.as_json } # convert to json
119
-
120
- ## note - sort by date/time
121
+
122
+ ## note - sort by date/time
121
123
  ## (assume stable sort; no reshuffle of matches if already sorted by date/time)
122
124
 
125
+
126
+ ###
127
+ ## note - convert date to date obj or nil !!!
128
+ matches = matches.map do |match|
129
+ if match['date'] && match['date'].empty? == false
130
+ match['date'] = Date.strptime( match['date'], '%Y-%m-%d' )
131
+ else
132
+ match['date'] = nil
133
+ end
134
+
135
+ match
136
+ end
137
+
138
+
123
139
  matches = matches.sort do |l,r|
124
140
  result = l['date'] <=> r['date']
125
- result = l['time'] <=> r['time'] if result == 0 &&
141
+ result = l['time'] <=> r['time'] if result == 0 &&
126
142
  (l['time'] && r['time'])
127
143
  result
128
144
  end
@@ -131,12 +147,13 @@ module Footty
131
147
  end
132
148
 
133
149
 
150
+
134
151
  def openfootball_url( path, season: )
135
152
  repo, local_path = path.split( '/', 2)
136
153
  url = "https://raw.githubusercontent.com/openfootball/#{repo}/master/#{local_path}"
137
154
  ## check for template vars too
138
155
  season = Season( season )
139
- url = url.gsub( '$year$', season.start_year.to_s )
156
+ url = url.gsub( '$year$', season.start_year.to_s )
140
157
  url = url.gsub( '$season$', season.to_path )
141
158
  url
142
159
  end
@@ -144,7 +161,7 @@ module Footty
144
161
 
145
162
  def matches() @matches; end
146
163
  def league_name() @league_name; end
147
-
164
+
148
165
 
149
166
 
150
167
  def get!( url )
@@ -0,0 +1,248 @@
1
+
2
+
3
+ module Footty
4
+
5
+
6
+ def self.main( args=ARGV )
7
+ puts banner # say hello
8
+
9
+
10
+ opts = { debug: false,
11
+ verbose: false, ## add more details
12
+ ## add cache/cache_dir - why? why not?
13
+
14
+ query: nil,
15
+
16
+ ## display format/mode - week/window/upcoming/past (default is today)
17
+ yesterday: nil,
18
+ tomorrow: nil,
19
+ upcoming: nil,
20
+ past: nil,
21
+
22
+ week: false,
23
+ # window: nil, ## 2 day plus/minus +2/-2
24
+ }
25
+
26
+
27
+ parser = OptionParser.new do |parser|
28
+ parser.banner = "Usage: #{$PROGRAM_NAME} [options] LEAGUES"
29
+
30
+ parser.on( "--verbose",
31
+ "turn on verbose output (default: #{opts[:verbose]})" ) do |verbose|
32
+ opts[:verbose] = true
33
+ end
34
+
35
+ parser.on( "-q NAME", "--query",
36
+ "query mode; display matches where team name matches query" ) do |query|
37
+ opts[:query] = query
38
+ end
39
+
40
+
41
+ parser.on( "-y", "--yesterday" ) do |yesterday|
42
+ opts[:yesterday] = true
43
+ end
44
+ parser.on( "-t", "--tomorrow" ) do |tomorrow|
45
+ opts[:tomorrow] = true
46
+ end
47
+ parser.on( "-p", "--past" ) do |past|
48
+ opts[:past] = true
49
+ end
50
+ parser.on( "-u", "--up", "--upcoming" ) do |upcoming|
51
+ opts[:upcoming] = true
52
+ end
53
+
54
+ parser.on( "-w", "--week",
55
+ "show matches of the (sport) week from tue to mon (default: #{opts[:week]})" ) do |week|
56
+ opts[:week] = true
57
+ end
58
+ end
59
+ parser.parse!( args )
60
+
61
+
62
+ puts "OPTS:"
63
+ p opts
64
+ puts "ARGV:"
65
+ p args
66
+
67
+
68
+ ###
69
+ ## use simple norm(alize) args (that is,) league codes for now
70
+ ## - downcase, strip dot (.) etc.)
71
+ ## e.g. en.facup => enfacup
72
+ ## at.cup => atcup etc.
73
+ args = args.map { |arg| arg.downcase.gsub( /[._-]/, '' ) }
74
+
75
+
76
+
77
+ ######################
78
+ ## note - first check for buil-in "magic" commands
79
+ ## e.g. leagues / codes - dump built-in league codes
80
+
81
+ if args.include?( 'leagues' )
82
+ puts "==> openfootball dataset sources:"
83
+ pp OpenfootballDataset::SOURCES
84
+
85
+ ## pretty print keys/codes only
86
+ puts
87
+ puts OpenfootballDataset::SOURCES.keys.join( ' ' )
88
+ puts " #{OpenfootballDataset::SOURCES.keys.size} league code(s)"
89
+
90
+ exit 1
91
+ end
92
+
93
+
94
+
95
+ top = [['world', '2026'], ## world cup (w/ national teams)
96
+ # ['euro', '2024'],
97
+ # ['mls', '2025'],
98
+ # ['concacafcl', '2025'],
99
+ # ['mx', '2024/25'],
100
+ ['en', '2025/26'],
101
+ ['es', '2025/26'],
102
+ ['it', '2025/26'],
103
+ ['fr', '2025/26'],
104
+ ['de', '2025/26'],
105
+ # ['decup', '2025/26'],
106
+ # ['at', '2025/26'],
107
+ # ['atcup', '2025/26'],
108
+ ['uefacl', '2025/26'],
109
+ # ['uefael', '2025/26'],
110
+ # ['uefaconf', '2025/26'],
111
+ ['br', '2026'],
112
+ ['copa', '2026'], ## copa libertadores (not copa america,etc.)
113
+ ]
114
+
115
+
116
+ leagues = if args.size == 0
117
+ top
118
+ else
119
+ ### auto-fill (latest) season/year
120
+ args.map do |arg|
121
+ [arg, OpenfootballDataset.latest_season( league: arg )]
122
+ end
123
+ end
124
+
125
+
126
+ ## fetch leagues
127
+ datasets = leagues.map do |league, season|
128
+ dataset = OpenfootballDataset.new( league: league, season: season )
129
+ ## parse matches
130
+ matches = dataset.matches
131
+ puts " #{league} #{season} - #{matches.size} match(es)"
132
+ dataset
133
+ end
134
+
135
+
136
+
137
+ ###################
138
+ ## check for query option to filter matches by query (team)
139
+ if opts[:query]
140
+ q = opts[:query]
141
+ puts
142
+ puts
143
+ datasets.each do |dataset|
144
+ matches = dataset.query( q )
145
+
146
+ if matches.size == 0
147
+ ## siltently skip for now
148
+ else ## assume matches found
149
+ print "==> #{dataset.league_name}"
150
+ print " #{dataset.start_date} - #{dataset.end_date}"
151
+ print " -- #{dataset.matches.size} match(es)"
152
+ print "\n"
153
+ print_matches( matches )
154
+ end
155
+ end
156
+ exit 1
157
+ end
158
+
159
+
160
+ # Dataset.new( league: 'euro', year: 2024 )
161
+ # dataset = Dataset.new( league: league, year: year )
162
+
163
+ ## in the future make today "configurable" as param - why? why not?
164
+ today = Date.today
165
+
166
+
167
+ what = if opts[:yesterday]
168
+ 'yesterday'
169
+ elsif opts[:tomorrow]
170
+ 'tomorrow'
171
+ elsif opts[:past]
172
+ 'past'
173
+ elsif opts[:upcoming]
174
+ 'upcoming'
175
+ elsif opts[:week]
176
+ 'week'
177
+ else
178
+ 'today'
179
+ end
180
+
181
+
182
+ ## if week get week number and start and end date (tuesday to mondey)
183
+ if what == 'week'
184
+ week_start, week_end = Footty.week_tue_to_mon( today)
185
+ puts
186
+ puts "=== " + Footty.fmt_week( week_start, week_end ) + " ==="
187
+ else
188
+ ## start with two empty lines - assume (massive) debug output before ;-)
189
+ puts
190
+ puts
191
+ end
192
+
193
+ datasets.each do |dataset|
194
+ print "==> #{dataset.league_name}"
195
+ print " #{dataset.start_date} - #{dataset.end_date}"
196
+ print " -- #{dataset.matches.size} match(es)"
197
+ print "\n"
198
+
199
+ if what == 'week'
200
+ matches = dataset.weeks_matches( week_start, week_end )
201
+ if matches.empty?
202
+ puts (' '*4) + "** No matches scheduled or played in week #{week_start.cweek}.\n"
203
+ end
204
+ elsif what == 'yesterday'
205
+ matches = dataset.yesterdays_matches
206
+ if matches.empty?
207
+ puts (' '*4) + "** No matches played yesterday.\n"
208
+ end
209
+ elsif what == 'tomorrow'
210
+ matches = dataset.tomorrows_matches
211
+ if matches.empty?
212
+ puts (' '*4) + "** No matches scheduled tomorrow.\n"
213
+ end
214
+ elsif what == 'past'
215
+ matches = dataset.past_matches
216
+ if matches.empty?
217
+ puts (' '*4) + "** No matches played yet.\n"
218
+ end
219
+ elsif what == 'upcoming'
220
+ matches = dataset.upcoming_matches
221
+ if matches.empty?
222
+ puts (' '*4) + "** No more matches scheduled.\n"
223
+ end
224
+ else ## assume today
225
+ matches = dataset.todays_matches
226
+
227
+ ## no matches today
228
+ if matches.empty?
229
+ puts (' '*4) + "** No matches scheduled today.\n"
230
+
231
+ if opts[:verbose]
232
+ ## note: was world cup 2018 - end date -- Date.new( 2018, 7, 11 )
233
+ ## note: was euro 2020 (in 2021) - end date -- Date.new( 2021, 7, 11 )
234
+ if Date.today > dataset.end_date ## tournament is over, look back
235
+ puts "Past matches:"
236
+ matches = dataset.past_matches
237
+ else ## world cup is upcoming /in-progress,look forward
238
+ puts "Upcoming matches:"
239
+ matches = dataset.upcoming_matches( limit: 18 )
240
+ end
241
+ end
242
+ end
243
+ end
244
+ print_matches( matches )
245
+ end
246
+
247
+ end # method self.main
248
+ end # module Footty
@@ -0,0 +1,135 @@
1
+
2
+ module Footty
3
+
4
+
5
+ ##
6
+ # note - assume "upstream" date is always date object or nil (NOT string)!!!
7
+
8
+ def self.print_matches( matches )
9
+
10
+ today = Date.today
11
+
12
+ matches.each do |match|
13
+ date = match['date']
14
+ print "#{date.strftime('%a %b %d')} " ## e.g. Thu Jun 14
15
+ print "#{match['time']} " if match['time']
16
+
17
+ if date > today
18
+ diff = (date - today).to_i
19
+ print "%10s" % "(in #{diff}d) "
20
+ end
21
+
22
+
23
+ if match['team1'].is_a?( Hash )
24
+ print "%22s" % "#{match['team1']['name']} (#{match['team1']['code']})"
25
+ else
26
+ print "%22s" % "#{match['team1']}"
27
+ end
28
+
29
+
30
+
31
+ if match['score'].is_a?( Hash )
32
+
33
+ if match['score']['et']
34
+ et = "#{match['score']['et'][0]}-#{match['score']['et'][1]} aet"
35
+ print " #{et}"
36
+ end
37
+
38
+ if match['score']['ft']
39
+ ft = "#{match['score']['ft'][0]}-#{match['score']['ft'][1]}"
40
+ if match['score']['et']
41
+ print " (#{ft})"
42
+ else
43
+ print " #{ft}"
44
+ end
45
+ end
46
+
47
+ if match['score']['p']
48
+ pen = "#{match['score']['p'][0]}-#{match['score']['p'][1]} pen"
49
+ print ", #{pen}"
50
+ end
51
+ print " "
52
+
53
+ elsif match['score'] &&
54
+ match['score'][0] && match['score'][1]
55
+ score = "#{match['score'][0]}-#{match['score'][1]}"
56
+ print " #{score} "
57
+ else
58
+ print " vs "
59
+ end
60
+
61
+ if match['team2'].is_a?( Hash )
62
+ print "%-22s" % "#{match['team2']['name']} (#{match['team2']['code']})"
63
+ else
64
+ print "%-22s" % "#{match['team2']}"
65
+ end
66
+
67
+
68
+ print "▪" ## note - add round marker!!
69
+
70
+
71
+ print " #{match['group']} /" if match['group'] ## (optional) group
72
+
73
+
74
+ print " #{match['round']} " ## knock out (k.o.) phase/stage
75
+
76
+
77
+ print "%-5s " % "(\##{match['num']}) " if match['num']
78
+
79
+ print " @ #{match['ground']}" if match['ground']
80
+
81
+
82
+ print "\n"
83
+
84
+
85
+ if match['goals1'] && match['goals2']
86
+ print " ("
87
+
88
+ print _pp_goals(match['goals1']) if match['goals1'].size > 0
89
+
90
+ print "; " if match['goals1'].size > 0 &&
91
+ match['goals2'].size > 0
92
+
93
+ print _pp_goals(match['goals2']) if match['goals2'].size > 0
94
+
95
+ print ")\n"
96
+ end
97
+ end
98
+ end
99
+
100
+
101
+
102
+ def self._pp_goals( recs )
103
+ players = {}
104
+
105
+ ## "fold" multiple goals of player
106
+ recs.each do |rec|
107
+
108
+ name = rec['name']
109
+
110
+ if rec['minute'].nil? || rec['minute'].empty?
111
+ puts "!! WARN - (goals) minute empty:"
112
+ pp rec
113
+ ## use '??'
114
+ rec['minute'] = '??'
115
+ ## raise ArgumentError, "minute empty"
116
+ end
117
+
118
+ goal = String.new
119
+ goal << "#{rec['minute']}'"
120
+ goal << '(og)' if rec['owngoal'] == true
121
+ goal << '(p)' if rec['penalty'] == true
122
+
123
+ player_rec = players[ name ] ||= { name: name, goals: [] }
124
+ player_rec[:goals] << goal
125
+ end
126
+
127
+
128
+ buf = players.map do |_,player|
129
+ "#{player[:name]} #{player[:goals].join(',')}"
130
+ end.join( ' ' )
131
+ buf
132
+ end
133
+
134
+
135
+ end # module Footty
@@ -0,0 +1,24 @@
1
+
2
+ module Footty
3
+
4
+
5
+ def self.week_tue_to_mon( today=Date.today )
6
+ ## Calculate the start of the (sport) week (tuesday)
7
+ ## note - wday starts counting sunday (0), monday (1), etc.
8
+ week_tue = today - (today.wday - 2) % 7
9
+ week_mon = week_tue + 6
10
+
11
+ [week_tue,week_mon]
12
+ end
13
+
14
+
15
+ def self.fmt_week( week_start, week_end )
16
+ buf = String.new
17
+ buf << "Week %02d" % week_start.cweek
18
+ buf << " - #{week_start.strftime( "%a %b %-d")}"
19
+ buf << " to #{week_end.strftime( "%a %b %-d %Y")}"
20
+ buf
21
+ end
22
+
23
+
24
+ end # module Footty
@@ -1,6 +1,6 @@
1
1
 
2
2
  module Footty
3
- VERSION = '2025.5.2'
3
+ VERSION = '2026.5.25'
4
4
 
5
5
  def self.banner
6
6
  "footty/#{VERSION} on Ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}] in (#{root})"
@@ -10,5 +10,3 @@ module Footty
10
10
  File.expand_path( File.dirname(File.dirname(File.dirname(__FILE__))) )
11
11
  end
12
12
  end
13
-
14
-
data/lib/footty.rb CHANGED
@@ -1,294 +1,33 @@
1
1
  require 'sportdb/quick' ## note - pulls in cocos et al
2
- require 'webget' ## add webcache support
3
-
4
- require 'optparse'
2
+ require 'webget' ## add webcache support
5
3
 
6
4
 
7
5
 
8
6
 
9
7
  # our own code
10
8
  require_relative 'footty/version' # let version always go first
11
- require_relative 'footty/dataset'
12
- require_relative 'footty/openfootball'
13
-
14
- require_relative 'footty/print'
15
-
16
-
17
-
18
- ## set cache to local .cache dir for now - why? why not?
19
- Webcache.root = './cache'
20
- # pp Webcache.root
21
- Webget.config.sleep = 1 ## set delay in secs (to 1 sec - default is/maybe 3)
22
-
23
-
24
- module Footty
25
-
26
-
27
- def self.week_tue_to_mon( today=Date.today )
28
- ## Calculate the start of the (sport) week (tuesday)
29
- ## note - wday starts counting sunday (0), monday (1), etc.
30
- week_tue = today - (today.wday - 2) % 7
31
- week_mon = week_tue + 6
32
-
33
- [week_tue,week_mon]
34
- end
35
-
36
- def self.fmt_week( week_start, week_end )
37
- buf = String.new
38
- buf << "Week %02d" % week_start.cweek
39
- buf << " - #{week_start.strftime( "%a %b/%-d")}"
40
- buf << " to #{week_end.strftime( "%a %b/%-d %Y")}"
41
- buf
42
- end
43
-
44
-
45
-
46
- def self.main( args=ARGV )
47
- puts banner # say hello
48
-
49
-
50
- opts = { debug: false,
51
- verbose: false, ## add more details
52
- ## add cache/cache_dir - why? why not?
53
-
54
- query: nil,
55
-
56
- ## display format/mode - week/window/upcoming/past (default is today)
57
- yesterday: nil,
58
- tomorrow: nil,
59
- upcoming: nil,
60
- past: nil,
61
-
62
- week: false,
63
- # window: nil, ## 2 day plus/minus +2/-2
64
- }
65
-
66
-
67
- parser = OptionParser.new do |parser|
68
- parser.banner = "Usage: #{$PROGRAM_NAME} [options] LEAGUES"
69
-
70
- parser.on( "--verbose",
71
- "turn on verbose output (default: #{opts[:verbose]})" ) do |verbose|
72
- opts[:verbose] = true
73
- end
74
-
75
- parser.on( "-q NAME", "--query",
76
- "query mode; display matches where team name matches query" ) do |query|
77
- opts[:query] = query
78
- end
79
9
 
10
+ require_relative 'footty/dataset/dataset'
11
+ require_relative 'footty/dataset/openfootball'
80
12
 
81
- parser.on( "-y", "--yesterday" ) do |yesterday|
82
- opts[:yesterday] = true
83
- end
84
- parser.on( "-t", "--tomorrow" ) do |tomorrow|
85
- opts[:tomorrow] = true
86
- end
87
- parser.on( "-p", "--past" ) do |past|
88
- opts[:past] = true
89
- end
90
- parser.on( "-u", "--up", "--upcoming" ) do |upcoming|
91
- opts[:upcoming] = true
92
- end
13
+ require_relative 'footty/pp_matches'
14
+ require_relative 'footty/pp_week'
93
15
 
94
- parser.on( "-w", "--week",
95
- "show matches of the (sport) week from tue to mon (default: #{opts[:week]})" ) do |week|
96
- opts[:week] = true
97
- end
98
- end
99
- parser.parse!( args )
16
+ require_relative 'footty/main'
100
17
 
101
18
 
102
- puts "OPTS:"
103
- p opts
104
- puts "ARGV:"
105
- p args
106
19
 
107
20
 
108
- ###
109
- ## use simple norm(alize) args (that is,) league codes for now
110
- ## - downcase, strip dot (.) etc.)
111
- ## e.g. en.facup => enfacup
112
- ## at.cup => atcup etc.
113
- args = args.map { |arg| arg.downcase.gsub( /[._-]/, '' ) }
114
21
 
22
+ ## set cache to local .cache dir for now - why? why not?
23
+ Webcache.root = './cache'
115
24
 
25
+ # pp Webcache.root
26
+ Webget.config.sleep = 1 ## set delay in secs (to 1 sec - default is/maybe 3)
116
27
 
117
- ######################
118
- ## note - first check for buil-in "magic" commands
119
- ## e.g. leagues / codes - dump built-in league codes
120
-
121
- if args.include?( 'leagues' )
122
- puts "==> openfootball dataset sources:"
123
- pp OpenfootballDataset::SOURCES
124
-
125
- ## pretty print keys/codes only
126
- puts
127
- puts OpenfootballDataset::SOURCES.keys.join( ' ' )
128
- puts " #{OpenfootballDataset::SOURCES.keys.size} league code(s)"
129
-
130
- exit 1
131
- end
132
-
133
-
134
-
135
-
136
-
137
- top = [['world', '2022'],
138
- ['euro', '2024'],
139
- ['mls', '2025'],
140
- ['concacafcl', '2025'],
141
- ['mx', '2024/25'],
142
- ['copa', '2025'], ## copa libertadores
143
- ['en', '2024/25'],
144
- ['es', '2024/25'],
145
- ['it', '2024/25'],
146
- ['fr', '2024/25'],
147
- ['de', '2024/25'],
148
- ['decup', '2024/25'],
149
- ['at', '2024/25'],
150
- ['atcup', '2024/25'],
151
- ['uefacl', '2024/25'],
152
- ['uefael', '2024/25'],
153
- ['uefaconf', '2024/25'],
154
- ]
155
-
156
-
157
- leagues = if args.size == 0
158
- top
159
- else
160
- ### auto-fill (latest) season/year
161
- args.map do |arg|
162
- [arg, OpenfootballDataset.latest_season( league: arg )]
163
- end
164
- end
165
-
166
-
167
- ## fetch leagues
168
- datasets = leagues.map do |league, season|
169
- dataset = OpenfootballDataset.new( league: league, season: season )
170
- ## parse matches
171
- matches = dataset.matches
172
- puts " #{league} #{season} - #{matches.size} match(es)"
173
- dataset
174
- end
175
-
176
-
177
-
178
- ###################
179
- ## check for query option to filter matches by query (team)
180
- if opts[:query]
181
- q = opts[:query]
182
- puts
183
- puts
184
- datasets.each do |dataset|
185
- matches = dataset.query( q )
186
-
187
- if matches.size == 0
188
- ## siltently skip for now
189
- else ## assume matches found
190
- print "==> #{dataset.league_name}"
191
- print " #{dataset.start_date} - #{dataset.end_date}"
192
- print " -- #{dataset.matches.size} match(es)"
193
- print "\n"
194
- print_matches( matches )
195
- end
196
- end
197
- exit 1
198
- end
199
-
200
-
201
- # Dataset.new( league: 'euro', year: 2024 )
202
- # dataset = Dataset.new( league: league, year: year )
203
-
204
- ## in the future make today "configurable" as param - why? why not?
205
- today = Date.today
206
-
207
-
208
- what = if opts[:yesterday]
209
- 'yesterday'
210
- elsif opts[:tomorrow]
211
- 'tomorrow'
212
- elsif opts[:past]
213
- 'past'
214
- elsif opts[:upcoming]
215
- 'upcoming'
216
- elsif opts[:week]
217
- 'week'
218
- else
219
- 'today'
220
- end
221
-
222
-
223
- ## if week get week number and start and end date (tuesday to mondey)
224
- if what == 'week'
225
- week_start, week_end = Footty.week_tue_to_mon( today)
226
- puts
227
- puts "=== " + Footty.fmt_week( week_start, week_end ) + " ==="
228
- else
229
- ## start with two empty lines - assume (massive) debug output before ;-)
230
- puts
231
- puts
232
- end
233
-
234
- datasets.each do |dataset|
235
- print "==> #{dataset.league_name}"
236
- print " #{dataset.start_date} - #{dataset.end_date}"
237
- print " -- #{dataset.matches.size} match(es)"
238
- print "\n"
239
-
240
- if what == 'week'
241
- matches = dataset.weeks_matches( week_start, week_end )
242
- if matches.empty?
243
- puts (' '*4) + "** No matches scheduled or played in week #{week_start.cweek}.\n"
244
- end
245
- elsif what == 'yesterday'
246
- matches = dataset.yesterdays_matches
247
- if matches.empty?
248
- puts (' '*4) + "** No matches played yesterday.\n"
249
- end
250
- elsif what == 'tomorrow'
251
- matches = dataset.tomorrows_matches
252
- if matches.empty?
253
- puts (' '*4) + "** No matches scheduled tomorrow.\n"
254
- end
255
- elsif what == 'past'
256
- matches = dataset.past_matches
257
- if matches.empty?
258
- puts (' '*4) + "** No matches played yet.\n"
259
- end
260
- elsif what == 'upcoming'
261
- matches = dataset.upcoming_matches
262
- if matches.empty?
263
- puts (' '*4) + "** No more matches scheduled.\n"
264
- end
265
- else ## assume today
266
- matches = dataset.todays_matches
267
-
268
- ## no matches today
269
- if matches.empty?
270
- puts (' '*4) + "** No matches scheduled today.\n"
271
-
272
- if opts[:verbose]
273
- ## note: was world cup 2018 - end date -- Date.new( 2018, 7, 11 )
274
- ## note: was euro 2020 (in 2021) - end date -- Date.new( 2021, 7, 11 )
275
- if Date.today > dataset.end_date ## tournament is over, look back
276
- puts "Past matches:"
277
- matches = dataset.past_matches
278
- else ## world cup is upcoming /in-progress,look forward
279
- puts "Upcoming matches:"
280
- matches = dataset.upcoming_matches( limit: 18 )
281
- end
282
- end
283
- end
284
- end
285
- print_matches( matches )
286
- end
287
28
 
288
- end # method self.main
289
- end # module Footty
290
29
 
291
30
 
292
31
 
293
32
 
294
- Footty.main if __FILE__ == $0
33
+ Footty.main if __FILE__ == $0
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: footty
3
3
  version: !ruby/object:Gem::Version
4
- version: 2025.5.2
4
+ version: 2026.5.25
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gerald Bauer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-05-02 00:00:00.000000000 Z
11
+ date: 2026-05-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sportdb-quick
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.2.0
19
+ version: 0.7.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 0.2.0
26
+ version: 0.7.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: webget
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -91,9 +91,11 @@ files:
91
91
  - bin/footty
92
92
  - bin/ftty
93
93
  - lib/footty.rb
94
- - lib/footty/dataset.rb
95
- - lib/footty/openfootball.rb
96
- - lib/footty/print.rb
94
+ - lib/footty/dataset/dataset.rb
95
+ - lib/footty/dataset/openfootball.rb
96
+ - lib/footty/main.rb
97
+ - lib/footty/pp_matches.rb
98
+ - lib/footty/pp_week.rb
97
99
  - lib/footty/version.rb
98
100
  homepage: https://github.com/sportdb/footty
99
101
  licenses:
data/lib/footty/print.rb DELETED
@@ -1,102 +0,0 @@
1
-
2
- module Footty
3
-
4
- def self.print_matches( matches )
5
-
6
- today = Date.today
7
-
8
- matches.each do |match|
9
- print " %5s" % "\##{match['num']} " if match['num']
10
-
11
- date = Date.strptime( match['date'], '%Y-%m-%d' )
12
- print "#{date.strftime('%a %b/%d')} " ## e.g. Thu Jun/14
13
- print "#{match['time']} " if match['time']
14
-
15
- if date > today
16
- diff = (date - today).to_i
17
- print "%10s" % "(in #{diff}d) "
18
- end
19
-
20
-
21
- if match['team1'].is_a?( Hash )
22
- print "%22s" % "#{match['team1']['name']} (#{match['team1']['code']})"
23
- else
24
- print "%22s" % "#{match['team1']}"
25
- end
26
-
27
-
28
- if match['score'].is_a?( Hash ) &&
29
- match['score']['ft']
30
- if match['score']['ft']
31
- print " #{match['score']['ft'][0]}-#{match['score']['ft'][1]} "
32
- end
33
- if match['score']['et']
34
- print "aet #{match['score']['et'][0]}-#{match['score']['et'][1]} "
35
- end
36
- if match['score']['p']
37
- print "pen #{match['score']['p'][0]}-#{match['score']['p'][1]} "
38
- end
39
- elsif match['score1'] && match['score2']
40
- ## todo/fix: add support for knockout scores
41
- ## with score1et/score1p (extra time and penalty)
42
- print " #{match['score1']}-#{match['score2']} "
43
- print "(#{match['score1i']}-#{match['score2i']}) "
44
- else
45
- print " vs "
46
- end
47
-
48
- if match['team2'].is_a?( Hash )
49
- print "%-22s" % "#{match['team2']['name']} (#{match['team2']['code']})"
50
- else
51
- print "%-22s" % "#{match['team2']}"
52
- end
53
-
54
- if match['stage']
55
- print " #{match['stage']} /" ## stage
56
- end
57
-
58
- if match['group']
59
- print " #{match['group']} /" ## group phase
60
- end
61
-
62
- print " #{match['round']} " ## knock out (k.o.) phase/stage
63
-
64
- ## todo/fix - check for ground name in use???
65
- if match['stadium']
66
- print " @ #{match['stadium']['name']}, #{match['city']}"
67
- end
68
-
69
- print "\n"
70
-
71
-
72
- if match['goals1'] && match['goals2']
73
- print " ["
74
- match['goals1'].each_with_index do |goal,i|
75
- print " " if i > 0
76
- print "#{goal['name']}"
77
- print " #{goal['minute']}"
78
- print "+#{goal['offset']}" if goal['offset']
79
- print "'"
80
- print " (o.g.)" if goal['owngoal']
81
- print " (pen.)" if goal['penalty']
82
- end
83
- match['goals2'].each_with_index do |goal,i|
84
- if i == 0
85
- print "; "
86
- else
87
- print " "
88
- end
89
- print "#{goal['name']}"
90
- print " #{goal['minute']}"
91
- print "+#{goal['offset']}" if goal['offset']
92
- print "'"
93
- print " (o.g.)" if goal['owngoal']
94
- print " (pen.)" if goal['penalty']
95
- end
96
- print "]\n"
97
- end
98
- end
99
- end
100
-
101
-
102
- end # module Footty