icu_tournament 0.8.9
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.
- 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
|