icu_tournament 0.8.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -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