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 +4 -4
- data/CHANGELOG.md +1 -1
- data/Manifest.txt +3 -0
- data/README.md +94 -7
- data/Rakefile +6 -4
- data/bin/fbtxt2csv +2 -236
- data/bin/fbtxt2json +3 -212
- data/lib/fbtxt2json/fbtxt2csv.rb +250 -0
- data/lib/fbtxt2json/fbtxt2json.rb +229 -0
- data/lib/fbtxt2json.rb +10 -0
- metadata +27 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7aec08e7f51705f370e3294a721ca14985cc5f8a762dbdbafa77bdab09059417
|
|
4
|
+
data.tar.gz: d3f306984b9649b928a132d4d5261ca6c86772a9f062161f5e4612ee6b6cd0d8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 063b7d90237901bb9db1ee517c11ebfb2cac50085e24a910e25932a298ab902aafc3136bd2437bf5d57d2839c4d8f6fc9cac1de7451d0f2aca99800321e64260
|
|
7
|
+
data.tar.gz: f1c58eb56adbc74689aabd6fee371513a61f3d7dcd965f4fed13d7f6d90947004878719693e0c5a17118d496a16d059ad3ae1c28c3064f9d6f9fdc391756aaf4
|
data/CHANGELOG.md
CHANGED
data/Manifest.txt
CHANGED
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]
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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',
|
|
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.
|
|
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
|
-
|
|
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
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.
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
134
|
+
summary: fbtxt2json (& fbtxt2csv) - convert football.txt match schedules & more to
|
|
135
|
+
(structured) json or (tabular) csv
|
|
117
136
|
test_files: []
|