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.
@@ -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 upcased.
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 << result
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
- They players in a tournament can be ranked by calling the _rerank_ method directly.
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
- a list of methods (strings or symbols) in order of precedence to the _rerank_ method or for the _rerank_
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.rerank('Sonneborn-Berger')
96
- t.rerank(:buchholz, :neustadtl, :blacks, :wins)
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 the default and is the same as calling _rerank_ with no options or setting the _rerank_ option to true
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
- The return value from _rerank_ is the tournament object itself.
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
- go up to the total number of players use the _renumber_ method. This method takes one optional
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(*tie_break_methods)
248
- tie_break_methods, tie_break_order, tie_break_hash = tie_break_data(tie_break_methods.flatten)
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 (player number to value).
262
- def tie_break_scores(*tie_break_methods)
263
- tie_break_methods, tie_break_order, tie_break_hash = tie_break_data(tie_break_methods)
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
- map = Hash.new
273
-
274
- # Decide how to renumber.
275
- criterion = criterion.to_s.downcase
276
- if criterion.match('rank')
277
- # Renumber by rank if possible.
278
- begin check_ranks rescue criterion = 'name' end
279
- @player.values.each{ |p| map[p.num] = p.rank }
280
- end
281
- if !criterion.match('rank')
282
- # Renumber by name alphabetically.
283
- @player.values.sort_by{ |p| p.name }.each_with_index{ |p, i| map[p.num] = i + 1 }
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_ or one of the valid tie-break
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(options[:rerank]) end if options[: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' then ICU::Tournament::Krause.new
326
- when 'foreigncsv' then ICU::Tournament::ForeignCSV.new
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(tie_break_methods)
424
- # Canonicalise the tie break method names.
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
- tie_break_methods.each do |m|
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
@@ -1,5 +1,5 @@
1
1
  module ICU
2
2
  class Tournament
3
- VERSION = "0.9.6"
3
+ VERSION = "1.0.0"
4
4
  end
5
5
  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
@@ -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
- scores = @t.tie_break_scores("Buchholz")
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
- scores = @t.tie_break_scores("Buchholz")
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
- scores = @t.tie_break_scores(:neustadtl)
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
- scores = @t.tie_break_scores("Neustadtl")
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
- scores = @t.tie_break_scores('harkness')
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
- scores = @t.tie_break_scores('Modified Median')
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
- scores = @t.tie_break_scores('Blacks')
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
- scores = @t.tie_break_scores(:blacks)
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
- scores = @t.tie_break_scores(:wins)
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
- scores = @t.tie_break_scores('WINS')
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.rerank('Buchholz')
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.rerank(:neustadtl)
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.rerank(:blacks)
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.rerank(:wins)
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.rerank('Sonneborn-Berger')
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.rerank(:neustadtl, :buchholz)
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.invalid(:rerank => :buchholz).should be_false
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.invalid(:rerank => [:neustadtl, :buchholz]).should be_false
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.9.6
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-04-18 00:00:00 +01:00
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