icu_tournament 0.9.6 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|