Module: Footty

Defined in:
lib/footty.rb,
lib/footty/print.rb,
lib/footty/dataset.rb,
lib/footty/version.rb,
lib/footty/openfootball.rb

Defined Under Namespace

Classes: Dataset, OpenfootballDataset

Constant Summary collapse

VERSION =
'2025.5.2'

Class Method Summary collapse

Class Method Details



5
6
7
# File 'lib/footty/version.rb', line 5

def self.banner
  "footty/#{VERSION} on Ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}] in (#{root})"
end

.fmt_week(week_start, week_end) ⇒ Object



36
37
38
39
40
41
42
# File 'lib/footty.rb', line 36

def self.fmt_week( week_start, week_end )
  buf = String.new
  buf << "Week %02d" % week_start.cweek
  buf << " - #{week_start.strftime( "%a %b/%-d")}"
  buf << " to #{week_end.strftime( "%a %b/%-d %Y")}"
  buf 
end

.main(args = ARGV) ⇒ Object



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/footty.rb', line 46

def self.main( args=ARGV )
  puts banner # say hello


  opts = {  debug:   false,
            verbose: false,    ## add more details
            ## add cache/cache_dir - why? why not?

            query:   nil,

            ## display format/mode  - week/window/upcoming/past (default is today)
            yesterday: nil,
            tomorrow:  nil,
            upcoming:  nil,
            past:      nil,

            week:      false,
            #  window:    nil,   ## 2 day plus/minus  +2/-2
         }

         
  parser = OptionParser.new do |parser|
    parser.banner = "Usage: #{$PROGRAM_NAME} [options] LEAGUES"

    parser.on( "--verbose", 
               "turn on verbose output (default: #{opts[:verbose]})" ) do |verbose|
      opts[:verbose] = true
    end   

    parser.on( "-q NAME", "--query",
                 "query mode; display matches where team name matches query" ) do |query|
      opts[:query] = query
    end


    parser.on( "-y", "--yesterday" ) do |yesterday|
      opts[:yesterday] = true
    end
    parser.on( "-t", "--tomorrow" ) do |tomorrow|
      opts[:tomorrow] = true
    end
    parser.on( "-p", "--past" ) do |past|
      opts[:past] = true
    end
    parser.on( "-u", "--up", "--upcoming" ) do |upcoming|
      opts[:upcoming] = true
    end

    parser.on( "-w", "--week",
                "show matches of the (sport) week from tue to mon (default: #{opts[:week]})" ) do |week|
      opts[:week] = true
    end
  end
  parser.parse!( args )


  puts "OPTS:"
  p opts
  puts "ARGV:"
  p args


  ###
  ##   use simple norm(alize) args (that is,) league codes for now
  ##      - downcase, strip dot (.) etc.)
  ##   e.g.  en.facup   => enfacup
  ##         at.cup     => atcup     etc.
  args = args.map { |arg| arg.downcase.gsub( /[._-]/, '' ) }



  ######################
  ## note - first check for buil-in "magic" commands
  ##   e.g. leagues / codes    -  dump built-in league codes

  if args.include?( 'leagues' ) 
     puts "==> openfootball dataset sources:"
     pp OpenfootballDataset::SOURCES
     
     ## pretty print keys/codes only
     puts  
     puts OpenfootballDataset::SOURCES.keys.join( ' ' )
     puts "   #{OpenfootballDataset::SOURCES.keys.size} league code(s)"

     exit 1
  end




  
  top = [['world',   '2022'],
         ['euro',   '2024'],
         ['mls',    '2025'],
         ['concacafcl', '2025'],
         ['mx',     '2024/25'],
         ['copa',   '2025'],       ## copa libertadores
         ['en',     '2024/25'],
         ['es',     '2024/25'],
         ['it',     '2024/25'],
         ['fr',     '2024/25'],
         ['de',     '2024/25'],
         ['decup',  '2024/25'],
         ['at',     '2024/25'],
         ['atcup',   '2024/25'],
         ['uefacl',   '2024/25'],
         ['uefael',   '2024/25'],
         ['uefaconf', '2024/25'],
        ]
 
        
  leagues =  if args.size == 0
                 top
             else
                 ### auto-fill (latest) season/year 
                 args.map do |arg| 
                            [arg, OpenfootballDataset.latest_season( league: arg )]  
                          end
             end


  ## fetch leagues
  datasets =  leagues.map do |league, season|
                              dataset = OpenfootballDataset.new( league: league, season: season )
                              ## parse matches
                              matches = dataset.matches
                              puts "  #{league} #{season} - #{matches.size} match(es)"                                 
                              dataset
                          end                     
  


  ###################
  ##  check for query option to filter matches by query (team)
  if opts[:query]
    q = opts[:query]
    puts
    puts
    datasets.each do |dataset|
      matches = dataset.query( q )

      if matches.size == 0
         ## siltently skip for now
      else  ## assume matches found
        print "==> #{dataset.league_name}"
        print "   #{dataset.start_date} - #{dataset.end_date}"
        print "   -- #{dataset.matches.size} match(es)"
        print "\n"
        print_matches( matches )
      end
    end
    exit 1
  end


  # Dataset.new( league: 'euro', year: 2024 )
  # dataset = Dataset.new( league: league, year: year )

  ## in the future make today "configurable" as param - why? why not?
  today = Date.today


   what =  if opts[:yesterday]
             'yesterday'
           elsif opts[:tomorrow]
             'tomorrow'
           elsif opts[:past]
             'past'
           elsif opts[:upcoming]
             'upcoming'
           elsif opts[:week]
             'week'
           else
             'today'
           end


  ## if week get week number and start and end date (tuesday to mondey)
  if what == 'week'
    week_start, week_end = Footty.week_tue_to_mon( today)
    puts
    puts  "=== " + Footty.fmt_week( week_start, week_end ) + " ==="
  else
    ## start with two empty lines - assume (massive) debug output before ;-)
    puts
    puts
  end

  datasets.each do |dataset|
    print "==> #{dataset.league_name}"
    print "   #{dataset.start_date} - #{dataset.end_date}"
    print "   -- #{dataset.matches.size} match(es)"
    print "\n"

    if what == 'week'
      matches = dataset.weeks_matches( week_start, week_end )    
      if matches.empty?
        puts (' '*4) + "** No matches scheduled or played in week #{week_start.cweek}.\n"
      end
   elsif what == 'yesterday'
      matches = dataset.yesterdays_matches
      if matches.empty?
         puts (' '*4) + "** No matches played yesterday.\n"
      end
    elsif what == 'tomorrow'
      matches = dataset.tomorrows_matches
      if matches.empty?
         puts (' '*4) + "** No matches scheduled tomorrow.\n"
      end
    elsif what == 'past'
      matches = dataset.past_matches
      if matches.empty?
         puts (' '*4) + "** No matches played yet.\n"
      end
    elsif what == 'upcoming'
      matches = dataset.upcoming_matches
      if matches.empty?
         puts (' '*4) + "** No more matches scheduled.\n"
      end
    else   ## assume today
       matches = dataset.todays_matches

       ## no matches today
       if matches.empty?
          puts (' '*4) + "** No matches scheduled today.\n"

          if opts[:verbose]
            ## note: was world cup 2018 - end date -- Date.new( 2018, 7, 11 )
            ## note: was euro 2020 (in 2021) - end date -- Date.new( 2021, 7, 11 )
            if Date.today > dataset.end_date    ## tournament is over, look back
              puts "Past matches:"
              matches = dataset.past_matches
            else  ## world cup is upcoming /in-progress,look forward
              puts "Upcoming matches:"
              matches = dataset.upcoming_matches( limit: 18 )
            end
          end
       end
     end
     print_matches( matches )
    end

