icu_tournament 0.9.6 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/icu_tournament.rb +1 -1
- data/lib/icu_tournament/player.rb +12 -2
- data/lib/icu_tournament/result.rb +2 -0
- data/lib/icu_tournament/tournament.rb +107 -85
- data/lib/icu_tournament/tournament_sp.rb +309 -0
- data/lib/icu_tournament/version.rb +1 -1
- data/spec/tournament_sp_spec.rb +157 -0
- data/spec/tournament_spec.rb +74 -26
- metadata +25 -2
data/lib/icu_tournament.rb
CHANGED
@@ -3,6 +3,6 @@
|
|
3
3
|
icu_tournament_files = Array.new
|
4
4
|
icu_tournament_files.concat %w{util name federation}
|
5
5
|
icu_tournament_files.concat %w{player result team tournament}
|
6
|
-
icu_tournament_files.concat %w{fcsv krause}.map{ |f| "tournament_#{f}"}
|
6
|
+
icu_tournament_files.concat %w{fcsv krause sp}.map{ |f| "tournament_#{f}"}
|
7
7
|
|
8
8
|
icu_tournament_files.each { |file| require "icu_tournament/#{file}" }
|
@@ -25,7 +25,7 @@ _fed_ (federation), _title_, _rating_, _rank_ and _dob_ (date of birth).
|
|
25
25
|
Some of these values will also be canonicalised to some extent. For example,
|
26
26
|
the date of birth conforms to a _yyyy-mm-dd_ format, the chess title will be two
|
27
27
|
to three capital letters always ending in _M_ and the federation, if it's three
|
28
|
-
letters long, will be
|
28
|
+
letters long, will be upper-cased.
|
29
29
|
|
30
30
|
peter.dob # => 1976-07-17
|
31
31
|
peter.title # => 'GM'
|
@@ -40,6 +40,11 @@ Total scores is available via the _points_ method.
|
|
40
40
|
|
41
41
|
peter.points # => 5.5
|
42
42
|
|
43
|
+
A player's _id_ is their ID number in some external database (typically either ICU or FIDE).
|
44
|
+
|
45
|
+
peter.id = 16790 # ICU, or
|
46
|
+
peter.id = 4102142 # FIDE
|
47
|
+
|
43
48
|
Players can be compared to see if they're roughly or exactly the same, which may be useful in detecting duplicates.
|
44
49
|
If the names match and the federations don't disagree then two players are equal according to the _==_ operator.
|
45
50
|
The player number is irrelevant.
|
@@ -152,7 +157,12 @@ All other attributes are unaffected.
|
|
152
157
|
already = @results.find_all { |r| r.round == result.round }
|
153
158
|
return if already.size == 1 && already[0].eql?(result)
|
154
159
|
raise "round number (#{result.round}) of new result is not unique and new result is not the same as existing one" unless already.size == 0
|
155
|
-
@results
|
160
|
+
if @results.size == 0 || @results.last.round <= result.round
|
161
|
+
@results << result
|
162
|
+
else
|
163
|
+
i = (0..@results.size-1).find { |n| @results[n].round > result.round }
|
164
|
+
@results.insert(i, result)
|
165
|
+
end
|
156
166
|
end
|
157
167
|
|
158
168
|
# Lookup a result by round number.
|
@@ -168,7 +168,9 @@ The _points_ read-only accessor always returns a floating point number: either 0
|
|
168
168
|
self.player = map[@player]
|
169
169
|
if @opponent
|
170
170
|
raise "result opponent number #{@opponent} not found in renumbering hash" unless map[@opponent]
|
171
|
+
old_rateable = @rateable
|
171
172
|
self.opponent = map[@opponent]
|
173
|
+
self.rateable = old_rateable # because setting the opponent has a side-effect which is undesirable in this context
|
172
174
|
end
|
173
175
|
self
|
174
176
|
end
|
@@ -25,7 +25,7 @@ For example:
|
|
25
25
|
t.add_result(ICU::Result.new(1, 10, 'D', :opponent => 30, :colour => 'W'))
|
26
26
|
t.add_result(ICU::Result.new(2, 20, 'W', :opponent => 30, :colour => 'B'))
|
27
27
|
t.add_result(ICU::Result.new(3, 20, 'L', :opponent => 10, :colour => 'W'))
|
28
|
-
|
28
|
+
|
29
29
|
t.validate!(:rerank => true)
|
30
30
|
|
31
31
|
and then:
|
@@ -43,7 +43,7 @@ would result in the following output:
|
|
43
43
|
042 2009-11-09
|
44
44
|
001 10 Fischer,Bobby 1.5 1 30 w = 20 b 1
|
45
45
|
001 20 Kasparov,Garry 1.0 2 30 b 1 10 w 0
|
46
|
-
001 30 Orr,Mark 0.5 3 10 b = 20 w 0
|
46
|
+
001 30 Orr,Mark 0.5 3 10 b = 20 w 0
|
47
47
|
|
48
48
|
Note that the players should be added first because the _add_result_ method will
|
49
49
|
raise an exception if the players it references through their tournament numbers
|
@@ -75,7 +75,7 @@ Side effects of calling _validate!_ or _invalid_ include:
|
|
75
75
|
|
76
76
|
== Ranking
|
77
77
|
|
78
|
-
|
78
|
+
The players in a tournament can be ranked by calling the _rerank_ method directly.
|
79
79
|
|
80
80
|
t.rerank
|
81
81
|
|
@@ -89,14 +89,11 @@ have a rank but some are ranked higher than others on lower scores.
|
|
89
89
|
|
90
90
|
To rank the players requires a tie break method to be specified to order players on the same score.
|
91
91
|
The default is alphabetical (by last name then first name). Other methods can be specified by supplying
|
92
|
-
|
93
|
-
option of the _validate_ method. Examples:
|
92
|
+
an array of methods (strings or symbols) in order of precedence to the _tie_breaks_ setter. Examples:
|
94
93
|
|
95
|
-
t.
|
96
|
-
t.
|
97
|
-
|
98
|
-
t.validate(:rerank => :sonneborn_berger)
|
99
|
-
t.validate(:rerank => ['Modified Median', 'Neustadtl', 'Buchholz', 'wins'])
|
94
|
+
t.tie_breaks = ['Sonneborn-Berger']
|
95
|
+
t.tie_breaks = [:buchholz, :neustadtl, :blacks, :wins]
|
96
|
+
t.tie_breaks = [] # reset to the default
|
100
97
|
|
101
98
|
The full list of supported methods is:
|
102
99
|
|
@@ -106,28 +103,31 @@ The full list of supported methods is:
|
|
106
103
|
* _Neustadtl_ (or _Sonneborn-Berger_): sum of scores of players defeated plus half sum of scores of players drawn against
|
107
104
|
* _blacks_: number of blacks
|
108
105
|
* _wins_: number of wins
|
109
|
-
* _name_: alphabetical by name is
|
106
|
+
* _name_: alphabetical by name (if _tie_breaks_ is set to an empty array, as it is initially, then this will be used as the back-up tie breaker)
|
107
|
+
|
108
|
+
The return value from _rerank_ is the tournament object itself, to allow chaining, for example:
|
110
109
|
|
111
|
-
|
110
|
+
t.rerank.renumber
|
112
111
|
|
113
112
|
|
114
113
|
== Renumbering
|
115
114
|
|
116
115
|
The numbers used to uniquely identify each player in a tournament can be any set of unique integers
|
117
116
|
(including zero and negative numbers). To renumber the players so that these numbers start at 1 and
|
118
|
-
|
117
|
+
end with the total number of players, use the _renumber_ method. This method takes one optional
|
119
118
|
argument to specify how the renumbering is done.
|
120
119
|
|
121
120
|
t.renumber(:rank) # renumber by rank (if there are consistent rankings), otherwise by name alphabetically
|
122
121
|
t.renumber # the same, as renumbering by rank is the default
|
123
122
|
t.renumber(:name) # renumber by name alphabetically
|
124
|
-
|
123
|
+
t.renumber(:order) # renumber maintaining the order of the original numbers
|
124
|
+
|
125
125
|
The return value from _renumber_ is the tournament object itself.
|
126
126
|
|
127
127
|
=end
|
128
128
|
|
129
129
|
class Tournament
|
130
|
-
|
130
|
+
|
131
131
|
extend ICU::Accessor
|
132
132
|
attr_date :start
|
133
133
|
attr_date_or_nil :finish
|
@@ -135,9 +135,9 @@ The return value from _renumber_ is the tournament object itself.
|
|
135
135
|
attr_string %r%[a-z]%i, :name
|
136
136
|
attr_string_or_nil %r%[a-z]%i, :city, :type, :arbiter, :deputy
|
137
137
|
attr_string_or_nil %r%[1-9]%i, :time_control
|
138
|
-
|
139
|
-
attr_reader :round_dates, :site, :fed, :teams
|
140
|
-
|
138
|
+
|
139
|
+
attr_reader :round_dates, :site, :fed, :teams, :tie_breaks
|
140
|
+
|
141
141
|
# Constructor. Name and start date must be supplied. Other attributes are optional.
|
142
142
|
def initialize(name, start, opt={})
|
143
143
|
self.name = name
|
@@ -146,15 +146,16 @@ The return value from _renumber_ is the tournament object itself.
|
|
146
146
|
@player = {}
|
147
147
|
@teams = []
|
148
148
|
@round_dates = []
|
149
|
+
@tie_breaks = []
|
149
150
|
end
|
150
|
-
|
151
|
+
|
151
152
|
# Set the tournament federation. Can be _nil_.
|
152
153
|
def fed=(fed)
|
153
154
|
obj = Federation.find(fed)
|
154
155
|
@fed = obj ? obj.code : nil
|
155
156
|
raise "invalid tournament federation (#{fed})" if @fed.nil? && fed.to_s.strip.length > 0
|
156
157
|
end
|
157
|
-
|
158
|
+
|
158
159
|
# Add a round date.
|
159
160
|
def add_round_date(round_date)
|
160
161
|
round_date = round_date.to_s.strip
|
@@ -163,12 +164,12 @@ The return value from _renumber_ is the tournament object itself.
|
|
163
164
|
@round_dates << parsed_date
|
164
165
|
@round_dates.sort!
|
165
166
|
end
|
166
|
-
|
167
|
+
|
167
168
|
# Return the date of a given round, or nil if unavailable.
|
168
169
|
def round_date(round)
|
169
170
|
@round_dates[round-1]
|
170
171
|
end
|
171
|
-
|
172
|
+
|
172
173
|
# Return the greatest round number according to the players results (which may not be the same as the set number of rounds).
|
173
174
|
def last_round
|
174
175
|
last_round = 0
|
@@ -187,7 +188,7 @@ The return value from _renumber_ is the tournament object itself.
|
|
187
188
|
@site = "http://#{@site}" if @site && !@site.match(/^https?:\/\//)
|
188
189
|
raise "invalid site (#{site})" unless @site.nil? || @site.match(/^https?:\/\/[-\w]+(\.[-\w]+)+(\/[^\s]*)?$/i)
|
189
190
|
end
|
190
|
-
|
191
|
+
|
191
192
|
# Add a new team. The argument is either a team (possibly already with members) or the name of a new team.
|
192
193
|
# The team's name must be unique in the tournament. Returns the the team instance.
|
193
194
|
def add_team(team)
|
@@ -196,34 +197,57 @@ The return value from _renumber_ is the tournament object itself.
|
|
196
197
|
@teams << team
|
197
198
|
team
|
198
199
|
end
|
199
|
-
|
200
|
+
|
200
201
|
# Return the team object that matches a given name, or nil if not found.
|
201
202
|
def get_team(name)
|
202
203
|
@teams.find{ |t| t.matches(name) }
|
203
204
|
end
|
204
|
-
|
205
|
+
|
206
|
+
# Set the tie break methods.
|
207
|
+
def tie_breaks=(tie_breaks)
|
208
|
+
raise "argument error - always set tie breaks to an array" unless tie_breaks.class == Array
|
209
|
+
# Canonicalise the tie break method names.
|
210
|
+
tie_breaks.map! do |m|
|
211
|
+
m = m.to_s if m.class == Symbol
|
212
|
+
m = m.downcase.gsub(/[-\s]/, '_') if m.class == String
|
213
|
+
case m
|
214
|
+
when true then 'name'
|
215
|
+
when 'sonneborn_berger' then 'neustadtl'
|
216
|
+
when 'modified_median' then 'modified'
|
217
|
+
when 'median' then 'harkness'
|
218
|
+
else m
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
# Check they're all valid.
|
223
|
+
tie_breaks.each { |m| raise "invalid tie break method '#{m}'" unless m.to_s.match(/^(blacks|buchholz|harkness|modified|name|neustadtl|wins)$/) }
|
224
|
+
|
225
|
+
# Finally set them.
|
226
|
+
@tie_breaks = tie_breaks;
|
227
|
+
end
|
228
|
+
|
205
229
|
# Add a new player to the tournament. Must have a unique player number.
|
206
230
|
def add_player(player)
|
207
231
|
raise "invalid player" unless player.class == ICU::Player
|
208
232
|
raise "player number (#{player.num}) should be unique" if @player[player.num]
|
209
233
|
@player[player.num] = player
|
210
234
|
end
|
211
|
-
|
235
|
+
|
212
236
|
# Get a player by their number.
|
213
237
|
def player(num)
|
214
238
|
@player[num]
|
215
239
|
end
|
216
|
-
|
240
|
+
|
217
241
|
# Return an array of all players in order of their player number.
|
218
242
|
def players
|
219
243
|
@player.values.sort_by{ |p| p.num }
|
220
244
|
end
|
221
|
-
|
245
|
+
|
222
246
|
# Lookup a player in the tournament by player number, returning _nil_ if the player number does not exist.
|
223
247
|
def find_player(player)
|
224
248
|
players.find { |p| p == player }
|
225
249
|
end
|
226
|
-
|
250
|
+
|
227
251
|
# Add a result to a tournament. An exception is raised if the players referenced in the result (by number)
|
228
252
|
# do not exist in the tournament. The result, which remember is from the perspective of one of the players,
|
229
253
|
# is added to that player's results. Additionally, the reverse of the result is automatically added to the player's
|
@@ -242,10 +266,10 @@ The return value from _renumber_ is the tournament object itself.
|
|
242
266
|
@player[result.opponent].add_result(reverse)
|
243
267
|
end
|
244
268
|
end
|
245
|
-
|
269
|
+
|
246
270
|
# Rerank the tournament by score first and if necessary using a configurable tie breaker method.
|
247
|
-
def rerank
|
248
|
-
tie_break_methods, tie_break_order, tie_break_hash = tie_break_data
|
271
|
+
def rerank
|
272
|
+
tie_break_methods, tie_break_order, tie_break_hash = tie_break_data
|
249
273
|
@player.values.sort do |a,b|
|
250
274
|
cmp = 0
|
251
275
|
tie_break_methods.each do |m|
|
@@ -257,10 +281,10 @@ The return value from _renumber_ is the tournament object itself.
|
|
257
281
|
end
|
258
282
|
self
|
259
283
|
end
|
260
|
-
|
261
|
-
# Return a hash of tie break scores
|
262
|
-
def tie_break_scores
|
263
|
-
tie_break_methods, tie_break_order, tie_break_hash = tie_break_data
|
284
|
+
|
285
|
+
# Return a hash (player number to value) of tie break scores for the main method.
|
286
|
+
def tie_break_scores
|
287
|
+
tie_break_methods, tie_break_order, tie_break_hash = tie_break_data
|
264
288
|
main_method = tie_break_methods[1]
|
265
289
|
scores = Hash.new
|
266
290
|
@player.values.each { |p| scores[p.num] = tie_break_hash[main_method][p.num] }
|
@@ -269,18 +293,30 @@ The return value from _renumber_ is the tournament object itself.
|
|
269
293
|
|
270
294
|
# Renumber the players according to a given criterion.
|
271
295
|
def renumber(criterion = :rank)
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
296
|
+
if (criterion.class == Hash)
|
297
|
+
# Undocumentted feature - supply your own hash.
|
298
|
+
map = criterion
|
299
|
+
else
|
300
|
+
# Official way of reordering.
|
301
|
+
map = Hash.new
|
302
|
+
|
303
|
+
# Renumber by rank only if possible.
|
304
|
+
criterion = criterion.to_s.downcase
|
305
|
+
if criterion == 'rank'
|
306
|
+
begin check_ranks rescue criterion = 'name' end
|
307
|
+
end
|
308
|
+
|
309
|
+
# Decide how to renumber.
|
310
|
+
if criterion == 'rank'
|
311
|
+
# Renumber by rank.
|
312
|
+
@player.values.each{ |p| map[p.num] = p.rank }
|
313
|
+
elsif criterion == 'order'
|
314
|
+
# Just keep the existing numbers in order.
|
315
|
+
@player.values.sort_by{ |p| p.num }.each_with_index{ |p, i| map[p.num] = i + 1 }
|
316
|
+
else
|
317
|
+
# Renumber by name alphabetically.
|
318
|
+
@player.values.sort_by{ |p| p.name }.each_with_index{ |p, i| map[p.num] = i + 1 }
|
319
|
+
end
|
284
320
|
end
|
285
321
|
|
286
322
|
# Apply renumbering.
|
@@ -290,7 +326,8 @@ The return value from _renumber_ is the tournament object itself.
|
|
290
326
|
hash[player.num] = player
|
291
327
|
hash
|
292
328
|
end
|
293
|
-
|
329
|
+
|
330
|
+
# Return self for chaining.
|
294
331
|
self
|
295
332
|
end
|
296
333
|
|
@@ -306,10 +343,9 @@ The return value from _renumber_ is the tournament object itself.
|
|
306
343
|
end
|
307
344
|
|
308
345
|
# Raise an exception if a tournament is not valid.
|
309
|
-
# The _rerank_ option can be set to _true_
|
310
|
-
# methods to rerank the tournament if ranking is missing or inconsistent.
|
346
|
+
# The _rerank_ option can be set to _true_ rerank the tournament if ranking is missing or inconsistent.
|
311
347
|
def validate!(options={})
|
312
|
-
begin check_ranks rescue rerank
|
348
|
+
begin check_ranks rescue rerank end if options[:rerank]
|
313
349
|
check_players
|
314
350
|
check_rounds
|
315
351
|
check_dates
|
@@ -317,20 +353,21 @@ The return value from _renumber_ is the tournament object itself.
|
|
317
353
|
check_ranks(:allow_none => true)
|
318
354
|
true
|
319
355
|
end
|
320
|
-
|
356
|
+
|
321
357
|
# Convenience method to serialise the tournament into a supported format.
|
322
358
|
# Throws and exception unless the name of a supported format is supplied (e.g. _Krause_).
|
323
359
|
def serialize(format)
|
324
360
|
serializer = case format.to_s.downcase
|
325
|
-
when 'krause'
|
326
|
-
when 'foreigncsv'
|
361
|
+
when 'krause' then ICU::Tournament::Krause.new
|
362
|
+
when 'foreigncsv' then ICU::Tournament::ForeignCSV.new
|
363
|
+
when 'swissperfect' then ICU::Tournament::SwissPerfect.new
|
327
364
|
else raise "unsupported serialisation format: '#{format}'"
|
328
365
|
end
|
329
366
|
serializer.serialize(self)
|
330
367
|
end
|
331
368
|
|
332
369
|
private
|
333
|
-
|
370
|
+
|
334
371
|
# Check players.
|
335
372
|
def check_players
|
336
373
|
raise "the number of players (#{@player.size}) must be at least 2" if @player.size < 2
|
@@ -342,7 +379,7 @@ The return value from _renumber_ is the tournament object itself.
|
|
342
379
|
end
|
343
380
|
end
|
344
381
|
end
|
345
|
-
|
382
|
+
|
346
383
|
# Round should go from 1 to a maximum, there should be at least one result in every round and,
|
347
384
|
# if the number of rounds has been set, it should agree with the largest round from the results.
|
348
385
|
def check_rounds
|
@@ -372,7 +409,7 @@ The return value from _renumber_ is the tournament object itself.
|
|
372
409
|
@finish = @round_dates[-1] unless @finish
|
373
410
|
end
|
374
411
|
end
|
375
|
-
|
412
|
+
|
376
413
|
# Check teams. Either there are none or:
|
377
414
|
# * every team member is a valid player, and
|
378
415
|
# * every player is a member of exactly one team.
|
@@ -417,48 +454,33 @@ The return value from _renumber_ is the tournament object itself.
|
|
417
454
|
end
|
418
455
|
end
|
419
456
|
end
|
420
|
-
|
457
|
+
|
421
458
|
# Return an array of tie break methods and an array of tie break orders (+1 for asc, -1 for desc).
|
422
459
|
# The first and most important method is always "score", the last and least important is always "name".
|
423
|
-
def tie_break_data
|
424
|
-
|
425
|
-
tie_break_methods.map! do |m|
|
426
|
-
m = m.to_s if m.class == Symbol
|
427
|
-
m = m.downcase.gsub(/[-\s]/, '_') if m.class == String
|
428
|
-
case m
|
429
|
-
when true then 'name'
|
430
|
-
when 'sonneborn_berger' then 'neustadtl'
|
431
|
-
when 'modified_median' then 'modified'
|
432
|
-
when 'median' then 'harkness'
|
433
|
-
else m
|
434
|
-
end
|
435
|
-
end
|
436
|
-
|
437
|
-
# Check they're all valid.
|
438
|
-
tie_break_methods.each { |m| raise "invalid tie break method '#{m}'" unless m.match(/^(blacks|buchholz|harkness|modified|name|neustadtl|wins)$/) }
|
439
|
-
|
460
|
+
def tie_break_data
|
461
|
+
|
440
462
|
# Construct the arrays and hashes to be returned.
|
441
463
|
methods, order, data = Array.new, Hash.new, Hash.new
|
442
|
-
|
464
|
+
|
443
465
|
# Score is always the most important.
|
444
466
|
methods << 'score'
|
445
467
|
order['score'] = -1
|
446
|
-
|
468
|
+
|
447
469
|
# Add the configured methods.
|
448
|
-
|
470
|
+
tie_breaks.each do |m|
|
449
471
|
methods << m
|
450
472
|
order[m] = -1
|
451
473
|
end
|
452
|
-
|
474
|
+
|
453
475
|
# Name is included as the last and least important tie breaker unless it's already been added.
|
454
476
|
unless methods.include?('name')
|
455
477
|
methods << 'name'
|
456
478
|
order['name'] = +1
|
457
479
|
end
|
458
|
-
|
480
|
+
|
459
481
|
# We'll need the number of rounds.
|
460
482
|
rounds = last_round
|
461
|
-
|
483
|
+
|
462
484
|
# Pre-calculate some scores that are not in themselves tie break score
|
463
485
|
# but are needed in the calculation of some of the actual tie-break scores.
|
464
486
|
pre_calculated = Array.new
|
@@ -469,18 +491,18 @@ The return value from _renumber_ is the tournament object itself.
|
|
469
491
|
data[m][p.num] = tie_break_score(data, m, p, rounds)
|
470
492
|
end
|
471
493
|
end
|
472
|
-
|
494
|
+
|
473
495
|
# Now calculate all the other scores.
|
474
496
|
methods.each do |m|
|
475
497
|
next if pre_calculated.include?(m)
|
476
498
|
data[m] = Hash.new
|
477
499
|
@player.values.each { |p| data[m][p.num] = tie_break_score(data, m, p, rounds) }
|
478
500
|
end
|
479
|
-
|
501
|
+
|
480
502
|
# Finally, return what we calculated.
|
481
503
|
[methods, order, data]
|
482
504
|
end
|
483
|
-
|
505
|
+
|
484
506
|
# Return a tie break score for a given player and a given tie break method.
|
485
507
|
def tie_break_score(hash, method, player, rounds)
|
486
508
|
case method
|
@@ -0,0 +1,309 @@
|
|
1
|
+
require 'inifile'
|
2
|
+
require 'dbf'
|
3
|
+
|
4
|
+
module ICU
|
5
|
+
class Tournament
|
6
|
+
|
7
|
+
=begin rdoc
|
8
|
+
|
9
|
+
== SwissPerfect
|
10
|
+
|
11
|
+
This is the format produced by the Windows program, SwissPerfect[http://www.swissperfect.com/]. It consists of three
|
12
|
+
files with the same name but different endings: <em>.ini</em> for meta data such as tournament name and tie-break
|
13
|
+
rules, <em>.trn</em> for the player details such as name and ID, and <em>.sco</em> for the results. The first
|
14
|
+
file is text and the other two are in an old binary format known as <em>DBase 3</em>.
|
15
|
+
|
16
|
+
To parse such a set of files, use either the _parse_file!_ or _parse_file_ method supplying the name of any one
|
17
|
+
of the three files or just the stem name without any ending. In case of error, such as any of the files not being
|
18
|
+
found, _parse_file!_ will throw an exception while _parse_file_ will return _nil_ and record an error message.
|
19
|
+
As well as a file name or stem name, you must also supply a start date because SwissPerfect does not record this
|
20
|
+
information.
|
21
|
+
|
22
|
+
parser = ICU::Tournament::SwissPerfect.new
|
23
|
+
tournament = parser.parse_file('champs', "2010-07-03") # looks for "champs.ini", "champs.trn" and "champs.sco"
|
24
|
+
puts parser.error unless tournament
|
25
|
+
|
26
|
+
Because the data is in three parts, some of which are in a legacy binary format, serialization to this format is
|
27
|
+
not supported. Instead, a method is provided to serialize any tournament text in the format of <em>SwissPerfects</em>
|
28
|
+
text export format, an example of which is shown below.
|
29
|
+
|
30
|
+
No Name Loc Id Total 1 2 3
|
31
|
+
|
32
|
+
1 Griffiths, Ryan-Rhys 6897 3 4:W 2:W 3:W
|
33
|
+
2 Flynn, Jamie 5226 2 3:W 1:L 4:W
|
34
|
+
3 Hulleman, Leon 6409 1 2:L 4:W 1:L
|
35
|
+
4 Dunne, Thomas 10914 0 1:L 3:L 2:L
|
36
|
+
|
37
|
+
This format is important in Irish chess, as it's the format used to submit results to the <em>MicroSoft Access</em>
|
38
|
+
implementation of the ICU ratings database.
|
39
|
+
|
40
|
+
swiss_perfect = tournament.serialize('SwissPerfect')
|
41
|
+
|
42
|
+
As a side effect of serialization, the player numbers will be reordered (to ensure they range from 1 to the total
|
43
|
+
number of players) and their order in the serialized format will be by player number. If you would like to have
|
44
|
+
rank order instead, then first rank the tournament (if it isn't already ranked) and then call the _renumber_ method
|
45
|
+
without the option argument (which will renumber by rank) before serializing. For example:
|
46
|
+
|
47
|
+
swiss_perfect = tournament.rerank(:neustadtl, :buchholz).renumber.serialize('SwissPerfect)
|
48
|
+
|
49
|
+
== Todo
|
50
|
+
|
51
|
+
* Allow parsing from 1 zip file
|
52
|
+
|
53
|
+
=end
|
54
|
+
|
55
|
+
class SwissPerfect
|
56
|
+
attr_reader :error
|
57
|
+
|
58
|
+
TRN = {
|
59
|
+
:dob => "BIRTH_DATE",
|
60
|
+
:fed => "FEDER",
|
61
|
+
:first_name => "FIRSTNAME",
|
62
|
+
:gender => "SEX",
|
63
|
+
:id => ["LOC_ID", "INTL_ID"],
|
64
|
+
:last_name => "SURNAME",
|
65
|
+
:num => "ID",
|
66
|
+
:rank => "ORDER",
|
67
|
+
:rating => ["LOC_RTG", "INTL_RTG"],
|
68
|
+
} # not used: ABSENT BOARD CLUB FORB_PAIRS LATE_ENTRY LOC_RTG2 MEMO TEAM TECH_SCORE WITHDRAWAL (START_NO, BONUS used below)
|
69
|
+
|
70
|
+
SCO = %w{ROUND WHITE BLACK W_SCORE B_SCORE W_TYPE B_TYPE} # not used W_SUBSCO, B_SUBSCO
|
71
|
+
|
72
|
+
# Parse SP data returning a Tournament or raising an exception on error.
|
73
|
+
def parse_file!(file, start)
|
74
|
+
@t = Tournament.new('Dummy', start)
|
75
|
+
@bonus = {}
|
76
|
+
@start_no = {}
|
77
|
+
ini, trn, sco = get_files(file)
|
78
|
+
parse_ini(ini)
|
79
|
+
parse_trn(trn)
|
80
|
+
parse_sco(sco)
|
81
|
+
fixup
|
82
|
+
@t.validate!(:rerank => true)
|
83
|
+
@t
|
84
|
+
end
|
85
|
+
|
86
|
+
# Parse SP data returning an ICU::Tournament or a nil on failure. In the latter
|
87
|
+
# case, an error message will be available via the <em>error</em> method.
|
88
|
+
def parse_file(file, start)
|
89
|
+
begin
|
90
|
+
parse_file!(file, start)
|
91
|
+
rescue => ex
|
92
|
+
@error = ex.message
|
93
|
+
nil
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Serialise a tournament to SwissPerfect text export format.
|
98
|
+
def serialize(t)
|
99
|
+
return nil unless t.class == ICU::Tournament && t.players.size > 2;
|
100
|
+
|
101
|
+
# Ensure a nice set of numbers.
|
102
|
+
t.renumber(:order)
|
103
|
+
|
104
|
+
# Widths for the rank, name and ID and the number of rounds.
|
105
|
+
m1 = t.players.inject(2) { |l, p| p.num.to_s.length > l ? p.num.to_s.length : l }
|
106
|
+
m2 = t.players.inject(4) { |l, p| p.name.length > l ? p.name.length : l }
|
107
|
+
m3 = t.players.inject(6) { |l, p| p.id.to_s.length > l ? p.id.to_s.length : l }
|
108
|
+
rounds = t.last_round
|
109
|
+
|
110
|
+
# The header, followed by a blank line.
|
111
|
+
formats = ["%-#{m1}s", "%-#{m2}s", "%-#{m3}s", "%-5s"]
|
112
|
+
(1..rounds).each { |r| formats << "%#{m1}d " % r }
|
113
|
+
sp = formats.join("\t") % ['No', 'Name', 'Loc Id', 'Total']
|
114
|
+
sp << "\r\n\r\n"
|
115
|
+
|
116
|
+
# Adjust the round parts of the formats for players results.
|
117
|
+
(1..t.last_round).each { |r| formats[r+3] = "%#{m1+2}s" }
|
118
|
+
|
119
|
+
# Now add a line for each player.
|
120
|
+
t.players.each { |p| sp << p.to_sp_text(rounds, "#{formats.join(%Q{\t})}\r\n") }
|
121
|
+
|
122
|
+
# And return the whole lot.
|
123
|
+
sp
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
def get_files(file)
|
129
|
+
file.match(/\.zip$/i) ? get_zipped_files(file) : get_bare_files(file)
|
130
|
+
end
|
131
|
+
|
132
|
+
def get_bare_files(file)
|
133
|
+
file.sub!(/\.\w+$/, '')
|
134
|
+
%w(ini trn sco).map do |p|
|
135
|
+
q = [p, p.upcase].detect { |r| File.file? "#{file}.#{r}" }
|
136
|
+
raise "cannot find file #{file}.#{p}" unless q
|
137
|
+
"#{file}.#{q}"
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def get_zipped_files(file)
|
142
|
+
raise "get_zip_files not implemented"
|
143
|
+
end
|
144
|
+
|
145
|
+
def parse_ini(file)
|
146
|
+
begin
|
147
|
+
ini = IniFile.load(file)
|
148
|
+
rescue
|
149
|
+
raise "invalid INI file"
|
150
|
+
end
|
151
|
+
raise "invalid INI file (no sections)" if ini.sections.size == 0
|
152
|
+
%w(name arbiter rounds).each do |key|
|
153
|
+
val = (ini['Tournament Info'][key.capitalize] || '').squeeze(" ").strip
|
154
|
+
@t.send("#{key}=", val) if val.size > 0
|
155
|
+
end
|
156
|
+
@t.tie_breaks = ini['Standings']['Tie Breaks'].to_s.split(/,/).map do |tbid|
|
157
|
+
case tbid.to_i # tie break name in SwissPerfect
|
158
|
+
when 1217 then :buchholz # Buchholz
|
159
|
+
when 1218 then :harkness # Median Buchholz
|
160
|
+
when 1219 then nil # Progress - not implenented yet
|
161
|
+
when 1220 then :neustadtl # Berger
|
162
|
+
when 1221 then nil # Rating Sum - not implemented yet
|
163
|
+
when 1222 then :wins # Number of Wins
|
164
|
+
when 1223 then nil # Minor Scores - not applicable
|
165
|
+
when 1226 then nil # Brightwell - not applicable
|
166
|
+
else nil
|
167
|
+
end
|
168
|
+
end.find_all { |tb| tb }
|
169
|
+
end
|
170
|
+
|
171
|
+
def parse_trn(file)
|
172
|
+
begin
|
173
|
+
trn = DBF::Table.new(file)
|
174
|
+
rescue
|
175
|
+
raise "invalid TRN file"
|
176
|
+
end
|
177
|
+
raise "invalid TRN file (no records)" if trn.record_count == 0
|
178
|
+
trn.each do |r|
|
179
|
+
next unless r
|
180
|
+
h = trn_record_to_hash(r)
|
181
|
+
@t.add_player(ICU::Player.new(h.delete(:first_name), h.delete(:last_name), h.delete(:num), h))
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def parse_sco(file)
|
186
|
+
begin
|
187
|
+
sco = DBF::Table.new(file)
|
188
|
+
rescue
|
189
|
+
raise "invalid SCO file"
|
190
|
+
end
|
191
|
+
raise "invalid SCO file (no records)" if sco.record_count == 0
|
192
|
+
sco.each do |r|
|
193
|
+
next unless r
|
194
|
+
hs = sco_record_to_hashes(r)
|
195
|
+
hs.each { |h| @t.add_result(ICU::Result.new(h.delete(:round), h.delete(:player), h.delete(:score), h)) }
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def trn_record_to_hash(r)
|
200
|
+
@bonus[r.attributes["ID"]] = %w{BONUS MEMO}.inject(0.0){ |b,k| b > 0.0 ? b : r.attributes[k].to_f }
|
201
|
+
@start_no[r.attributes["ID"]] = r.attributes["START_NO"]
|
202
|
+
TRN.inject(Hash.new) do |hash, pair|
|
203
|
+
keys = pair[1]
|
204
|
+
keys = [keys] unless keys.class == Array
|
205
|
+
val, val2 = keys.map { |k| r.attributes[k] }
|
206
|
+
case pair[0]
|
207
|
+
when :fed then val = val && val.match(/^[A-Z]{3}$/i) ? val.upcase : nil
|
208
|
+
when :gender then val = val.to_i > 0 ? %w(M F)[val.to_i-1] : nil
|
209
|
+
when :id then val = val.to_i > 0 ? val : (val2.to_i > 0 ? val2 : nil)
|
210
|
+
when :rating then val = val.to_i > 0 ? val : (val2.to_i > 0 ? val2 : nil)
|
211
|
+
when :title then val = val.to_i > 0 ? %w(GM WGM IM WIM FM WFM)[val.to_i-1] : nil
|
212
|
+
end
|
213
|
+
hash[pair[0]] = val unless val.nil? || val == ''
|
214
|
+
hash
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def sco_record_to_hashes(record)
|
219
|
+
r, w, b, ws, bs, wt, bt = SCO.map { |k| record.attributes[k] }
|
220
|
+
hashes = []
|
221
|
+
if w > 0 && b > 0 && ws + bs == 2
|
222
|
+
hashes.push({ :round => r, :player => w, :score => %w(L D W)[ws], :opponent => b, :colour => 'W' })
|
223
|
+
hashes.last[:rateable] = false unless wt == 1 && bt == 1
|
224
|
+
else
|
225
|
+
hashes.push({ :round => r, :player => w, :score => %w(L D W)[ws], :colour => 'W' }) if w > 0
|
226
|
+
hashes.push({ :round => r, :player => b, :score => %w(L D W)[bs], :colour => 'B' }) if b > 0
|
227
|
+
end
|
228
|
+
hashes
|
229
|
+
end
|
230
|
+
|
231
|
+
def fixup
|
232
|
+
fix_number_of_rounds
|
233
|
+
fix_missing_results
|
234
|
+
fix_bonuses
|
235
|
+
fix_numbering
|
236
|
+
end
|
237
|
+
|
238
|
+
def fix_number_of_rounds
|
239
|
+
rounds = @t.last_round
|
240
|
+
@t.rounds = rounds
|
241
|
+
end
|
242
|
+
|
243
|
+
def fix_missing_results
|
244
|
+
@t.players.each { |p| @t.add_result(ICU::Result.new(1, p.num, 'L')) if p.results.size == 0 }
|
245
|
+
end
|
246
|
+
|
247
|
+
def fix_bonuses
|
248
|
+
@t.players.each do |p|
|
249
|
+
bonus = @bonus[p.num] || 0
|
250
|
+
next unless bonus > 0
|
251
|
+
|
252
|
+
# Try to distribute the bonus in half-points to rounds where the player has no result.
|
253
|
+
(1..@t.rounds).each do |r|
|
254
|
+
result = p.find_result(r)
|
255
|
+
next if result
|
256
|
+
bonus = bonus - 0.5
|
257
|
+
p.add_result(ICU::Result.new(r, p.num, 'D'))
|
258
|
+
break if bonus <= 0
|
259
|
+
end
|
260
|
+
next unless bonus > 0
|
261
|
+
|
262
|
+
# Try to distribute the bonus in half-points to rounds where the player has unrated results.
|
263
|
+
(1..@t.rounds).each do |r|
|
264
|
+
result = p.find_result(r)
|
265
|
+
next unless result
|
266
|
+
next if result.opponent
|
267
|
+
next if result.score == 'W'
|
268
|
+
bonus = bonus - 0.5
|
269
|
+
result.score = result.score == 'D' ? 'W' : 'D'
|
270
|
+
break if bonus <= 0
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
def fix_numbering
|
276
|
+
@t.renumber(@start_no)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
class Player
|
282
|
+
# Format a player's record as it would appear in an SP text export file.
|
283
|
+
def to_sp_text(rounds, format)
|
284
|
+
attrs = [num.to_s, name, id.to_s, ('%.1f' % points).sub(/\.0/, '')]
|
285
|
+
(1..rounds).each do |r|
|
286
|
+
result = find_result(r)
|
287
|
+
attrs << (result ? result.to_sp_text : " : ")
|
288
|
+
end
|
289
|
+
format % attrs
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
class Result
|
294
|
+
# Format a player's result as it would appear in an SP text export file.
|
295
|
+
def to_sp_text
|
296
|
+
sp = opponent ? opponent.to_s : '0'
|
297
|
+
sp << ':'
|
298
|
+
if rateable
|
299
|
+
sp << score
|
300
|
+
else
|
301
|
+
sp << case score
|
302
|
+
when 'W' then '+'
|
303
|
+
when 'L' then '-'
|
304
|
+
else '='
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
309
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
SAMPLES = File.dirname(__FILE__) + '/samples/sp/'
|
3
|
+
|
4
|
+
module ICU
|
5
|
+
class Tournament
|
6
|
+
def signature
|
7
|
+
[name, arbiter, rounds, start, players.size].join("|")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
class Player
|
11
|
+
def signature
|
12
|
+
[
|
13
|
+
name, id, rating, points,
|
14
|
+
results.map{ |r| r.round }.join(''),
|
15
|
+
results.map{ |r| r.score }.join(''),
|
16
|
+
results.map{ |r| r.colour || "-" }.join(''),
|
17
|
+
results.map{ |r| r.rateable ? 'T' : 'F' }.join(''),
|
18
|
+
].join("|")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module ICU
|
24
|
+
class Tournament
|
25
|
+
describe SwissPerfect do
|
26
|
+
|
27
|
+
context "Gonzaga Challengers 2010" do
|
28
|
+
|
29
|
+
before(:all) do
|
30
|
+
@p = ICU::Tournament::SwissPerfect.new
|
31
|
+
@t = @p.parse_file(SAMPLES + 'gonzaga_challengers_2010.trn', "2010-01-29")
|
32
|
+
@s = open(SAMPLES + 'gonzaga_challengers_2010.txt') { |f| f.read }
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should parse and have the right basic details" do
|
36
|
+
@p.error.should be_nil
|
37
|
+
@t.signature.should == "Gonzaga Chess Classic 2010 Challengers Section|Herbert Scarry|6|2010-01-29|56"
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should have correct details for selected players" do
|
41
|
+
@t.player(2).signature.should == "Mullooly, Neil M.|6438|1083|6.0|123456|WWWWWW|WBWBWB|TTTTTT" # winner
|
42
|
+
@t.player(4).signature.should == "Gallagher, Mark|12138|1036|4.0|123456|WLWWWL|WBWBWB|FTTTTT" # had one bye
|
43
|
+
@t.player(45).signature.should == "Catre, Loredan||507|3.5|123456|WDLWLW|BWBWBW|FTTTFT" # had two byes
|
44
|
+
@t.player(56).signature.should == "McDonnell, Cathal||498|0.0|1|L|-|F" # last
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should have the correct tie breaks" do
|
48
|
+
@t.tie_breaks.join('|').should == "buchholz|harkness"
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should serialize to the text export format" do
|
52
|
+
@t.serialize('SwissPerfect').should == @s
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context "U19 Junior Championships 2010" do
|
57
|
+
|
58
|
+
before(:all) do
|
59
|
+
@p = ICU::Tournament::SwissPerfect.new
|
60
|
+
@t = @p.parse_file(SAMPLES + 'junior_championships_u19_2010.sco', "2010-04-11")
|
61
|
+
@s = open(SAMPLES + 'junior_championships_u19_2010.txt') { |f| f.read }
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should parse and have the right basic details" do
|
65
|
+
@p.error.should be_nil
|
66
|
+
@t.signature.should == "U - 19 All Ireland||3|2010-04-11|4"
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should have correct details for selected players" do
|
70
|
+
@t.player(1).signature.should == "Griffiths, Ryan-Rhys|6897|2225|3.0|123|WWW|WWB|TTT"
|
71
|
+
@t.player(2).signature.should == "Flynn, Jamie|5226|1633|2.0|123|WLW|WBW|TTT"
|
72
|
+
@t.player(3).signature.should == "Hulleman, Leon|6409|1466|1.0|123|LWL|BBW|TTT"
|
73
|
+
@t.player(4).signature.should == "Dunne, Thomas|10914||0.0|123|LLL|BWB|TTT"
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should have the no tie breaks" do
|
77
|
+
@t.tie_breaks.join('|').should == ""
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should serialize to the text export format" do
|
81
|
+
@t.rerank.renumber.serialize('SwissPerfect').should == @s
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
context "Limerick Club Championship 2009-10" do
|
86
|
+
|
87
|
+
before(:all) do
|
88
|
+
@p = ICU::Tournament::SwissPerfect.new
|
89
|
+
@t = @p.parse_file(SAMPLES + 'LimerickClubChampionship09.ini', "2009-09-15")
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should parse and have the right basic details" do
|
93
|
+
@p.error.should be_nil
|
94
|
+
@t.signature.should == "Limerick Club Championship 2009||7|2009-09-15|19"
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should have correct details for selected players" do
|
98
|
+
@t.player(15).signature.should == "Talazec, Laurent|10692|1570|5.5|1234567|WWWDDDW|WWBWBWB|FTTTTTT" # winner
|
99
|
+
@t.player(6).signature.should == "Foenander, Phillip|7168|1434|4.0|1234567|WLWLLWW|BWBWBWB|TTFFTTT" # had some byes
|
100
|
+
@t.player(19).signature.should == "Wall, Robert|||3.0|34567|WWLWL|WWBBW|FTTTT" # didn't play 1st 2 rounds
|
101
|
+
@t.player(17).signature.should == "Freeman, Conor|||2.0|1234567|DDLWLLL|--BWBWB|FFTTTTT" # had byes and bonus (in BONUS)
|
102
|
+
@t.player(18).signature.should == "Freeman, Ruiri|||2.0|1234567|DDLLLLW|--WBBWB|FFTTTTF" # had byes and bonus (in BONUS)
|
103
|
+
@t.player(16).signature.should == "O'Connor, David|||1.0|123|WLL|WBW|FTF" # last
|
104
|
+
end
|
105
|
+
|
106
|
+
it "should have the correct tie breaks" do
|
107
|
+
@t.tie_breaks.join('|').should == "harkness|buchholz"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
context "Junior Inter Provincials U16 2010" do
|
112
|
+
|
113
|
+
before(:all) do
|
114
|
+
@p = ICU::Tournament::SwissPerfect.new
|
115
|
+
@t = @p.parse_file(SAMPLES + 'junior_provincials_u16_2010', "2010-02-02")
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should parse and have the right basic details" do
|
119
|
+
@p.error.should be_nil
|
120
|
+
@t.signature.should == "U16 Inter Provincials 2010|David B Murray|3|2010-02-02|18"
|
121
|
+
end
|
122
|
+
|
123
|
+
it "should have correct details for selected players" do
|
124
|
+
@t.player(15).signature.should == "Gupta, Radhika||1247|3.0|123|WWW|BBW|TTT" # won all his games
|
125
|
+
@t.player(18).signature.should == "Hurley, Thomas|6292|820|1.0|1|W|B|F" # scored just 1 from a bye in R1
|
126
|
+
@t.player(8).signature.should == "Berney, Mark|10328|1948|2.0|23|WW|BW|TT" # didn't play in round 1
|
127
|
+
@t.player(10).signature.should == "O'Donnell, Conor E.|10792|1073|2.0|123|LWW|WBW|TFT" # got just 1 point for a bye
|
128
|
+
end
|
129
|
+
|
130
|
+
it "should have the correct tie breaks" do
|
131
|
+
@t.tie_breaks.join('|').should == "neustadtl"
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
context "Mulcahy Cup 2010" do
|
136
|
+
|
137
|
+
before(:all) do
|
138
|
+
@p = ICU::Tournament::SwissPerfect.new
|
139
|
+
@t = @p.parse_file(SAMPLES + 'mulcahy_2010', "2010-01-15")
|
140
|
+
end
|
141
|
+
|
142
|
+
it "should parse and have the right basic details" do
|
143
|
+
@p.error.should be_nil
|
144
|
+
@t.signature.should == "Mulcahy Cup 2010|Stephen Short|6|2010-01-15|50"
|
145
|
+
end
|
146
|
+
|
147
|
+
it "should have correct details for selection of players who got bonuses (in MEMO)" do
|
148
|
+
@t.player(23).signature.should == "Long, Killian|10293|1506|2.5|123456|WDLLWL|WWBWBB|TFTTTT"
|
149
|
+
@t.player(26).signature.should == "Bradley, Michael|6756|1413|3.0|123456|DDLWWL|BWWBWW|TFTTTT"
|
150
|
+
@t.player(15).signature.should == "Twomey, Pat|1637|1656|4.5|123456|WDLWWW|WWWBWB|FFTTTT"
|
151
|
+
@t.player(46).signature.should == "O'Riordan, Pat|10696|900|2.0|123456|LDDLDD|BWBWWB|TTTTFT"
|
152
|
+
@t.player(38).signature.should == "Gill, Craig I.|10637|1081|2.0|123456|LLWDDL|BWBWWB|TTTTFT"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
data/spec/tournament_spec.rb
CHANGED
@@ -262,6 +262,37 @@ EOS
|
|
262
262
|
lambda { @t.time_control = 'abc' }.should raise_error(/invalid.*time.*control/)
|
263
263
|
end
|
264
264
|
end
|
265
|
+
|
266
|
+
context "tie breaks" do
|
267
|
+
before(:each) do
|
268
|
+
@t = Tournament.new('Edinburgh Masters', '2009-11-09')
|
269
|
+
end
|
270
|
+
|
271
|
+
it "should an empty tie breaks list by default" do
|
272
|
+
@t.tie_breaks.should be_an_instance_of(Array)
|
273
|
+
@t.tie_breaks.should be_empty
|
274
|
+
end
|
275
|
+
|
276
|
+
it "should be settable to one or more valid tie break methods" do
|
277
|
+
@t.tie_breaks = [:neustadtl]
|
278
|
+
@t.tie_breaks.join('|').should == "neustadtl"
|
279
|
+
@t.tie_breaks = [:neustadtl, :blacks]
|
280
|
+
@t.tie_breaks.join('|').should == "neustadtl|blacks"
|
281
|
+
@t.tie_breaks = ['Wins', 'Sonneborn-Berger', :harkness]
|
282
|
+
@t.tie_breaks.join('|').should == "wins|neustadtl|harkness"
|
283
|
+
@t.tie_breaks = []
|
284
|
+
@t.tie_breaks.join('|').should == ""
|
285
|
+
end
|
286
|
+
|
287
|
+
it "should rasie an error is not given an array" do
|
288
|
+
lambda { @t.tie_breaks = :neustadtl }.should raise_error(/array/i)
|
289
|
+
end
|
290
|
+
|
291
|
+
it "should rasie an error is given any invalid tie-break methods" do
|
292
|
+
lambda { @t.tie_breaks = ["My Bum"] }.should raise_error(/invalid/i)
|
293
|
+
lambda { @t.tie_breaks = [:neustadtl, "Your arse"] }.should raise_error(/invalid/i)
|
294
|
+
end
|
295
|
+
end
|
265
296
|
|
266
297
|
context "players" do
|
267
298
|
before(:each) do
|
@@ -502,6 +533,13 @@ EOS
|
|
502
533
|
@t.players.map{ |p| p.num }.join('|').should == '1|2|3'
|
503
534
|
@t.players.map{ |p| p.last_name }.join('|').should == 'Fischer|Kasparov|Orr'
|
504
535
|
end
|
536
|
+
|
537
|
+
it "should be renumberable by order" do
|
538
|
+
@t.rerank.renumber(:order)
|
539
|
+
@t.invalid.should be_false
|
540
|
+
@t.players.map{ |p| p.num }.join('|').should == '1|2|3'
|
541
|
+
@t.players.map{ |p| p.last_name }.join('|').should == 'Fischer|Orr|Kasparov'
|
542
|
+
end
|
505
543
|
end
|
506
544
|
|
507
545
|
context "reranking" do
|
@@ -545,7 +583,8 @@ EOS
|
|
545
583
|
end
|
546
584
|
|
547
585
|
it "should have correct Buchholz tie break scores" do
|
548
|
-
|
586
|
+
@t.tie_breaks = ["Buchholz"]
|
587
|
+
scores = @t.tie_break_scores
|
549
588
|
scores[1].should == 2.0
|
550
589
|
scores[2].should == 2.5
|
551
590
|
scores[3].should == 7.0
|
@@ -557,7 +596,8 @@ EOS
|
|
557
596
|
it "Buchholz should be sensitive to unplayed games" do
|
558
597
|
@t.player(1).find_result(1).opponent = nil
|
559
598
|
@t.player(6).find_result(1).opponent = nil
|
560
|
-
|
599
|
+
@t.tie_breaks = ["Buchholz"]
|
600
|
+
scores = @t.tie_break_scores
|
561
601
|
scores[1].should == 1.5 # 0.5 from Orr changed to 0
|
562
602
|
scores[2].should == 2.5 # didn't play Fischer or Orr so unaffected
|
563
603
|
scores[3].should == 6.5 # 3 from Fischer's changed to 2.5
|
@@ -567,7 +607,8 @@ EOS
|
|
567
607
|
end
|
568
608
|
|
569
609
|
it "should have correct Neustadtl tie break scores" do
|
570
|
-
|
610
|
+
@t.tie_breaks = [:neustadtl]
|
611
|
+
scores = @t.tie_break_scores
|
571
612
|
scores[1].should == 2.0
|
572
613
|
scores[2].should == 2.5
|
573
614
|
scores[3].should == 1.0
|
@@ -579,7 +620,8 @@ EOS
|
|
579
620
|
it "Neustadtl should be sensitive to unplayed games" do
|
580
621
|
@t.player(1).find_result(1).opponent = nil
|
581
622
|
@t.player(6).find_result(1).opponent = nil
|
582
|
-
|
623
|
+
@t.tie_breaks = ["Neustadtl"]
|
624
|
+
scores = @t.tie_break_scores
|
583
625
|
scores[1].should == 1.5 # 0.5 from Orr changed to 0
|
584
626
|
scores[2].should == 2.5 # didn't play Fischer or Orr so unaffected
|
585
627
|
scores[3].should == 1.0 # win against Minnie unaffected
|
@@ -589,7 +631,8 @@ EOS
|
|
589
631
|
end
|
590
632
|
|
591
633
|
it "should have correct Harkness tie break scores" do
|
592
|
-
|
634
|
+
@t.tie_breaks = ['harkness']
|
635
|
+
scores = @t.tie_break_scores
|
593
636
|
scores[1].should == 0.5
|
594
637
|
scores[2].should == 1.0
|
595
638
|
scores[3].should == 3.0
|
@@ -599,7 +642,8 @@ EOS
|
|
599
642
|
end
|
600
643
|
|
601
644
|
it "should have correct Modified Median tie break scores" do
|
602
|
-
|
645
|
+
@t.tie_breaks = ['Modified Median']
|
646
|
+
scores = @t.tie_break_scores
|
603
647
|
scores[1].should == 1.5
|
604
648
|
scores[2].should == 2.0
|
605
649
|
scores[3].should == 4.0
|
@@ -609,7 +653,8 @@ EOS
|
|
609
653
|
end
|
610
654
|
|
611
655
|
it "should have correct tie break scores for number of blacks" do
|
612
|
-
|
656
|
+
@t.tie_breaks = ['Blacks']
|
657
|
+
scores = @t.tie_break_scores
|
613
658
|
scores[3].should == 0
|
614
659
|
scores[4].should == 2
|
615
660
|
end
|
@@ -617,13 +662,15 @@ EOS
|
|
617
662
|
it "number of blacks should should be sensitive to unplayed games" do
|
618
663
|
@t.player(2).find_result(1).opponent = nil
|
619
664
|
@t.player(4).find_result(1).opponent = nil
|
620
|
-
|
665
|
+
@t.tie_breaks = [:blacks]
|
666
|
+
scores = @t.tie_break_scores
|
621
667
|
scores[3].should == 0
|
622
668
|
scores[4].should == 1
|
623
669
|
end
|
624
670
|
|
625
671
|
it "should have correct tie break scores for number of wins" do
|
626
|
-
|
672
|
+
@t.tie_breaks = [:wins]
|
673
|
+
scores = @t.tie_break_scores
|
627
674
|
scores[1].should == 3
|
628
675
|
scores[6].should == 0
|
629
676
|
end
|
@@ -631,7 +678,8 @@ EOS
|
|
631
678
|
it "number of wins should should be sensitive to unplayed games" do
|
632
679
|
@t.player(1).find_result(1).opponent = nil
|
633
680
|
@t.player(6).find_result(1).opponent = nil
|
634
|
-
|
681
|
+
@t.tie_breaks = ['WINS']
|
682
|
+
scores = @t.tie_break_scores
|
635
683
|
scores[1].should == 2
|
636
684
|
scores[6].should == 0
|
637
685
|
end
|
@@ -647,7 +695,8 @@ EOS
|
|
647
695
|
end
|
648
696
|
|
649
697
|
it "should be configurable to use Buchholz" do
|
650
|
-
@t.
|
698
|
+
@t.tie_breaks = ['Buchholz']
|
699
|
+
@t.rerank
|
651
700
|
@t.player(2).rank.should == 1 # 3.0/2.5
|
652
701
|
@t.player(1).rank.should == 2 # 3.0/2.0
|
653
702
|
@t.player(3).rank.should == 3 # 1.0/7.0
|
@@ -657,7 +706,8 @@ EOS
|
|
657
706
|
end
|
658
707
|
|
659
708
|
it "should be configurable to use Neustadtl" do
|
660
|
-
@t.
|
709
|
+
@t.tie_breaks = [:neustadtl]
|
710
|
+
@t.rerank
|
661
711
|
@t.player(2).rank.should == 1 # 3.0/2.5
|
662
712
|
@t.player(1).rank.should == 2 # 3.0/2.0
|
663
713
|
@t.player(3).rank.should == 3 # 1.0/1.0
|
@@ -667,7 +717,8 @@ EOS
|
|
667
717
|
end
|
668
718
|
|
669
719
|
it "should be configurable to use number of blacks" do
|
670
|
-
@t.
|
720
|
+
@t.tie_breaks = [:blacks]
|
721
|
+
@t.rerank
|
671
722
|
@t.player(2).rank.should == 1 # 3.0/2
|
672
723
|
@t.player(1).rank.should == 2 # 3.0/1
|
673
724
|
@t.player(4).rank.should == 3 # 1.0/2
|
@@ -677,7 +728,8 @@ EOS
|
|
677
728
|
end
|
678
729
|
|
679
730
|
it "should be configurable to use number of wins" do
|
680
|
-
@t.
|
731
|
+
@t.tie_breaks = [:wins]
|
732
|
+
@t.rerank
|
681
733
|
@t.player(1).rank.should == 1 # 3.0/3/"Fi"
|
682
734
|
@t.player(2).rank.should == 2 # 3.0/3/"Ka"
|
683
735
|
@t.player(3).rank.should == 3 # 1.0/1/"Mic"
|
@@ -687,12 +739,14 @@ EOS
|
|
687
739
|
end
|
688
740
|
|
689
741
|
it "should exhibit equivalence between Neustadtl and Sonneborn-Berger" do
|
690
|
-
@t.
|
742
|
+
@t.tie_breaks = ['Sonneborn-Berger']
|
743
|
+
@t.rerank
|
691
744
|
(1..6).inject(''){ |t,r| t << @t.player(r).rank.to_s }.should == '213465'
|
692
745
|
end
|
693
746
|
|
694
747
|
it "should be able to use more than one method" do
|
695
|
-
@t.
|
748
|
+
@t.tie_breaks = [:neustadtl, :buchholz]
|
749
|
+
@t.rerank
|
696
750
|
@t.player(2).rank.should == 1 # 3.0/2.5
|
697
751
|
@t.player(1).rank.should == 2 # 3.0/2.0
|
698
752
|
@t.player(3).rank.should == 3 # 1.0/1.0
|
@@ -700,17 +754,10 @@ EOS
|
|
700
754
|
@t.player(5).rank.should == 5 # 0.5/0.25/6.5
|
701
755
|
@t.player(6).rank.should == 6 # 0.5/0.25/4.5
|
702
756
|
end
|
703
|
-
|
704
|
-
it "should throw exception on invalid tie break method" do
|
705
|
-
lambda { @t.rerank(:no_such_tie_break_method) }.should raise_error(/invalid.*method/)
|
706
|
-
end
|
707
|
-
|
708
|
-
it "should throw exception on invalid tie break method via validation" do
|
709
|
-
lambda { @t.validate!(:rerank => :stupid_tie_break_method) }.should raise_error(/invalid.*method/)
|
710
|
-
end
|
711
757
|
|
712
758
|
it "should be possible as a side effect of validation" do
|
713
|
-
@t.
|
759
|
+
@t.tie_breaks = [:buchholz]
|
760
|
+
@t.invalid(:rerank => true).should be_false
|
714
761
|
@t.player(2).rank.should == 1 # 3/3
|
715
762
|
@t.player(1).rank.should == 2 # 3/2
|
716
763
|
@t.player(3).rank.should == 3 # 1/7
|
@@ -720,7 +767,8 @@ EOS
|
|
720
767
|
end
|
721
768
|
|
722
769
|
it "should be possible as a side effect of validation with multiple tie break methods" do
|
723
|
-
@t.
|
770
|
+
@t.tie_breaks = [:neustadtl, :buchholz]
|
771
|
+
@t.invalid(:rerank => true).should be_false
|
724
772
|
@t.player(2).rank.should == 1 # 3/3
|
725
773
|
@t.player(1).rank.should == 2 # 3/2
|
726
774
|
@t.player(3).rank.should == 3 # 1/7
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: icu_tournament
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mark Orr
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2010-
|
12
|
+
date: 2010-05-14 00:00:00 +01:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -22,6 +22,26 @@ dependencies:
|
|
22
22
|
- !ruby/object:Gem::Version
|
23
23
|
version: 1.4.0
|
24
24
|
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: inifile
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.3.0
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: dbf
|
37
|
+
type: :runtime
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 1.2.5
|
44
|
+
version:
|
25
45
|
- !ruby/object:Gem::Dependency
|
26
46
|
name: rspec
|
27
47
|
type: :development
|
@@ -50,6 +70,7 @@ files:
|
|
50
70
|
- lib/icu_tournament/tournament.rb
|
51
71
|
- lib/icu_tournament/tournament_fcsv.rb
|
52
72
|
- lib/icu_tournament/tournament_krause.rb
|
73
|
+
- lib/icu_tournament/tournament_sp.rb
|
53
74
|
- lib/icu_tournament/util.rb
|
54
75
|
- lib/icu_tournament/version.rb
|
55
76
|
- lib/icu_tournament.rb
|
@@ -61,6 +82,7 @@ files:
|
|
61
82
|
- spec/team_spec.rb
|
62
83
|
- spec/tournament_fcsv_spec.rb
|
63
84
|
- spec/tournament_krause_spec.rb
|
85
|
+
- spec/tournament_sp_spec.rb
|
64
86
|
- spec/tournament_spec.rb
|
65
87
|
- spec/util_spec.rb
|
66
88
|
- LICENCE
|
@@ -102,5 +124,6 @@ test_files:
|
|
102
124
|
- spec/team_spec.rb
|
103
125
|
- spec/tournament_fcsv_spec.rb
|
104
126
|
- spec/tournament_krause_spec.rb
|
127
|
+
- spec/tournament_sp_spec.rb
|
105
128
|
- spec/tournament_spec.rb
|
106
129
|
- spec/util_spec.rb
|