icu_tournament 0.8.9
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/LICENCE +22 -0
- data/README.rdoc +75 -0
- data/Rakefile +57 -0
- data/VERSION.yml +5 -0
- data/lib/icu_tournament.rb +8 -0
- data/lib/icu_tournament/federation.rb +303 -0
- data/lib/icu_tournament/name.rb +274 -0
- data/lib/icu_tournament/player.rb +204 -0
- data/lib/icu_tournament/result.rb +191 -0
- data/lib/icu_tournament/team.rb +90 -0
- data/lib/icu_tournament/tournament.rb +508 -0
- data/lib/icu_tournament/tournament_fcsv.rb +310 -0
- data/lib/icu_tournament/tournament_krause.rb +329 -0
- data/lib/icu_tournament/util.rb +156 -0
- data/spec/federation_spec.rb +176 -0
- data/spec/name_spec.rb +208 -0
- data/spec/player_spec.rb +313 -0
- data/spec/result_spec.rb +203 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/team_spec.rb +60 -0
- data/spec/tournament_fcsv_spec.rb +548 -0
- data/spec/tournament_krause_spec.rb +379 -0
- data/spec/tournament_spec.rb +733 -0
- data/spec/util_spec.rb +357 -0
- metadata +97 -0
@@ -0,0 +1,310 @@
|
|
1
|
+
module ICU
|
2
|
+
class Tournament
|
3
|
+
|
4
|
+
=begin rdoc
|
5
|
+
|
6
|
+
== Foreign CSV
|
7
|
+
|
8
|
+
This is a format ({specification}[http://www.icu.ie/articles/display.php?id=172]) used by the ICU[http://icu.ie]
|
9
|
+
for players to submit their individual results in foreign tournaments for domestic rating.
|
10
|
+
|
11
|
+
Suppose, for example, that the following data is the file <em>tournament.csv</em>:
|
12
|
+
|
13
|
+
Event,"Isle of Man Masters, 2007"
|
14
|
+
Start,2007-09-22
|
15
|
+
Rounds,9
|
16
|
+
Website,http://www.bcmchess.co.uk/monarch2007/
|
17
|
+
|
18
|
+
Player,456,Fox,Anthony
|
19
|
+
1,0,B,Taylor,Peter P.,2209,,ENG
|
20
|
+
2,=,W,Nadav,Egozi,2205,,ISR
|
21
|
+
3,=,B,Cafolla,Peter,2048,,IRL
|
22
|
+
4,1,W,Spanton,Tim R.,1982,,ENG
|
23
|
+
5,1,B,Grant,Alan,2223,,SCO
|
24
|
+
6,0,-
|
25
|
+
7,=,W,Walton,Alan J.,2223,,ENG
|
26
|
+
8,0,B,Bannink,Bernard,2271,FM,NED
|
27
|
+
9,=,W,Phillips,Roy,2271,,MAU
|
28
|
+
Total,4
|
29
|
+
|
30
|
+
This file can be parsed as follows.
|
31
|
+
|
32
|
+
data = open('tournament.csv') { |f| f.read }
|
33
|
+
parser = ICU::Tournament::ForeignCSV.new
|
34
|
+
tournament = parser.parse(data)
|
35
|
+
|
36
|
+
If the file is correctly specified, the return value from the <em>parse</em> method is an instance of
|
37
|
+
ICU::Tournament (rather than <em>nil</em>, which indicates an error). In this example the file is valid, so:
|
38
|
+
|
39
|
+
tournament.name # => "Isle of Man Masters, 2007"
|
40
|
+
tournament.start # => "2007-09-22"
|
41
|
+
tournament.rounds # => 9
|
42
|
+
tournament.website # => "http://www.bcmchess.co.uk/monarch2007/"
|
43
|
+
|
44
|
+
The main player (the player whose results are being reported for rating) played 9 rounds
|
45
|
+
but only 8 other players (he had a bye in round 6), so the total number of players is 9.
|
46
|
+
|
47
|
+
tournament.players.size # => 9
|
48
|
+
|
49
|
+
Each player has a unique number for the tournament. The main player always occurs first in this type of file, so his number is 1.
|
50
|
+
|
51
|
+
player = tournament.player(1)
|
52
|
+
player.name # => "Fox, Anthony"
|
53
|
+
|
54
|
+
This player has 4 points from 9 rounds but only 8 of his results are are rateable (because of the bye).
|
55
|
+
|
56
|
+
player.points # => 4.0
|
57
|
+
player.results.size # => 9
|
58
|
+
player.results.find_all{ |r| r.rateable }.size # => 8
|
59
|
+
|
60
|
+
The other players all have numbers greater than 1.
|
61
|
+
|
62
|
+
opponents = tournamnet.players.reject { |o| o.num == 1 }
|
63
|
+
|
64
|
+
There are 8 opponents (of the main player) each with exactly one game.
|
65
|
+
|
66
|
+
opponents.size # => 8
|
67
|
+
opponents.find_all{ |o| o.results.size == 1 }.size # => 8
|
68
|
+
|
69
|
+
However, none of the opponents' results are rateable because they are foreign to the domestic rating list
|
70
|
+
to which the main player belongs. For example:
|
71
|
+
|
72
|
+
opponent = tournament.players(2)
|
73
|
+
opponent.name # => "Taylor, Peter P."
|
74
|
+
opponent.results[0].rateable # => false
|
75
|
+
|
76
|
+
A tournament can be serialized back to CSV format (the reverse of parsing) with the _serialize_ method
|
77
|
+
of the parser object.
|
78
|
+
|
79
|
+
csv = parser.serialize(tournament)
|
80
|
+
|
81
|
+
Or equivalently, the _serialize_ instance method of the tournament, if the appropriate parser name is supplied.
|
82
|
+
|
83
|
+
csv = tournament.serialize('ForeignCSV')
|
84
|
+
|
85
|
+
You can also build the tournament object from scratch using your own data and then serialize it.
|
86
|
+
For example, here are the commands to reproduce the example above.
|
87
|
+
|
88
|
+
t = ICU::Tournament.new("Isle of Man Masters, 2007", '2007-09-22', :rounds => 9)
|
89
|
+
t.site = 'http://www.bcmchess.co.uk/monarch2007/'
|
90
|
+
t.add_player(ICU::Player.new('Anthony', 'Fox', 1, :rating => 2100, :fed => 'IRL', :id => 456))
|
91
|
+
t.add_player(ICU::Player.new('Peter P.', 'Taylor', 2, :rating => 2209, :fed => 'ENG'))
|
92
|
+
t.add_player(ICU::Player.new('Egozi', 'Nadav', 3, :rating => 2205, :fed => 'ISR'))
|
93
|
+
t.add_player(ICU::Player.new('Peter', 'Cafolla', 4, :rating => 2048, :fed => 'IRL'))
|
94
|
+
t.add_player(ICU::Player.new('Tim R.', 'Spanton', 5, :rating => 1982, :fed => 'ENG'))
|
95
|
+
t.add_player(ICU::Player.new('Alan', 'Grant', 6, :rating => 2223, :fed => 'SCO'))
|
96
|
+
t.add_player(ICU::Player.new('Alan J.', 'Walton', 7, :rating => 2223, :fed => 'ENG'))
|
97
|
+
t.add_player(ICU::Player.new('Bernard', 'Bannink', 8, :rating => 2271, :fed => 'NED', :title => 'FM'))
|
98
|
+
t.add_player(ICU::Player.new('Roy', 'Phillips', 9, :rating => 2271, :fed => 'MAU'))
|
99
|
+
t.add_result(ICU::Result.new(1, 1, 'L', :opponent => 2, :colour => 'B'))
|
100
|
+
t.add_result(ICU::Result.new(2, 1, 'D', :opponent => 3, :colour => 'W'))
|
101
|
+
t.add_result(ICU::Result.new(3, 1, 'D', :opponent => 4, :colour => 'B'))
|
102
|
+
t.add_result(ICU::Result.new(4, 1, 'W', :opponent => 5, :colour => 'W'))
|
103
|
+
t.add_result(ICU::Result.new(5, 1, 'W', :opponent => 6, :colour => 'B'))
|
104
|
+
t.add_result(ICU::Result.new(6, 1, 'L'))
|
105
|
+
t.add_result(ICU::Result.new(7, 1, 'D', :opponent => 7, :colour => 'W'))
|
106
|
+
t.add_result(ICU::Result.new(8, 1, 'L', :opponent => 8, :colour => 'B'))
|
107
|
+
t.add_result(ICU::Result.new(9, 1, 'D', :opponent => 9, :colour => 'W'))
|
108
|
+
t.validate!
|
109
|
+
puts t.serialize('ForeignCSV')
|
110
|
+
|
111
|
+
=end
|
112
|
+
|
113
|
+
class ForeignCSV
|
114
|
+
attr_reader :error
|
115
|
+
|
116
|
+
# Parse CSV data returning a Tournament on success or a nil on failure.
|
117
|
+
# In the case of failure, an error message can be retrived via the <em>error</em> method.
|
118
|
+
def parse(csv)
|
119
|
+
begin
|
120
|
+
parse!(csv)
|
121
|
+
rescue => ex
|
122
|
+
@error = ex.message
|
123
|
+
nil
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Parse CSV data returning a Tournament on success or raising an exception on error.
|
128
|
+
def parse!(csv)
|
129
|
+
@state, @line, @round, @sum, @error = 0, 0, nil, nil, nil
|
130
|
+
@tournament = Tournament.new('Dummy', '2000-01-01')
|
131
|
+
|
132
|
+
Util::CSV.parse(csv, :row_sep => :auto) do |r|
|
133
|
+
@line += 1 # increment line number
|
134
|
+
next if r.size == 0 # skip empty lines
|
135
|
+
r = r.map{|c| c.nil? ? '' : c.strip} # trim all spaces, turn nils to blanks
|
136
|
+
next if r[0] == '' # skip blanks in column 1
|
137
|
+
@r = r # remember this record for later
|
138
|
+
|
139
|
+
begin
|
140
|
+
case @state
|
141
|
+
when 0 then event
|
142
|
+
when 1 then start
|
143
|
+
when 2 then rounds
|
144
|
+
when 3 then website
|
145
|
+
when 4 then player
|
146
|
+
when 5 then result
|
147
|
+
when 6 then total
|
148
|
+
else raise "internal error - state #{@state} does not exist"
|
149
|
+
end
|
150
|
+
rescue => err
|
151
|
+
raise err.class, "line #{@line}: #{err.message}", err.backtrace unless err.message.match(/^line [1-9]/)
|
152
|
+
raise
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
unless @state == 4
|
157
|
+
exp = case @state
|
158
|
+
when 0 then "the event name"
|
159
|
+
when 1 then "the start date"
|
160
|
+
when 2 then "the number of rounds"
|
161
|
+
when 3 then "the website address"
|
162
|
+
when 5 then "a result for round #{@round+1}"
|
163
|
+
when 6 then "a total score"
|
164
|
+
end
|
165
|
+
raise "line #{@line}: premature termination - expected #{exp}"
|
166
|
+
end
|
167
|
+
raise "line #{@line}: no players found in file" if @tournament.players.size == 0
|
168
|
+
|
169
|
+
@tournament.validate!
|
170
|
+
|
171
|
+
@tournament
|
172
|
+
end
|
173
|
+
|
174
|
+
# Serialise a tournament back into CSV format.
|
175
|
+
def serialize(t)
|
176
|
+
return nil unless t.class == ICU::Tournament;
|
177
|
+
Util::CSV.generate do |csv|
|
178
|
+
csv << ["Event", t.name]
|
179
|
+
csv << ["Start", t.start]
|
180
|
+
csv << ["Rounds", t.rounds]
|
181
|
+
csv << ["Website", t.site]
|
182
|
+
t.players.each do |p|
|
183
|
+
next unless p.id
|
184
|
+
csv << []
|
185
|
+
csv << ["Player", p.id, p.last_name, p.first_name]
|
186
|
+
(1..t.rounds).each do |n|
|
187
|
+
data = []
|
188
|
+
data << n
|
189
|
+
r = p.find_result(n)
|
190
|
+
data << case r.score; when 'W' then '1'; when 'L' then '0'; else '='; end
|
191
|
+
if r.rateable
|
192
|
+
data << r.colour
|
193
|
+
o = t.player(r.opponent)
|
194
|
+
data << o.last_name
|
195
|
+
data << o.first_name
|
196
|
+
data << o.rating
|
197
|
+
data << o.title
|
198
|
+
data << o.fed
|
199
|
+
else
|
200
|
+
data << '-'
|
201
|
+
end
|
202
|
+
csv << data
|
203
|
+
end
|
204
|
+
csv << ["Total", p.points]
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
private
|
210
|
+
|
211
|
+
def event
|
212
|
+
abort "the 'Event' keyword", 0 unless @r[0].match(/^(Event|Tournament)$/i)
|
213
|
+
abort "the event name", 1 unless @r.size > 1 && @r[1] != ''
|
214
|
+
@tournament.name = @r[1]
|
215
|
+
@state = 1
|
216
|
+
end
|
217
|
+
|
218
|
+
def start
|
219
|
+
abort "the 'Start' keyword", 0 unless @r[0].match(/^(Start(\s+Date)?|Date)$/i)
|
220
|
+
abort "the start date", 1 unless @r.size > 1 && @r[1] != ''
|
221
|
+
@tournament.start = @r[1]
|
222
|
+
@state = 2
|
223
|
+
end
|
224
|
+
|
225
|
+
def rounds
|
226
|
+
abort "the 'Rounds' keyword", 0 unless @r[0].match(/(Number of )?Rounds$/)
|
227
|
+
abort "the number of rounds", 1 unless @r.size > 1 && @r[1].match(/^[1-9]\d*/)
|
228
|
+
@tournament.rounds = @r[1]
|
229
|
+
@state = 3
|
230
|
+
end
|
231
|
+
|
232
|
+
def website
|
233
|
+
abort "the 'Website' keyword", 0 unless @r[0].match(/^(Web(\s?site)?|Site)$/i)
|
234
|
+
abort "the event website", 1 unless @r.size > 1 && @r[1] != ''
|
235
|
+
@tournament.site = @r[1]
|
236
|
+
@state = 4
|
237
|
+
end
|
238
|
+
|
239
|
+
def player
|
240
|
+
abort "the 'Player' keyword", 0 unless @r[0].match(/^Player$/i)
|
241
|
+
abort "a player's ICU number", 1 unless @r.size > 1 && @r[1].match(/^[1-9]/i)
|
242
|
+
abort "a player's last name", 2 unless @r.size > 2 && @r[2].match(/[a-z]/i)
|
243
|
+
abort "a player's first name", 3 unless @r.size > 3 && @r[3].match(/[a-z]/i)
|
244
|
+
@player = Player.new(@r[3], @r[2], @tournament.players.size + 1, :id => @r[1])
|
245
|
+
old_player = @tournament.find_player(@player)
|
246
|
+
if old_player
|
247
|
+
raise "two players with the same name (#{@player.name}) have conflicting details" unless old_player.eql?(@player)
|
248
|
+
raise "same player (#{@player.name}) has more than one set of results" if old_player.id
|
249
|
+
old_player.merge(@player)
|
250
|
+
@player = old_player
|
251
|
+
else
|
252
|
+
@tournament.add_player(@player)
|
253
|
+
end
|
254
|
+
@round = 0
|
255
|
+
@state = 5
|
256
|
+
end
|
257
|
+
|
258
|
+
def result
|
259
|
+
@round+= 1
|
260
|
+
abort "round number #{round}", 0 unless @r[0].to_i == @round
|
261
|
+
abort "a colour (W/B) or dash (for a bye)", 2 unless @r.size > 2 && @r[2].match(/^(W|B|-)/i)
|
262
|
+
result = Result.new(@round, @player.num, @r[1])
|
263
|
+
if @r[2] == '-'
|
264
|
+
@tournament.add_result(result)
|
265
|
+
else
|
266
|
+
result.colour = @r[2]
|
267
|
+
opponent = Player.new(@r[4], @r[3], @tournament.players.size + 1, :rating => @r[5], :title => @r[6], :fed => @r[7])
|
268
|
+
raise "opponent must have a federation" unless opponent.fed
|
269
|
+
old_player = @tournament.find_player(opponent)
|
270
|
+
if old_player
|
271
|
+
raise "two players with the same name (#{opponent.name}) have conflicting details" unless old_player.eql?(opponent)
|
272
|
+
result.opponent = old_player.num
|
273
|
+
if old_player.id
|
274
|
+
old_player.merge(opponent)
|
275
|
+
old_result = @player.find_result(@round)
|
276
|
+
raise "missing result for player (#{@player.name}) in round #{@round}" unless old_result
|
277
|
+
raise "mismatched results for player (#{old_player.name}) in round #{@round}" unless result == old_result
|
278
|
+
old_result.rateable = true
|
279
|
+
else
|
280
|
+
old_result = old_player.find_result(@round)
|
281
|
+
raise "a player (#{old_player.name}) has more than one game in the same round (#{@round})" if old_result
|
282
|
+
@tournament.add_result(result, false)
|
283
|
+
end
|
284
|
+
else
|
285
|
+
@tournament.add_player(opponent)
|
286
|
+
result.opponent = opponent.num
|
287
|
+
@tournament.add_result(result, false)
|
288
|
+
end
|
289
|
+
end
|
290
|
+
@state = 6 if @round == @tournament.rounds
|
291
|
+
end
|
292
|
+
|
293
|
+
def total
|
294
|
+
points = @player.points
|
295
|
+
abort "the 'Total' keyword", 0 unless @r[0].match(/^Total$/i)
|
296
|
+
abort "the player's (#{@player.object_id}, #{@player.results.size}) total points to be #{points}", 1 unless @r[1].to_f == points
|
297
|
+
@state = 4
|
298
|
+
end
|
299
|
+
|
300
|
+
def abort(expected, cell)
|
301
|
+
got = @r[cell]
|
302
|
+
error = "line #{@line}"
|
303
|
+
error << ", cell #{cell+1}"
|
304
|
+
error << ": expected #{expected}"
|
305
|
+
error << " but got #{got == '' ? 'a blank cell' : "'#{got}'"}"
|
306
|
+
raise error
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
@@ -0,0 +1,329 @@
|
|
1
|
+
module ICU
|
2
|
+
class Tournament
|
3
|
+
|
4
|
+
=begin rdoc
|
5
|
+
|
6
|
+
== Krause
|
7
|
+
|
8
|
+
This is the {format}[http://www.fide.com/component/content/article/5-whats-news/2245-736-general-data-exchange-format-for-tournament-results]
|
9
|
+
used to submit tournament results to FIDE[http://www.fide.com] for rating.
|
10
|
+
|
11
|
+
Suppose, for example, that the following data is the file <em>tournament.tab</em>:
|
12
|
+
|
13
|
+
012 Fantasy Tournament
|
14
|
+
032 IRL
|
15
|
+
042 2009.09.09
|
16
|
+
0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
|
17
|
+
132 09.09.09 09.09.10 09.09.11
|
18
|
+
001 1 w Mouse,Minerva 1900 USA 1234567 1928.05.15 1.0 2 2 b 0 3 w 1
|
19
|
+
001 2 m m Duck,Daffy 2200 IRL 7654321 1937.04.17 2.0 1 1 w 1 3 b 1
|
20
|
+
001 3 m g Mouse,Mickey 2600 USA 1726354 1928.05.15 0.0 3 1 b 0 2 w 0
|
21
|
+
|
22
|
+
This file can be parsed as follows.
|
23
|
+
|
24
|
+
data = open('tournament.tab') { |f| f.read }
|
25
|
+
parser = ICU::Tournament::Krause.new
|
26
|
+
tournament = parser.parse(data)
|
27
|
+
|
28
|
+
If the file is correctly specified, the return value from the <em>parse</em> method is an instance of
|
29
|
+
ICU::Tournament (rather than <em>nil</em>, which indicates an error). In this example the file is valid, so:
|
30
|
+
|
31
|
+
tournament.name # => "Fantasy Tournament"
|
32
|
+
tournament.start # => "2009-09-09"
|
33
|
+
tournament.fed # => "IRL"
|
34
|
+
tournament.players.size # => 9
|
35
|
+
|
36
|
+
Some values, not explicitly set in the file, are deduced:
|
37
|
+
|
38
|
+
tournament.rounds # => 3
|
39
|
+
tournament.finish # => "2009-09-11"
|
40
|
+
|
41
|
+
A player can be retrieved from the tournament via the _players_ array or by sending a valid player number to the _player_ method.
|
42
|
+
|
43
|
+
minnie = tournament.player(1)
|
44
|
+
minnie.name # => "Mouse, Minerva"
|
45
|
+
minnie.points # => 1.0
|
46
|
+
minnie.results.size # => 2
|
47
|
+
|
48
|
+
daffy = tournament.player(2)
|
49
|
+
daffy.title # => "IM"
|
50
|
+
daffy.rating # => 2200
|
51
|
+
daffy.fed # => "IRL"
|
52
|
+
daffy.id # => 7654321
|
53
|
+
daffy.dob # => "1937-04-17"
|
54
|
+
|
55
|
+
If the ranking numbers are missing from the file or inconsistent (e.g. player A is ranked above player B
|
56
|
+
but has less points than player B) they are recalculated as a side effect of the parse.
|
57
|
+
|
58
|
+
daffy.rank # => 1
|
59
|
+
minnie.rank # => 2
|
60
|
+
mickey.rank # => 3
|
61
|
+
|
62
|
+
Comments in the input file (lines that do not start with a valid data identification number) are available from the parser
|
63
|
+
instance via its _comments_ method (returning a string). Note that these comments are reset evry time the instance is used
|
64
|
+
to parse another file.
|
65
|
+
|
66
|
+
parser.comments # => "0123456789..."
|
67
|
+
|
68
|
+
A tournament can be serialized back to Krause format (the reverse of parsing) with the _serialize_ method of the parser.
|
69
|
+
|
70
|
+
krause = parser.serialize(tournament)
|
71
|
+
|
72
|
+
Or alternatively, by the _serialize_ method of the tournament object if the name of the serializer is supplied.
|
73
|
+
|
74
|
+
krause = tournament.serialize('Krause')
|
75
|
+
|
76
|
+
The following lists Krause data identification numbers, their description and, where available, their corresponding
|
77
|
+
attributes in an ICU::Tournament instance.
|
78
|
+
|
79
|
+
[001 Player record] Use _players_ to get all players or _player_ with a player number to get a single instance.
|
80
|
+
[012 Name] Get or set with _name_. Free text. A tounament name is mandatory.
|
81
|
+
[013 Teams] Create an ICU::Team, add player numbers to it, use _add_team_ to add to tournament, _get_team_/_teams_ to retrive it/them.
|
82
|
+
[022 City] Get or set with _city_. Free text.
|
83
|
+
[032 Federation] Get or set with _fed_. Getter returns either _nil_ or a three letter code. Setter can take various formats (see ICU::Federation).
|
84
|
+
[042 Start date] Get or set with _start_. Getter returns <em>yyyy-mm-dd</em> format, but setter can use any reasonable date format. Start date is mandadory.
|
85
|
+
[052 End date] Get or set with _finish_. Returns either <em>yyyy-mm-dd</em> format or _nil_ if not set. Like _start_, can be set with various date formats.
|
86
|
+
[062 Number of players] Not used. Treated as comment in parsed files. Can be determined from the size of the _players_ array.
|
87
|
+
[072 Number of rated players] Not used. Treated as comment in parsed files. Can be determined by analysing the array returned by _players_.
|
88
|
+
[082 Number of teams] Not used. Treated as comment in parsed files.
|
89
|
+
[092 Type of tournament] Get or set with _type_. Free text.
|
90
|
+
[102 Arbiter(s)] Get or set with -arbiter_. Free text.
|
91
|
+
[112 Deputy(ies)] Get or set with _deputy_. Free text.
|
92
|
+
[122 Time control] Get or set with _time_control_. Free text.
|
93
|
+
[132 Round dates] Get an array of dates using _round_dates_ or one specific round date by calling _round_date_ with a round number.
|
94
|
+
|
95
|
+
=end
|
96
|
+
|
97
|
+
class Krause
|
98
|
+
attr_reader :error, :comments
|
99
|
+
|
100
|
+
# Parse Krause data returning a Tournament on success or a nil on failure.
|
101
|
+
# In the case of failure, an error message can be retrived via the <em>error</em> method.
|
102
|
+
def parse(krs)
|
103
|
+
begin
|
104
|
+
parse!(krs)
|
105
|
+
rescue => ex
|
106
|
+
@error = ex.message
|
107
|
+
nil
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Parse Krause data returning a Tournament on success or raising an exception on error.
|
112
|
+
def parse!(krs)
|
113
|
+
@lineno = 0
|
114
|
+
@tournament = Tournament.new('Dummy', '2000-01-01')
|
115
|
+
@name_set, @start_set = false, false
|
116
|
+
@comments = ''
|
117
|
+
@results = Array.new
|
118
|
+
|
119
|
+
# Process all lines.
|
120
|
+
krs.each_line do |line|
|
121
|
+
@lineno += 1 # increment line number
|
122
|
+
line.strip! # remove leading and trailing white space
|
123
|
+
next if line == '' # skip blank lines
|
124
|
+
@line = line # remember this line for later
|
125
|
+
|
126
|
+
# Does it havea DIN or is it just a comment?
|
127
|
+
if @line.match(/^(\d{3}) (.*)$/)
|
128
|
+
din = $1 # data identification number (DIN)
|
129
|
+
@data = $2 # the data after the DIN
|
130
|
+
else
|
131
|
+
add_comment
|
132
|
+
next
|
133
|
+
end
|
134
|
+
|
135
|
+
# Process the line given the DIN.
|
136
|
+
begin
|
137
|
+
case din
|
138
|
+
when '001' then add_player # player and results record
|
139
|
+
when '012' then set_name # name (mandatory)
|
140
|
+
when '013' then add_team # team name and members
|
141
|
+
when '022' then @tournament.city = @data # city
|
142
|
+
when '032' then @tournament.fed = @data # federation
|
143
|
+
when '042' then set_start # start date (mandatory)
|
144
|
+
when '052' then @tournament.finish = @data # end date
|
145
|
+
when '062' then add_comment # number of players (calculated from 001 records)
|
146
|
+
when '072' then add_comment # number of rated players (calculated from 001 records)
|
147
|
+
when '082' then add_comment # number of teams (calculated from 013 records)
|
148
|
+
when '092' then @tournament.type = @data # type of tournament
|
149
|
+
when '102' then @tournament.arbiter = @data # arbiter(s)
|
150
|
+
when '112' then @tournament.deputy = @data # deputy(ies)
|
151
|
+
when '122' then @tournament.time_control = @data # time control
|
152
|
+
when '132' then add_round_dates # round dates
|
153
|
+
else raise "invalid DIN #{din}"
|
154
|
+
end
|
155
|
+
rescue => err
|
156
|
+
raise err.class, "line #{@lineno}: #{err.message}", err.backtrace
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Now that all players are present, add the results to the tournament.
|
161
|
+
@results.each do |r|
|
162
|
+
lineno, player, data, result = r
|
163
|
+
begin
|
164
|
+
@tournament.add_result(result)
|
165
|
+
rescue => err
|
166
|
+
raise "line #{lineno}, player #{player}, result '#{data}': #{err.message}"
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Certain attributes are mandatory and should have been specifically set.
|
171
|
+
raise "tournament name missing" unless @name_set
|
172
|
+
raise "tournament start date missing" unless @start_set
|
173
|
+
|
174
|
+
# Finally, exercise the tournament object's internal validation, reranking if neccessary.
|
175
|
+
@tournament.validate!(:rerank => true)
|
176
|
+
|
177
|
+
@tournament
|
178
|
+
end
|
179
|
+
|
180
|
+
# Serialise a tournament back into Krause format.
|
181
|
+
def serialize(t)
|
182
|
+
return nil unless t.class == ICU::Tournament;
|
183
|
+
krause = ''
|
184
|
+
krause << "012 #{t.name}\n"
|
185
|
+
krause << "022 #{t.city}\n" if t.city
|
186
|
+
krause << "032 #{t.fed}\n" if t.fed
|
187
|
+
krause << "042 #{t.start}\n"
|
188
|
+
krause << "052 #{t.finish}\n" if t.finish
|
189
|
+
krause << "092 #{t.type}\n" if t.type
|
190
|
+
krause << "102 #{t.arbiter}\n" if t.arbiter
|
191
|
+
krause << "112 #{t.deputy}\n" if t.deputy
|
192
|
+
krause << "122 #{t.time_control}\n" if t.time_control
|
193
|
+
t.teams.each do |team|
|
194
|
+
krause << sprintf('013 %-31s', team.name)
|
195
|
+
team.members.each{ |m| krause << sprintf(' %4d', m) }
|
196
|
+
krause << "\n"
|
197
|
+
end
|
198
|
+
rounds = t.last_round
|
199
|
+
if t.round_dates.size == rounds && rounds > 0
|
200
|
+
krause << "132 #{' ' * 85}"
|
201
|
+
t.round_dates.each{ |d| krause << d.sub(/^../, ' ') }
|
202
|
+
krause << "\n"
|
203
|
+
end
|
204
|
+
t.players.each{ |p| krause << p.to_krause(rounds) }
|
205
|
+
krause
|
206
|
+
end
|
207
|
+
|
208
|
+
private
|
209
|
+
|
210
|
+
def set_name
|
211
|
+
@tournament.name = @data
|
212
|
+
@name_set = true
|
213
|
+
end
|
214
|
+
|
215
|
+
def set_start
|
216
|
+
@tournament.start = @data
|
217
|
+
@start_set = true
|
218
|
+
end
|
219
|
+
|
220
|
+
def add_player
|
221
|
+
raise "player record less than minimum length" if @line.length < 99
|
222
|
+
|
223
|
+
# Player details.
|
224
|
+
num = @data[0, 4]
|
225
|
+
nam = Name.new(@data[10, 32])
|
226
|
+
opt =
|
227
|
+
{
|
228
|
+
:gender => @data[5, 1],
|
229
|
+
:title => @data[6, 3],
|
230
|
+
:rating => @data[44, 4],
|
231
|
+
:fed => @data[49, 3],
|
232
|
+
:id => @data[53, 11],
|
233
|
+
:dob => @data[65, 10],
|
234
|
+
:rank => @data[81, 4],
|
235
|
+
}
|
236
|
+
player = Player.new(nam.first, nam.last, num, opt)
|
237
|
+
@tournament.add_player(player)
|
238
|
+
|
239
|
+
# Results.
|
240
|
+
points = @data[77, 4].strip
|
241
|
+
points = points == '' ? nil : points.to_f
|
242
|
+
index = 87
|
243
|
+
round = 1
|
244
|
+
total = 0.0
|
245
|
+
while @data.length >= index + 8
|
246
|
+
total+= add_result(round, player.num, @data[index, 8])
|
247
|
+
index+= 10
|
248
|
+
round+= 1
|
249
|
+
end
|
250
|
+
raise "declared points total (#{points}) does not agree with total from summed results (#{total})" if points && points != total
|
251
|
+
end
|
252
|
+
|
253
|
+
def add_result(round, player, data)
|
254
|
+
return 0.0 if data.strip! == '' # no result for this round
|
255
|
+
raise "invalid result '#{data}'" unless data.match(/^(0{1,4}|[1-9]\d{0,3}) (w|b|-) (1|0|=|\+|-)$/)
|
256
|
+
opponent = $1.to_i
|
257
|
+
colour = $2
|
258
|
+
score = $3
|
259
|
+
options = Hash.new
|
260
|
+
options[:opponent] = opponent unless opponent == 0
|
261
|
+
options[:colour] = colour unless colour == '-'
|
262
|
+
options[:rateable] = false unless score.match(/^(1|0|=)$/)
|
263
|
+
result = Result.new(round, player, score, options)
|
264
|
+
@results << [@lineno, player, data, result]
|
265
|
+
result.points
|
266
|
+
end
|
267
|
+
|
268
|
+
def add_team
|
269
|
+
raise error "team record less than minimum length" if @line.length < 40
|
270
|
+
team = Team.new(@data[0, 31])
|
271
|
+
index = 32
|
272
|
+
while @data.length >= index + 4
|
273
|
+
team.add_member(@data[index, 4])
|
274
|
+
index+= 5
|
275
|
+
end
|
276
|
+
@tournament.add_team(team)
|
277
|
+
end
|
278
|
+
|
279
|
+
def add_round_dates
|
280
|
+
raise "round dates record less than minimum length" if @line.length < 99
|
281
|
+
index = 87
|
282
|
+
while @data.length >= index + 8
|
283
|
+
date = @data[index, 8].strip
|
284
|
+
@tournament.add_round_date("20#{date}") unless date == ''
|
285
|
+
index+= 10
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
def add_comment
|
290
|
+
@comments << @line
|
291
|
+
@comments << "\n"
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
class Player
|
297
|
+
# Format a player's 001 record as it would appear in a Krause formatted file (including the final newline).
|
298
|
+
def to_krause(rounds)
|
299
|
+
krause = '001'
|
300
|
+
krause << sprintf(' %4d', @num)
|
301
|
+
krause << sprintf(' %1s', case @gender; when 'M' then 'm'; when 'F' then 'w'; else ''; end)
|
302
|
+
krause << sprintf(' %2s', case @title; when nil then ''; when 'IM' then 'm'; when 'WIM' then 'wm'; else @title[0, @title.length-1].downcase; end)
|
303
|
+
krause << sprintf(' %-33s', "#{@last_name},#{@first_name}")
|
304
|
+
krause << sprintf(' %4s', @rating)
|
305
|
+
krause << sprintf(' %3s', @fed)
|
306
|
+
krause << sprintf(' %11s', @id)
|
307
|
+
krause << sprintf(' %10s', @dob)
|
308
|
+
krause << sprintf(' %4.1f', points)
|
309
|
+
krause << sprintf(' %4s', @rank)
|
310
|
+
(1..rounds).each do |r|
|
311
|
+
result = find_result(r)
|
312
|
+
krause << sprintf(' %8s', result ? result.to_krause : '')
|
313
|
+
end
|
314
|
+
krause << "\n"
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
class Result
|
319
|
+
# Format a player's result as it would appear in a Krause formatted file (exactly 8 characters long, including leading whitespace).
|
320
|
+
def to_krause
|
321
|
+
return ' ' * 8 if !@opponent && !@colour && @score == 'L'
|
322
|
+
krause = sprintf('%4s ', @opponent || '0000')
|
323
|
+
krause << sprintf('%1s ', @colour ? @colour.downcase : '-')
|
324
|
+
krause << case @score; when 'W' then '1'; when 'L' then '0'; else '='; end if @rateable
|
325
|
+
krause << case @score; when 'W' then '+'; when 'L' then '-'; else '='; end if !@rateable
|
326
|
+
krause
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|