end


4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/footty/print.rb', line 4

def self.print_matches( matches )

  today = Date.today

  matches.each do |match|
    print "   %5s" % "\##{match['num']} "   if match['num']

    date = Date.strptime( match['date'], '%Y-%m-%d' )
    print "#{date.strftime('%a %b/%d')} "      ## e.g. Thu Jun/14
    print "#{match['time']} "  if match['time']

    if date > today
       diff = (date - today).to_i
       print "%10s" % "(in #{diff}d) "
    end


    if match['team1'].is_a?( Hash )
      print "%22s" % "#{match['team1']['name']} (#{match['team1']['code']})"
    else
      print "%22s" % "#{match['team1']}"
    end


    if match['score'].is_a?( Hash ) &&
       match['score']['ft']
        if match['score']['ft']
           print " #{match['score']['ft'][0]}-#{match['score']['ft'][1]} "
        end
        if match['score']['et']
          print "aet #{match['score']['et'][0]}-#{match['score']['et'][1]} "
        end
        if match['score']['p']
          print "pen #{match['score']['p'][0]}-#{match['score']['p'][1]} "
        end
    elsif match['score1'] && match['score2']
      ## todo/fix: add support for knockout scores
      ##                 with score1et/score1p  (extra time and penalty)
      print " #{match['score1']}-#{match['score2']} "
      print "(#{match['score1i']}-#{match['score2i']}) "
    else
      print "    vs    "
    end

    if match['team2'].is_a?( Hash )
      print "%-22s" % "#{match['team2']['name']} (#{match['team2']['code']})"
    else
      print "%-22s" % "#{match['team2']}"
    end

    if match['stage']
      print " #{match['stage']} /"    ## stage
    end

    if match['group']
      print " #{match['group']} /"    ## group phase
    end

    print " #{match['round']} "    ## knock out (k.o.) phase/stage

    ## todo/fix - check for ground name in use???
    if match['stadium']
      print " @ #{match['stadium']['name']}, #{match['city']}"
    end

    print "\n"


    if match['goals1'] && match['goals2']
      print "                     ["
      match['goals1'].each_with_index do |goal,i|
        print " "    if i > 0
        print "#{goal['name']}"
        print " #{goal['minute']}"
        print "+#{goal['offset']}"   if goal['offset']
        print "'"
        print " (o.g.)"    if goal['owngoal']
        print " (pen.)"    if goal['penalty']
      end
      match['goals2'].each_with_index do |goal,i|
        if i == 0
          print "; "
        else
         print " "
        end
        print "#{goal['name']}"
        print " #{goal['minute']}"
        print "+#{goal['offset']}"  if goal['offset']
        print "'"
        print " (o.g.)"  if goal['owngoal']
        print " (pen.)"  if goal['penalty']
      end
      print "]\n"
    end
  end
end

.rootObject



9
10
11
# File 'lib/footty/version.rb', line 9

def self.root
  File.expand_path( File.dirname(File.dirname(File.dirname(__FILE__))) )
end

.week_tue_to_mon(today = Date.today) ⇒ Object



27
28
29
30
31
32
33
34
# File 'lib/footty.rb', line 27

def self.week_tue_to_mon( today=Date.today )
  ## Calculate the start of the (sport) week (tuesday)
  ##  note - wday starts counting sunday (0), monday (1), etc.
  week_tue = today - (today.wday - 2) % 7
  week_mon = week_tue + 6

  [week_tue,week_mon]
end