fbtxt2json 0.3.0 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5ef1887740ca10a869bb772542efbd1c0f96bfdfd17368e425c104f6653c497c
4
- data.tar.gz: 45d1fb3ec650e499c9c0f8318dc56c942d92806d76e4a534e0ee0d541da20f5b
3
+ metadata.gz: 7aec08e7f51705f370e3294a721ca14985cc5f8a762dbdbafa77bdab09059417
4
+ data.tar.gz: d3f306984b9649b928a132d4d5261ca6c86772a9f062161f5e4612ee6b6cd0d8
5
5
  SHA512:
6
- metadata.gz: af4f21b7f278fced209a80fac3c043868c64c82b5c1246da38bb56774ff34f4df06404a661ef0c449e0a62eca02dcd4e20f64826cab3011530761078f18db594
7
- data.tar.gz: c1dc3ea735f2af33794c708be36bf9c6bb6ff37ae2d8cc218c61005fc923ed1775a7f7c2955c4755bc24b364020bf69924a53cacfafb6af6fa30c20b3951cd45
6
+ metadata.gz: 063b7d90237901bb9db1ee517c11ebfb2cac50085e24a910e25932a298ab902aafc3136bd2437bf5d57d2839c4d8f6fc9cac1de7451d0f2aca99800321e64260
7
+ data.tar.gz: f1c58eb56adbc74689aabd6fee371513a61f3d7dcd965f4fed13d7f6d90947004878719693e0c5a17118d496a16d059ad3ae1c28c3064f9d6f9fdc391756aaf4
data/CHANGELOG.md CHANGED
@@ -1,4 +1,4 @@
1
- ### 0.3.0
1
+ ### 0.4.0
2
2
  ### 0.0.1 / 2024-09-28
3
3
 
4
4
  * Everything is new. First release.
data/Manifest.txt CHANGED
@@ -4,3 +4,6 @@ README.md
4
4
  Rakefile
5
5
  bin/fbtxt2csv
6
6
  bin/fbtxt2json
7
+ lib/fbtxt2json.rb
8
+ lib/fbtxt2json/fbtxt2csv.rb
9
+ lib/fbtxt2json/fbtxt2json.rb
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # fbtxt2json - convert football.txt match schedules & more to json
1
+ # fbtxt2json (& fbtxt2csv) - convert football.txt match schedules & more to (structured) json or (tabular) csv
2
2
 
3
3
 
4
4
  * home :: [github.com/sportdb/footty](https://github.com/sportdb/footty)
@@ -7,6 +7,7 @@
7
7
  * rdoc :: [rubydoc.info/gems/fbtxt2json](http://rubydoc.info/gems/fbtxt2json)
8
8
 
9
9
 
10
+
10
11
  ## Step 0 - Installation Via Gems
11
12
 
12
13
  To install the command-line tool via gems (ruby's package manager) use:
@@ -27,21 +28,27 @@ $ fbtxt2json -h
27
28
  resulting in:
28
29
 
29
30
  ```
30
- Usage: fbtxt2json [options] PATH
31
+ Usage: fbtxt2json [options] DATAFILES or DIRS
31
32
  --verbose, --debug turn on verbose / debug output (default: false)
32
33
  -o, --output PATH output to file / dir
34
+ --seasons SEASONS turn on processing only seasons (default: false)
33
35
  ```
34
36
 
35
37
 
36
- Note - the football.txt to .json converter works in two modes. (1) you can pass in one or more files to concat(enate) into one .json output or (2) you can pass in one or more directories to convert all files (automagically) one-by-one.
38
+ Note - the football.txt to .json converter works in two modes.
39
+
40
+ (i) you can pass in one or more (match data) files to concat(enate) into one .json output or <br>
41
+ (ii) you can pass in one or more directories to convert all (match data) files (automagically) one-by-one.
42
+
37
43
 
38
44
 
39
45
 
40
- ### Concat(enate) one or more files into one .json output
46
+ ### Concat(enate) one or more (match data) files into one .json output
41
47
 
42
48
  Let's try to convert the "Euro" European Championship 2024
43
49
  in the Football.TXT format (see [`euro/2024--germany/euro.txt`](https://github.com/openfootball/euro/blob/master/2024--germany/euro.txt)) to JSON:
44
50
 
51
+
45
52
  ```
46
53
  $ fbtxt2json euro/2024--germany/euro.txt
47
54
  ```
@@ -53,7 +60,6 @@ resulting in:
53
60
  "name": "Euro 2024",
54
61
  "matches": [
55
62
  {
56
- "num": 1,
57
63
  "round": "Matchday 1",
58
64
  "date": "2024-06-14", "time": "21:00",
59
65
  "team1": "Germany",
@@ -62,7 +68,6 @@ resulting in:
62
68
  "group": "Group A"
63
69
  },
64
70
  {
65
- "num": 2,
66
71
  "round": "Matchday 1",
67
72
  "date": "2024-06-15", "time": "15:00",
68
73
  "team1": "Hungary",
@@ -145,9 +150,11 @@ england/
145
150
  ...
146
151
  ```
147
152
 
148
- Note - by default all `.txt` file extensions get changed to `.json`.
153
+
154
+ Note - by default all `.txt` file extensions get changed to `.json` (if the include a season in the basename or the dirname).
149
155
  To use a different output directory use the `-o/--output` option. Example:
150
156
 
157
+
151
158
  ```
152
159
  $ fbtxt2json england -o ./o
153
160
  ```
@@ -174,6 +181,86 @@ That's it.
174
181
 
175
182
 
176
183
 
184
+
185
+ ## Bonus - fbtxt2csv - convert football.txt (match data) files to the (tabular) comma-separated values (.csv) format
186
+
187
+
188
+ Try in your shell / terminal:
189
+
190
+ ```
191
+ $ fbtxt2csv -h
192
+ ```
193
+
194
+ resulting in:
195
+
196
+ ```
197
+ Usage: fbtxt2csv [options] DATAFILES and/or DIRS
198
+ --verbose, --debug turn on verbose / debug output (default: false)
199
+ -o, --output PATH output to file
200
+ --seasons SEASONS turn on processing only seasons (default: false)
201
+ ```
202
+
203
+ Let's try to convert the "Euro" European Championship 2024
204
+ in the Football.TXT format (see [`euro/2024--germany/euro.txt`](https://github.com/openfootball/euro/blob/master/2024--germany/euro.txt)) to CSV:
205
+
206
+
207
+ ```
208
+ $ fbtxt2csv euro/2024--germany/euro.txt -o euro2024.csv
209
+ ```
210
+
211
+ resulting in:
212
+
213
+ ``` csv
214
+ League,Date,Time,Team 1,Team 2,Score,HT,FT,ET,P,Round,Ground
215
+ Euro 2024,2024-06-14,21:00,Germany,Scotland,,3-0,5-1,,,"Group A, Matchday 1",München
216
+ Euro 2024,2024-06-15,15:00,Hungary,Switzerland,,0-2,1-3,,,"Group A, Matchday 1",Köln
217
+ Euro 2024,2024-06-19,18:00,Germany,Hungary,,1-0,2-0,,,"Group A, Matchday 2",Stuttgart
218
+ Euro 2024,2024-06-19,21:00,Scotland,Switzerland,,1-1,1-1,,,"Group A, Matchday 2",Köln
219
+ Euro 2024,2024-06-23,21:00,Switzerland,Germany,,1-0,1-1,,,"Group A, Matchday 3",Frankfurt
220
+ Euro 2024,2024-06-23,21:00,Scotland,Hungary,,0-0,0-1,,,"Group A, Matchday 3",Stuttgart
221
+ Euro 2024,2024-06-15,18:00,Spain,Croatia,,3-0,3-0,,,"Group B, Matchday 1",Berlin
222
+ Euro 2024,2024-06-15,21:00,Italy,Albania,,2-1,2-1,,,"Group B, Matchday 1",Dortmund
223
+ Euro 2024,2024-06-19,15:00,Croatia,Albania,,0-1,2-2,,,"Group B, Matchday 2",Hamburg
224
+ Euro 2024,2024-06-20,21:00,Spain,Italy,,0-0,1-0,,,"Group B, Matchday 2",Gelsenkirchen
225
+ Euro 2024,2024-06-24,21:00,Albania,Spain,,0-1,0-1,,,"Group B, Matchday 3",Düsseldorf
226
+ Euro 2024,2024-06-24,21:00,Croatia,Italy,,0-0,1-1,,,"Group B, Matchday 3",Leipzig
227
+ ...
228
+ ```
229
+
230
+ or pass in the directory and get all (match data) files rolled-into-one tabular .csv file.
231
+ Try:
232
+
233
+ ```
234
+ $ fbtxt2csv euro -o euro.csv
235
+ ```
236
+
237
+ resulting in:
238
+
239
+ ``` csv
240
+ League,Date,Time,Team 1,Team 2,Score,HT,FT,ET,P,Round,Ground
241
+ Euro 1960,1960-07-06,20:00,France,Yugoslavia,4-5,,,,,Semi-finals,"Parc des Princes, Paris"
242
+ Euro 1960,1960-07-06,20:30,Czechoslovakia,Soviet Union,0-3,,,,,Semi-finals,"Stade Vélodrome, Marseille"
243
+ Euro 1960,1960-07-09,21:30,Czechoslovakia,France,2-0,,,,,Third place play-off,"Stade Vélodrome, Marseille"
244
+ Euro 1960,1960-07-10,20:30,Soviet Union,Yugoslavia,,,,2-1,,Final,"Parc des Princes, Paris"
245
+ Euro 1964,1964-06-17,20:00,Spain,Hungary,,,,2-1,,Semi-finals,"Santiago Bernabéu, Madrid"
246
+ Euro 1964,1964-06-17,22:30,Denmark,Soviet Union,0-3,,,,,Semi-finals,"Camp Nou, Barcelona"
247
+ Euro 1964,1964-06-20,20:00,Hungary,Denmark,,,,3-1,,Third place play-off,"Camp Nou, Barcelona"
248
+ Euro 1964,1964-06-21,18:30,Spain,Soviet Union,2-1,,,,,Final,"Santiago Bernabéu, Madrid"
249
+ Euro 1968,1968-06-05,18:00,Italy,Soviet Union,,,,0-0,,Semi-finals,"Stadio San Paolo, Naples"
250
+ Euro 1968,1968-06-05,21:15,Yugoslavia,England,1-0,,,,,Semi-finals,"Stadio Comunale, Florence"
251
+ Euro 1968,1968-06-08,15:00,England,Soviet Union,2-0,,,,,Third place play-off,"Stadio Olimpico, Rome"
252
+ Euro 1968,1968-06-08,21:15,Italy,Yugoslavia,,,,1-1,,Final,"Stadio Olimpico, Rome"
253
+ Euro 1968,1968-06-10,21:15,Italy,Yugoslavia,2-0,,,,,"Final, Replay","Stadio Olimpico, Rome"
254
+ Euro 1972,1972-06-14,20:00,West Germany,Belgium,2-1,,,,,Semi-finals,"Antwerpen, Bosuil"
255
+ Euro 1972,1972-06-14,20:00,Soviet Union,Hungary,1-0,,,,,Semi-finals,"Brussel, Astridpark"
256
+ Euro 1972,1972-06-17,20:00,Belgium,Hungary,2-1,,,,,Third place play-off,"Liège, Stade Sclessin"
257
+ Euro 1972,1972-06-18,16:00,West Germany,Soviet Union,3-0,,,,,Final,"Bruxelles, Stade Heysel"
258
+ ...
259
+ ```
260
+
261
+
262
+
263
+
177
264
  ## Questions? Comments?
178
265
 
179
266
  Yes, you can. More than welcome.
data/Rakefile CHANGED
@@ -2,9 +2,9 @@ require 'hoe'
2
2
 
3
3
 
4
4
  Hoe.spec 'fbtxt2json' do
5
- self.version = '0.3.0'
5
+ self.version = '0.4.0'
6
6
 
7
- self.summary = "fbtxt2json - convert football.txt match schedules & more to json"
7
+ self.summary = "fbtxt2json (& fbtxt2csv) - convert football.txt match schedules & more to (structured) json or (tabular) csv"
8
8
  self.description = summary
9
9
 
10
10
  self.urls = { home: 'https://github.com/sportdb/footty' }
@@ -18,10 +18,12 @@ Hoe.spec 'fbtxt2json' do
18
18
 
19
19
  self.licenses = ['Public Domain']
20
20
 
21
+
21
22
  self.extra_deps = [
22
- ['sportdb-quick', '>= 0.2.1'],
23
+ ['sportdb-quick', '>= 0.7.0'],
24
+ ['sportdb-parser', '>= 0.7.1'],
23
25
  ### note - include fbtok gem for (shared) command-line helpers/machinery!!!
24
- ['fbtok', '>= 0.3.3'],
26
+ ['fbtok', '>= 0.4.1'],
25
27
  ]
26
28
 
27
29
  self.spec_extras = {
data/bin/fbtxt2csv CHANGED
@@ -1,244 +1,10 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- ###
4
- # todo/check - move fbtxt2csv to its own top-level gem
5
- ## or add to ___ - why? why not?
6
-
7
-
8
3
  ## tip: to test run:
9
4
  ## ruby -I ./lib bin/fbtxt2csv
10
5
 
11
6
 
12
- ## our own code
13
- require 'sportdb/quick'
14
-
15
- require 'fbtok' ### check if requires sportdb/quick (no need to duplicate)
16
-
17
-
18
-
19
- require 'optparse'
20
-
21
-
22
-
23
- args = ARGV
24
- opts = { debug: false,
25
- output: nil,
26
- seasons: [],
27
- }
28
-
29
- parser = OptionParser.new do |parser|
30
- parser.banner = "Usage: #{$PROGRAM_NAME} [options] PATH"
31
-
32
- ##
33
- ## check if git has a offline option?? (use same)
34
- ## check for other tools - why? why not?
35
- # parser.on( "-q", "--quiet",
36
- # "less debug output/messages - default is (#{!opts[:debug]})" ) do |debug|
37
- # opts[:debug] = false
38
- # end
39
-
40
- parser.on( "--verbose", "--debug",
41
- "turn on verbose / debug output (default: #{opts[:debug]})" ) do |debug|
42
- opts[:debug] = true
43
- end
44
-
45
- parser.on( "-o PATH", "--output PATH",
46
- "output to file" ) do |output|
47
- opts[:output] = output
48
- end
49
-
50
- parser.on( "--seasons SEASONS",
51
- "turn on processing only seasons (default: #{!opts[:seasons].empty?})" ) do |seasons|
52
- pp seasons
53
- seasons = seasons.split( /[, ]/ )
54
- seasons = seasons.map {|season| Season.parse(season) }
55
- opts[:seasons] = seasons
56
- end
57
- end
58
- parser.parse!( args )
59
-
60
-
61
- puts "OPTS:"
62
- p opts
63
- puts "ARGV:"
64
- p args
65
-
66
-
67
-
68
- paths = if args.empty?
69
- ['/sports/openfootball/euro/2021--europe/euro.txt']
70
- else
71
- args
72
- end
73
-
74
-
75
- if opts[:debug]
76
- SportDb::QuickMatchReader.debug = true
77
- SportDb::MatchParser.debug = true
78
- else
79
- SportDb::QuickMatchReader.debug = false
80
- SportDb::MatchParser.debug = false
81
- LogUtils::Logger.root.level = :info
82
- end
83
-
84
-
85
-
86
-
87
- MAX_HEADERS = [
88
- 'League',
89
- 'Date',
90
- 'Time',
91
- 'Team 1',
92
- 'Team 2',
93
- 'HT',
94
- 'FT',
95
- 'ET',
96
- 'P',
97
- 'Round',
98
- 'Status',
99
- ]
100
-
101
- MIN_HEADERS = [ ## always keep even if all empty
102
- 'League',
103
- 'Date',
104
- 'Team 1',
105
- 'FT',
106
- 'Team 2'
107
- ]
108
-
109
-
110
-
111
- def vacuum( rows, headers: MAX_HEADERS, fixed_headers: MIN_HEADERS )
112
- ## check for unused columns and strip/remove
113
- counter = Array.new( MAX_HEADERS.size, 0 )
114
- rows.each do |row|
115
- row.each_with_index do |col, idx|
116
- counter[idx] += 1 unless col.nil? || col.empty?
117
- end
118
- end
119
-
120
- ## pp counter
121
-
122
- ## check empty columns
123
- headers = []
124
- indices = []
125
- empty_headers = []
126
- empty_indices = []
127
-
128
- counter.each_with_index do |num, idx|
129
- header = MAX_HEADERS[ idx ]
130
- if num > 0 || (num == 0 && fixed_headers.include?( header ))
131
- headers << header
132
- indices << idx
133
- else
134
- empty_headers << header
135
- empty_indices << idx
136
- end
137
- end
138
-
139
- if empty_indices.size > 0
140
- rows = rows.map do |row|
141
- row_vacuumed = []
142
- row.each_with_index do |col, idx|
143
- ## todo/fix: use values or such??
144
- row_vacuumed << col unless empty_indices.include?( idx )
145
- end
146
- row_vacuumed
147
- end
148
- end
149
-
150
- [rows, headers]
151
- end
152
-
153
-
154
-
155
- def parse( txt ) ### check - name parse_txt or txt_to_csv or such - why? why not?
156
- quick = SportDb::QuickMatchReader.new( txt )
157
- matches = quick.parse
158
- name = quick.league_name ## quick hack - get league+season via league_name
159
-
160
- recs = []
161
-
162
- matches.each do |match|
163
-
164
- ## for separator use comma (,) or pipe (|) or ???
165
- round = String.new
166
- ## round << "#{match.stage} | " if match.stage
167
- ## round << "#{match.group} | " if match.group
168
- round << "#{match.stage}, " if match.stage
169
- round << "#{match.group}, " if match.group
170
- round << match.round
171
-
172
- rec = [
173
- #############################
174
- ## todo/fix - split league into league_name and season!!!!
175
- ###############################
176
- name, ## league name
177
- match.date ? match.date : '',
178
- match.time ? match.time : '',
179
- match.team1,
180
- match.team2,
181
- (match.score1i && match.score2i) ? "#{match.score1i}-#{match.score2i}" : '',
182
- (match.score1 && match.score2) ? "#{match.score1}-#{match.score2}" : '',
183
- (match.score1et && match.score2et) ? "#{match.score1et}-#{match.score2et}" : '',
184
- (match.score1p && match.score2p) ? "#{match.score1p}-#{match.score2p}" : '',
185
- round,
186
- match.status ? match.status : '',
187
- ]
188
-
189
- ## add more attributes e.g. ground, etc.
190
-
191
- recs << rec
192
- end
193
-
194
- puts " #{recs.size} record(s)"
195
-
196
- if quick.errors?
197
- puts "!! #{quick.errors.size} parse error(s):"
198
- pp quick.errors
199
- exit 1
200
- end
201
-
202
- recs
203
- end
204
-
205
-
206
- recs = []
207
-
208
- paths.each do |path|
209
- if Dir.exist?( path )
210
- puts "==> reading dir >#{path}<..."
211
-
212
- datafiles = SportDb::Parser::Opts._find( path, seasons: opts[:seasons] )
213
- pp datafiles
214
- puts " #{datafiles.size} datafile(s)"
215
- datafiles.each_with_index do |datafile,j|
216
- puts " reading file [#{j+1}/#{datafiles.size}] >#{datafile}<..."
217
- txt = read_text( datafile )
218
- recs += parse( txt )
219
- end
220
- elsif File.exist?( path )
221
- puts "==> reading file >#{path}<..."
222
- txt = read_text( path )
223
- recs += parse( txt )
224
- else ## not a file or dir repprt errr
225
- raise ArgumentError, "file/dir does NOT exist - #{path}"
226
- end
227
- end
228
-
229
-
230
- recs, headers = vacuum( recs )
231
- pp recs[0,10] ## dump first 10 records
232
- pp headers
233
- puts " #{recs.size} record(s)"
234
-
235
-
236
- if opts[:output]
237
- puts "==> writing matches to #{opts[:output]}"
238
- write_csv( opts[:output], recs,
239
- headers: headers )
240
- end
241
-
7
+ require 'fbtxt2json'
242
8
 
243
- puts "bye"
244
9
 
10
+ Fbtxt2csv.main
data/bin/fbtxt2json CHANGED
@@ -1,219 +1,10 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  ## tip: to test run:
4
- ## ruby -I ./lib bin/fbtxt2json
4
+ ## $ ruby -I ./lib bin/fbtxt2json
5
5
 
6
6
 
7
- #####
8
- ## todo
9
- ## add option for [no]-halt-on-error (default: false)
10
- ## or use a different (shorter) name e.g. --resume?
7
+ require 'fbtxt2json'
11
8
 
12
9
 
13
-
14
- ## our own code
15
- require 'sportdb/quick'
16
-
17
- require 'fbtok' ### check if requires sportdb/quick (no need to duplicate)
18
-
19
-
20
-
21
- require 'optparse'
22
-
23
-
24
-
25
- args = ARGV
26
- opts = { debug: false,
27
- output: nil,
28
- summary: false,
29
- seasons: [],
30
- }
31
-
32
- parser = OptionParser.new do |parser|
33
- parser.banner = "Usage: #{$PROGRAM_NAME} [options] PATH"
34
-
35
- ##
36
- ## check if git has a offline option?? (use same)
37
- ## check for other tools - why? why not?
38
- # parser.on( "-q", "--quiet",
39
- # "less debug output/messages - default is (#{!opts[:debug]})" ) do |debug|
40
- # opts[:debug] = false
41
- # end
42
-
43
- parser.on( "--verbose", "--debug",
44
- "turn on verbose / debug output (default: #{opts[:debug]})" ) do |debug|
45
- opts[:debug] = true
46
- end
47
-
48
- parser.on( "-o PATH", "--output PATH",
49
- "output to file / dir" ) do |output|
50
- opts[:output] = output
51
- end
52
-
53
- parser.on( "--summary",
54
- "(auto-)generate summary (index.html) page (default: #{opts[:summary]})" ) do |summary|
55
- opts[:summary] = summary
56
- end
57
-
58
- parser.on( "--seasons SEASONS",
59
- "turn on processing only seasons (default: #{!opts[:seasons].empty?})" ) do |seasons|
60
- pp seasons
61
- seasons = seasons.split( /[, ]/ )
62
- seasons = seasons.map {|season| Season.parse(season) }
63
- opts[:seasons] = seasons
64
- end
65
- end
66
- parser.parse!( args )
67
-
68
-
69
- puts "OPTS:"
70
- p opts
71
- puts "ARGV:"
72
- p args
73
-
74
-
75
-
76
- paths = if args.empty?
77
- ['/sports/openfootball/euro/2021--europe/euro.txt']
78
- else
79
- args
80
- end
81
-
82
-
83
- if opts[:debug]
84
- SportDb::QuickMatchReader.debug = true
85
- SportDb::MatchParser.debug = true
86
- else
87
- SportDb::QuickMatchReader.debug = false
88
- SportDb::MatchParser.debug = false
89
- LogUtils::Logger.root.level = :info
90
- end
91
-
92
-
93
- ###
94
- ## two modes - process directories or concat(enate)d files
95
- ##
96
- ## check if args is a directory
97
- ##
98
- dirs = 0
99
- files = 0
100
- paths.each do |path|
101
- if Dir.exist?( path )
102
- dirs += 1
103
- elsif File.exist?( path )
104
- files += 1
105
- else ## not a file or dir repprt errr
106
- raise ArgumentError, "file/dir does NOT exist - #{path}"
107
- end
108
- end
109
-
110
- if dirs > 0 && files > 0
111
- raise ArgumentError, "#{files} file(s), #{dirs} dir(s) - can only process dirs or files but NOT both; sorry"
112
- end
113
-
114
-
115
-
116
- def parse( txt,
117
- summary: nil,
118
- dump: false ) ### check - name parse_txt or txt_to_json or such - why? why not?
119
- quick = SportDb::QuickMatchReader.new( txt )
120
- matches = quick.parse
121
- name = quick.league_name ## quick hack - get league+season via league_name
122
-
123
- data = { 'name' => name,
124
- 'matches' => matches.map {|match| match.as_json }}
125
-
126
- if dump
127
- pp data
128
- puts
129
- end
130
- puts " #{matches.size} match(es)"
131
-
132
- if quick.errors?
133
- puts "!! #{quick.errors.size} parse error(s):"
134
- pp quick.errors
135
- exit 1
136
- end
137
-
138
-
139
- if summary
140
- ## add stats to summary page
141
- summary << "- #{name} | #{matches.size} match(es)\n"
142
- end
143
-
144
- data
145
- end
146
-
147
-
148
- if files > 0
149
- ## step 1 - concat(enate) all text files into one
150
- txt = String.new
151
- paths.each_with_index do |path,i|
152
- puts "==> reading file [#{i+1}/#{paths.size}] >#{path}<..."
153
- txt += "\n\n" if i > 0
154
- txt += read_text( path )
155
- end
156
-
157
- ## step 2 - parse (matches) in the football.txt format
158
- data = parse( txt, dump: true )
159
-
160
- if opts[:output]
161
- puts "==> writing matches to #{opts[:output]}"
162
- write_json( opts[:output], data )
163
- end
164
- elsif dirs > 0
165
-
166
- ## use a html pre(formatted) text
167
- summary = String.new
168
- summary << "<pre>\n"
169
- summary << "run on #{Time.now.to_s}\n\n" ## add version and such - why? why not?
170
-
171
- paths.each_with_index do |path,i|
172
- puts "==> reading dir [#{i+1}/#{paths.size}] >#{path}<..."
173
- summary << "==> [#{i+1}/#{paths.size}] dir #{path}\n"
174
-
175
-
176
- datafiles = SportDb::Parser::Opts._find( path, seasons: opts[:seasons] )
177
- pp datafiles
178
- puts " #{datafiles.size} datafile(s)"
179
- summary << " #{datafiles.size} datafile(s)\n\n"
180
-
181
- datafiles.each do |datafile|
182
- txt = read_text( datafile )
183
- data = parse( txt, summary: summary )
184
-
185
- if opts[:output]
186
- ### norm - File.expand_path !!!
187
- ## note - use '.' to use (relative to) local directory !!!
188
- reldir = File.expand_path(File.dirname( path )) ## keep last dir (in relative name)
189
- relpath = datafile.sub( reldir+'/', '' )
190
- dir = File.dirname( relpath )
191
- ## puts " reldir = #{reldir}, datafile = #{datafile}"
192
- ## puts " relpath = #{relpath}, dir = #{dir}"
193
- basename = File.basename( relpath, File.extname(relpath))
194
- outpath = "#{opts[:output]}/#{dir}/#{basename}.json"
195
- else
196
- dir = File.dirname( datafile )
197
- basename = File.basename( datafile, File.extname(datafile) )
198
- outpath = "#{dir}/#{basename}.json"
199
- end
200
- puts " writing matches to #{outpath}"
201
- write_json( outpath, data )
202
- end
203
- end
204
-
205
- summary << "</pre>"
206
- puts summary
207
- ## note - do NOT write-out summary if seasons filter is used!!!
208
- if opts[:summary] && opts[:seasons].size == 0
209
- write_text( "#{opts[:output]}/index.html", summary )
210
- end
211
- else
212
- ## do nothing; no args
213
- end
214
-
215
-
216
-
217
-
218
- puts "bye"
219
-
10
+ Fbtxt2json.main
@@ -0,0 +1,250 @@
1
+
2
+ module Fbtxt2csv
3
+
4
+
5
+ def self.main( args=ARGV )
6
+
7
+
8
+
9
+ opts = { debug: false,
10
+ output: nil,
11
+ seasons: [],
12
+ }
13
+
14
+ parser = OptionParser.new do |parser|
15
+ parser.banner = "Usage: #{$PROGRAM_NAME} [options] DATAFILES and/or DIRS"
16
+
17
+ ##
18
+ ## check if git has a offline option?? (use same)
19
+ ## check for other tools - why? why not?
20
+ # parser.on( "-q", "--quiet",
21
+ # "less debug output/messages - default is (#{!opts[:debug]})" ) do |debug|
22
+ # opts[:debug] = false
23
+ # end
24
+
25
+ parser.on( "--verbose", "--debug",
26
+ "turn on verbose / debug output (default: #{opts[:debug]})" ) do |debug|
27
+ opts[:debug] = true
28
+ end
29
+
30
+ parser.on( "-o PATH", "--output PATH",
31
+ "output to file" ) do |output|
32
+ opts[:output] = output
33
+ end
34
+
35
+ parser.on( "--seasons SEASONS",
36
+ "turn on processing only seasons (default: #{!opts[:seasons].empty?})" ) do |seasons|
37
+ pp seasons
38
+ seasons = seasons.split( /[, ]/ )
39
+ seasons = seasons.map {|season| Season.parse(season) }
40
+ opts[:seasons] = seasons
41
+ end
42
+ end
43
+ parser.parse!( args )
44
+
45
+
46
+ puts "OPTS:"
47
+ p opts
48
+ puts "ARGV:"
49
+ p args
50
+
51
+
52
+
53
+ paths = if args.empty?
54
+ ['/sports/openfootball/euro/2021--europe/euro.txt']
55
+ else
56
+ args
57
+ end
58
+
59
+
60
+ if opts[:debug]
61
+ SportDb::QuickMatchReader.debug = true
62
+ SportDb::MatchParser.debug = true
63
+ else
64
+ SportDb::QuickMatchReader.debug = false
65
+ SportDb::MatchParser.debug = false
66
+ LogUtils::Logger.root.level = :info
67
+ end
68
+
69
+
70
+
71
+ recs = []
72
+
73
+ paths.each do |path|
74
+ if Dir.exist?( path )
75
+ puts "==> reading dir >#{path}<..."
76
+
77
+ datafiles = SportDb::Pathspec._find( path, seasons: opts[:seasons] )
78
+ pp datafiles
79
+ puts " #{datafiles.size} datafile(s)"
80
+ datafiles.each_with_index do |datafile,j|
81
+ puts " reading file [#{j+1}/#{datafiles.size}] >#{datafile}<..."
82
+ txt = read_text( datafile )
83
+ recs += parse( txt )
84
+ end
85
+ elsif File.file?( path ) ## note - File.exist? also incl. Dir - use anyway - why? why not?
86
+ puts "==> reading file >#{path}<..."
87
+ txt = read_text( path )
88
+ recs += parse( txt )
89
+ else ## not a file or dir report error
90
+ raise ArgumentError, "file/dir does NOT exist - #{path}"
91
+ end
92
+ end
93
+
94
+
95
+ recs, headers = vacuum( recs )
96
+ pp recs[0,2] ## dump first 2 records
97
+ pp headers
98
+ puts " #{recs.size} record(s)"
99
+
100
+
101
+ if opts[:output]
102
+ puts "==> writing matches to #{opts[:output]}"
103
+ write_csv( opts[:output], recs,
104
+ headers: headers )
105
+ end
106
+
107
+
108
+ puts "bye"
109
+ end # self.main
110
+
111
+
112
+
113
+
114
+ MAX_HEADERS = [
115
+ 'League',
116
+ 'Date',
117
+ 'Time',
118
+ 'Team 1',
119
+ 'Team 2',
120
+ 'Score', ## generic score - do NOT know if FT/ET or such
121
+ 'HT',
122
+ 'FT',
123
+ 'ET',
124
+ 'P',
125
+ 'Round',
126
+ 'Status',
127
+ 'Ground',
128
+ ]
129
+
130
+
131
+ MIN_HEADERS = [ ## always keep even if all empty
132
+ 'League',
133
+ 'Date',
134
+ 'Team 1',
135
+ 'Team 2'
136
+ ]
137
+
138
+
139
+
140
+ def self.vacuum( rows, headers: MAX_HEADERS, fixed_headers: MIN_HEADERS )
141
+ ## check for unused columns and strip/remove
142
+ counter = Array.new( MAX_HEADERS.size, 0 )
143
+ rows.each do |row|
144
+ row.each_with_index do |col, idx|
145
+ counter[idx] += 1 unless col.nil? || col.empty?
146
+ end
147
+ end
148
+
149
+ ## pp counter
150
+
151
+ ## check empty columns
152
+ headers = []
153
+ indices = []
154
+ empty_headers = []
155
+ empty_indices = []
156
+
157
+ counter.each_with_index do |num, idx|
158
+ header = MAX_HEADERS[ idx ]
159
+ if num > 0 || (num == 0 && fixed_headers.include?( header ))
160
+ headers << header
161
+ indices << idx
162
+ else
163
+ empty_headers << header
164
+ empty_indices << idx
165
+ end
166
+ end
167
+
168
+ if empty_indices.size > 0
169
+ rows = rows.map do |row|
170
+ row_vacuumed = []
171
+ row.each_with_index do |col, idx|
172
+ ## todo/fix: use values or such??
173
+ row_vacuumed << col unless empty_indices.include?( idx )
174
+ end
175
+ row_vacuumed
176
+ end
177
+ end
178
+
179
+ [rows, headers]
180
+ end
181
+
182
+
183
+
184
+ def self.parse( txt ) ### check - name parse_txt or txt_to_csv or such - why? why not?
185
+ quick = SportDb::QuickMatchReader.new( txt )
186
+ matches = quick.parse
187
+ name = quick.league_name ## quick hack - get league+season via league_name
188
+
189
+ recs = []
190
+
191
+
192
+ matches.each do |match|
193
+ ## pp match
194
+ ## pp match.status
195
+ ## pp match.round
196
+ ## pp match.score
197
+ ## pp match.score
198
+
199
+ round = String.new
200
+ round << "#{match.group}, " if match.group
201
+ round << match.round if match.round
202
+
203
+ ## note - make.score hash uses symbols!!!
204
+ ## e.g. score[:ht] and NOT score['ht'] !!!
205
+ # make sure hash keys are always strings
206
+ score = match.score
207
+ score = score.transform_keys(&:to_s) if score.is_a?( Hash )
208
+
209
+ ground = if match.ground.is_a?( Array )
210
+ match.ground.join(', ')
211
+ else ## assume string or nil
212
+ match.ground ? match.ground : ''
213
+ end
214
+
215
+ rec = [
216
+ #############################
217
+ ## todo/fix - split league into league_name and season!!!!
218
+ ###############################
219
+ name, ## league name
220
+ match.date ? match.date : '',
221
+ match.time ? match.time : '',
222
+ match.team1,
223
+ match.team2,
224
+ score.is_a?( Array ) && score.size == 2 ? "#{score[0]}-#{score[1]}" : '',
225
+ score.is_a?( Hash ) && score['ht'] ? "#{score['ht'][0]}-#{score['ht'][1]}" : '',
226
+ score.is_a?( Hash ) && score['ft'] ? "#{score['ft'][0]}-#{score['ft'][1]}" : '',
227
+ score.is_a?( Hash ) && score['et'] ? "#{score['et'][0]}-#{score['et'][1]}" : '',
228
+ score.is_a?( Hash ) && score['p'] ? "#{score['p'][0]}-#{score['p'][1]}" : '',
229
+ round,
230
+ match.status ? match.status : '',
231
+ ground,
232
+ ]
233
+
234
+ ## add more attributes e.g. ground, etc.
235
+
236
+ recs << rec
237
+ end
238
+
239
+ puts " #{recs.size} record(s)"
240
+
241
+ if quick.errors?
242
+ puts "!! #{quick.errors.size} parse error(s):"
243
+ pp quick.errors
244
+ exit 1
245
+ end
246
+
247
+ recs
248
+ end # self.parse
249
+
250
+ end # module Fbtxt2csv
@@ -0,0 +1,229 @@
1
+ #####
2
+ ## todo
3
+ ## add option for [no]-halt-on-error (default: false)
4
+ ## or use a different (shorter) name e.g. --resume?
5
+
6
+
7
+ module Fbtxt2json
8
+
9
+
10
+ def self.main( args=ARGV )
11
+
12
+ opts = { debug: false,
13
+ output: nil,
14
+ summary: false,
15
+ seasons: [],
16
+ }
17
+
18
+
19
+ parser = OptionParser.new do |parser|
20
+ parser.banner = "Usage: #{$PROGRAM_NAME} [options] DATAFILES or DIRS"
21
+
22
+ ##
23
+ ## check if git has a offline option?? (use same)
24
+ ## check for other tools - why? why not?
25
+ # parser.on( "-q", "--quiet",
26
+ # "less debug output/messages - default is (#{!opts[:debug]})" ) do |debug|
27
+ # opts[:debug] = false
28
+ # end
29
+
30
+ parser.on( "--verbose", "--debug",
31
+ "turn on verbose / debug output (default: #{opts[:debug]})" ) do |debug|
32
+ opts[:debug] = true
33
+ end
34
+
35
+ parser.on( "-o PATH", "--output PATH",
36
+ "output to file / dir" ) do |output|
37
+ opts[:output] = output
38
+ end
39
+
40
+ parser.on( "--summary",
41
+ "(auto-)generate summary (index.html) page (default: #{opts[:summary]})" ) do |summary|
42
+ opts[:summary] = summary
43
+ end
44
+
45
+ parser.on( "--seasons SEASONS",
46
+ "turn on processing only seasons (default: #{!opts[:seasons].empty?})" ) do |seasons|
47
+ pp seasons
48
+ seasons = seasons.split( /[, ]/ )
49
+ seasons = seasons.map {|season| Season.parse(season) }
50
+ opts[:seasons] = seasons
51
+ end
52
+ end
53
+ parser.parse!( args )
54
+
55
+
56
+ puts "OPTS:"
57
+ p opts
58
+ puts "ARGV:"
59
+ p args
60
+
61
+
62
+
63
+ paths = if args.empty?
64
+ ['/sports/openfootball/euro/2021--europe/euro.txt']
65
+ else
66
+ args
67
+ end
68
+
69
+
70
+ if opts[:debug]
71
+ SportDb::QuickMatchReader.debug = true
72
+ SportDb::MatchParser.debug = true
73
+ else
74
+ SportDb::QuickMatchReader.debug = false
75
+ SportDb::MatchParser.debug = false
76
+ LogUtils::Logger.root.level = :info
77
+ end
78
+
79
+
80
+ ###
81
+ ## two modes - process directories or concat(enate)d files
82
+ ##
83
+ ## check if args is a directory
84
+ ##
85
+ dirs = 0
86
+ files = 0
87
+ paths.each do |path|
88
+ if Dir.exist?( path )
89
+ dirs += 1
90
+ elsif File.file?( path ) ## note - strictly File.exist? also checks for dirs (use File.file?)!!
91
+ files += 1
92
+ else ## not a file or dir repprt errr
93
+ raise ArgumentError, "file/dir does NOT exist - #{path}"
94
+ end
95
+ end
96
+
97
+ if dirs > 0 && files > 0
98
+ raise ArgumentError, "#{files} file(s), #{dirs} dir(s) - can only process dirs or files but NOT both; sorry"
99
+ end
100
+
101
+
102
+
103
+
104
+ if files > 0
105
+ do_files( paths, outpath: opts[:output] )
106
+ elsif dirs > 0
107
+ do_dirs( paths, outdir: opts[:output],
108
+ seasons: opts[:seasons],
109
+ write_summary: opts[:summary] )
110
+ else
111
+ ## do nothing; no args
112
+ end
113
+
114
+
115
+ puts "bye"
116
+ end # self.main
117
+
118
+
119
+
120
+ def self.do_files( paths, outpath: nil )
121
+ data = nil
122
+ paths.each_with_index do |path,i|
123
+ puts "==> reading file [#{i+1}/#{paths.size}] >#{path}<..."
124
+ txt = read_text( path )
125
+ more_data = parse( txt )
126
+
127
+ if data.nil?
128
+ data = more_data
129
+ else
130
+ ## concat matches
131
+ ## check if name match up!!!
132
+ if data['name'] != more_data['name']
133
+ raise ArgumentError, "cannot merge matchfiles - league names do NOT match: #{data['name']} != #{more_data['name']}"
134
+ else
135
+ data['matches'] += more_data['matches']
136
+ end
137
+ end
138
+ end
139
+
140
+ pp data
141
+ puts
142
+ puts " #{data['matches'].size} match(es)"
143
+
144
+ if outpath
145
+ puts "==> writing matches to #{outpath}"
146
+ write_json( outpath, data )
147
+ end
148
+ end
149
+
150
+
151
+
152
+ def self.do_dirs( paths, outdir: nil,
153
+ seasons: [],
154
+ write_summary: false )
155
+
156
+ ## use a html pre(formatted) text
157
+ summary = String.new
158
+ summary << "<pre>\n"
159
+ summary << "run on #{Time.now.to_s}\n\n" ## add version and such - why? why not?
160
+
161
+
162
+ paths.each_with_index do |path,i|
163
+ puts "==> reading dir [#{i+1}/#{paths.size}] >#{path}<..."
164
+ summary << "==> [#{i+1}/#{paths.size}] dir #{path}\n"
165
+
166
+ datafiles = SportDb::Pathspec._find( path, seasons: seasons )
167
+ pp datafiles
168
+ puts " #{datafiles.size} datafile(s)"
169
+ summary << " #{datafiles.size} datafile(s)\n\n"
170
+
171
+ datafiles.each do |datafile|
172
+ txt = read_text( datafile )
173
+ data = parse( txt )
174
+
175
+ ## add stats to summary page
176
+ summary << "- #{data['name']} | #{data['matches'].size} match(es) - #{datafile}\n"
177
+
178
+
179
+ if outdir
180
+ ### norm - File.expand_path !!!
181
+ ## note - use '.' to use (relative to) local directory !!!
182
+ reldir = File.expand_path(File.dirname( path )) ## keep last dir (in relative name)
183
+ relpath = datafile.sub( reldir+'/', '' )
184
+ dir = File.dirname( relpath )
185
+ ## puts " reldir = #{reldir}, datafile = #{datafile}"
186
+ ## puts " relpath = #{relpath}, dir = #{dir}"
187
+ basename = File.basename( relpath, File.extname(relpath))
188
+ outpath = "#{outdir}/#{dir}/#{basename}.json"
189
+ else
190
+ dir = File.dirname( datafile )
191
+ basename = File.basename( datafile, File.extname(datafile) )
192
+ outpath = "#{dir}/#{basename}.json"
193
+ end
194
+ puts " writing matches to #{outpath}"
195
+ write_json( outpath, data )
196
+ end
197
+ end
198
+
199
+ summary << "</pre>"
200
+ puts summary
201
+
202
+ ## note - do NOT write-out summary if seasons filter is used!!!
203
+ if outdir && write_summary && seasons.empty?
204
+ write_text( "#{outdir}/index.html", summary )
205
+ end
206
+ end
207
+
208
+
209
+
210
+ def self.parse( txt ) ### check - name parse_txt or txt_to_json or such - why? why not?
211
+ quick = SportDb::QuickMatchReader.new( txt )
212
+ matches = quick.parse
213
+ name = quick.league_name ## quick hack - get league+season via league_name
214
+
215
+ data = { 'name' => name,
216
+ 'matches' => matches.map {|match| match.as_json }}
217
+
218
+
219
+ if quick.errors?
220
+ puts "!! #{quick.errors.size} parse error(s):"
221
+ pp quick.errors
222
+ exit 1
223
+ end
224
+
225
+ data
226
+ end
227
+
228
+
229
+ end # module Fbtxt2json
data/lib/fbtxt2json.rb ADDED
@@ -0,0 +1,10 @@
1
+
2
+ require 'sportdb/quick'
3
+
4
+ require 'fbtok' ### check if requires sportdb/quick (no need to duplicate)
5
+
6
+
7
+
8
+ ## our own code
9
+ require_relative 'fbtxt2json/fbtxt2json'
10
+ require_relative 'fbtxt2json/fbtxt2csv'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fbtxt2json
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
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-04-08 00:00:00.000000000 Z
11
+ date: 2026-05-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sportdb-quick
@@ -16,28 +16,42 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.2.1
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.1
26
+ version: 0.7.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: sportdb-parser
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.7.1
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 0.7.1
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: fbtok
29
43
  requirement: !ruby/object:Gem::Requirement
30
44
  requirements:
31
45
  - - ">="
32
46
  - !ruby/object:Gem::Version
33
- version: 0.3.3
47
+ version: 0.4.1
34
48
  type: :runtime
35
49
  prerelease: false
36
50
  version_requirements: !ruby/object:Gem::Requirement
37
51
  requirements:
38
52
  - - ">="
39
53
  - !ruby/object:Gem::Version
40
- version: 0.3.3
54
+ version: 0.4.1
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: rdoc
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -72,7 +86,8 @@ dependencies:
72
86
  - - "~>"
73
87
  - !ruby/object:Gem::Version
74
88
  version: '4.2'
75
- description: fbtxt2json - convert football.txt match schedules & more to json
89
+ description: fbtxt2json (& fbtxt2csv) - convert football.txt match schedules & more
90
+ to (structured) json or (tabular) csv
76
91
  email: gerald.bauer@gmail.com
77
92
  executables:
78
93
  - fbtxt2csv
@@ -89,6 +104,9 @@ files:
89
104
  - Rakefile
90
105
  - bin/fbtxt2csv
91
106
  - bin/fbtxt2json
107
+ - lib/fbtxt2json.rb
108
+ - lib/fbtxt2json/fbtxt2csv.rb
109
+ - lib/fbtxt2json/fbtxt2json.rb
92
110
  homepage: https://github.com/sportdb/footty
93
111
  licenses:
94
112
  - Public Domain
@@ -113,5 +131,6 @@ requirements: []
113
131
  rubygems_version: 3.5.22
114
132
  signing_key:
115
133
  specification_version: 4
116
- summary: fbtxt2json - convert football.txt match schedules & more to json
134
+ summary: fbtxt2json (& fbtxt2csv) - convert football.txt match schedules & more to
135
+ (structured) json or (tabular) csv
117
136
  test_files: []