icu_tournament 1.4.3 → 1.5.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.
@@ -181,11 +181,19 @@ module ICU
181
181
  end
182
182
  end
183
183
 
184
- # Lookup a result by round number.
184
+ # Lookup a result by round number and return it (or nil if there is no such result).
185
185
  def find_result(round)
186
186
  @results.find { |r| r.round == round }
187
187
  end
188
188
 
189
+ # Lookup a result by round number, remove it and return it (or return nil if there is no such result).
190
+ def remove_result(round)
191
+ result = find_result(round)
192
+ return unless result
193
+ @results.delete(result)
194
+ result
195
+ end
196
+
189
197
  # Return the player's total points.
190
198
  def points
191
199
  @results.inject(0.0) { |t, r| t += r.points }
@@ -66,6 +66,19 @@ module ICU
66
66
  #
67
67
  # The _points_ read-only accessor always returns a floating point number: either 0.0, 0.5 or 1.0.
68
68
  #
69
+ # Two results are <em>eql?</em> if all there attributes are the same, unless exceptions are specified.
70
+ #
71
+ # r = ICU::Result.new(1, 1, 'W', :opponent => 2)
72
+ # r1 = ICU::Result.new(1, 1, 'W', :opponent => 2)
73
+ # r2 = ICU::Result.new(1, 1, 'W', :opponent => 2, :rateable => false)
74
+ # r3 = ICU::Result.new(1, 1, 'L', :opponent => 2, :rateable => false)
75
+ #
76
+ # r.eql?(r1) # => true
77
+ # r.eql?(r2) # => false
78
+ # r.eql?(r3) # => false
79
+ # r.eql?(r2, :except => :rateable) # => true
80
+ # r.eql?(r3, :except => [:rateable, :score]) # => true
81
+ #
69
82
  class Result
70
83
  extend ICU::Accessor
71
84
  attr_positive :round
@@ -167,20 +180,22 @@ module ICU
167
180
  "R#{@round}P#{@player}O#{@opponent || '-'}#{@score}#{@colour || '-'}#{@rateable ? 'R' : 'U'}"
168
181
  end
169
182
 
170
- # Loose equality. True if the round, player and opponent numbers, colour and score all match.
171
- def ==(other)
183
+ # Equality. True if all attributes equal, exceptions allowed.
184
+ def eql?(other, opt={})
185
+ return true if equal?(other)
172
186
  return unless other.is_a? Result
173
- [:round, :player, :opponent, :colour, :score].each do |m|
174
- return false unless self.send(m) == other.send(m)
187
+ except = Hash.new
188
+ if opt[:except]
189
+ if opt[:except].is_a?(Array)
190
+ opt[:except].each { |x| except[x.to_sym] = true }
191
+ else
192
+ except[opt[:except].to_sym] = true
193
+ end
194
+ end
195
+ [:round, :player, :opponent, :colour, :score, :rateable].each do |m|
196
+ return false unless except[m] || self.send(m) == other.send(m)
175
197
  end
176
198
  true
177
199
  end
178
-
179
- # Strict equality. True if the there's loose equality and also the rateablity is the same.
180
- def eql?(other)
181
- return true if equal?(other)
182
- return false unless self == other
183
- self.rateable == other.rateable
184
- end
185
200
  end
186
201
  end
@@ -55,6 +55,22 @@ module ICU
55
55
  # t.add_result(ICU::Result.new(3, 20, 'L', :opponent => 10, :colour => 'W'))
56
56
  # t.add_result(ICU::Result.new(3, 10, 'D', :opponent => 20, :colour => 'B')) # would raise an exception
57
57
  #
58
+ # == Asymmetric Scores
59
+ #
60
+ # There is one exception to the rule that two corresponding results must be consistent:
61
+ # if both results are unrateable then the two scores need not sum to 1. The commonest case
62
+ # this caters for is probably that of a double default. To create such asymmetric results
63
+ # you must add the result from both players' perspectives. For example:
64
+ #
65
+ # t.add_result(ICU::Result.new(3, 20, 'L', :opponent => 10, :rateable => false))
66
+ # t.add_result(ICU::Result.new(3, 10, 'L', :opponent => 20, :rateable => false))
67
+ #
68
+ # After the first _add_result_ the two results are, as usual, consistent (in particular, the loss for player 20
69
+ # is balanced by a win for player 10). However, the second _add_result_, which asserts player 10 lost, does not cause
70
+ # an exception. It would have done if the results had been rateable but, because they are not, the scores are
71
+ # allowed to add up to something other than 1.0 (in this case, zero) and the effect of the second call to _add_result_
72
+ # is merely to adjust the score of player 10 from a win to a loss (while maintaining the loss for player 20).
73
+ #
58
74
  # See ICU::Player and ICU::Result for more details about players and results.
59
75
  #
60
76
  # == Validation
@@ -67,11 +83,11 @@ module ICU
67
83
  # Validations checks that:
68
84
  #
69
85
  # * there are at least two players
70
- # * every player has a least one result
71
- # * the result round numbers are consistent (no more than one game per player per round)
86
+ # * result round numbers are consistent (no more than one game per player per round)
87
+ # * corresponding results are consistent (although they may have asymmetric scores if unrateable, as previously desribed)
72
88
  # * the tournament dates (start, finish, round dates), if there are any, are consistent
73
- # * the player ranks are consistent with their scores
74
- # * there are no players with duplicate ICU IDs or duplicate FIDE IDs
89
+ # * player ranks are consistent with their scores
90
+ # * there are no players with duplicate \ICU IDs or duplicate \FIDE IDs
75
91
  #
76
92
  # Side effects of calling <em>validate!</em> or _invalid_ include:
77
93
  #
@@ -87,7 +103,7 @@ module ICU
87
103
  #
88
104
  # t.validate!(:type => 'ForeignCSV')
89
105
  #
90
- # which, amongst other tests, checks that there is at least one player with an ICU number and
106
+ # which, amongst other tests, checks that there is at least one player with an \ICU number and
91
107
  # that all such players have a least one game against a FIDE rated opponent. This is an example
92
108
  # of a specialized check that is only appropriate for a particular serializer. If it raises an
93
109
  # exception then the tournament cannot be serialized that way.
@@ -278,6 +294,7 @@ module ICU
278
294
  raise "invalid result" unless result.class == ICU::Result
279
295
  raise "result round number (#{result.round}) inconsistent with number of tournament rounds" if @rounds && result.round > @rounds
280
296
  raise "player number (#{result.player}) does not exist" unless @player[result.player]
297
+ return if add_asymmetric_result?(result)
281
298
  @player[result.player].add_result(result)
282
299
  if result.opponent
283
300
  raise "opponent number (#{result.opponent}) does not exist" unless @player[result.opponent]
@@ -418,14 +435,15 @@ module ICU
418
435
  raise "duplicate FIDE IDs, players #{p.num} and #{fide_ids[p.fide_id]}" if fide_ids[p.fide_id]
419
436
  fide_ids[p.fide_id] = num
420
437
  end
421
- raise "player #{num} has no results" if p.results.size == 0
438
+ return if p.results.size == 0
422
439
  p.results.each do |r|
423
440
  next unless r.opponent
424
441
  opponent = @player[r.opponent]
425
442
  raise "opponent #{r.opponent} of player #{num} is not in the tournament" unless opponent
426
443
  o = opponent.find_result(r.round)
427
444
  raise "opponent #{r.opponent} of player #{num} has no result in round #{r.round}" unless o
428
- raise "opponent's result (#{o.inspect}) is not reverse of player's (#{r.inspect})" unless o.reverse.eql?(r)
445
+ score = r.rateable || o.rateable ? [] : [:score]
446
+ raise "opponent's result (#{o.inspect}) is not reverse of player's (#{r.inspect})" unless o.reverse.eql?(r, :except => score)
429
447
  end
430
448
  end
431
449
  end
@@ -587,5 +605,29 @@ module ICU
587
605
  else player.name
588
606
  end
589
607
  end
608
+
609
+ # Detect when an asymmetric result is about to be added, make the appropriate adjustment and return true.
610
+ # The conditions for an asymric result are: the player's result already exists, the opponent's result
611
+ # already exists, both results are unrateable and the reverse of one result is equal to the other, apart
612
+ # from score. In this case all we do update score of the player's result, thus allowing two results whose
613
+ # total score does not add to 1.
614
+ def add_asymmetric_result?(result)
615
+ return false if result.rateable
616
+
617
+ plr = @player[result.player]
618
+ opp = @player[result.opponent]
619
+ return false unless plr && opp
620
+
621
+ plr_result = plr.find_result(result.round)
622
+ opp_result = opp.find_result(result.round)
623
+ return false unless plr_result && opp_result
624
+ return false if plr_result.rateable || opp_result.rateable
625
+
626
+ reversed = plr_result.reverse
627
+ return false unless reversed && reversed.eql?(opp_result, :except => :score)
628
+
629
+ plr_result.score = result.score
630
+ true
631
+ end
590
632
  end
591
633
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ICU
4
4
  class Tournament
5
- VERSION = "1.4.3"
5
+ VERSION = "1.5.0"
6
6
  end
7
7
  end
data/spec/player_spec.rb CHANGED
@@ -247,6 +247,22 @@ module ICU
247
247
  end
248
248
  end
249
249
 
250
+ context "removing a result" do
251
+ before(:all) do
252
+ @p = Player.new('Mark', 'Orr', 1)
253
+ @p.add_result(Result.new(1, 1, 'W', :opponent => 37, :score => 'W', :colour => 'W'))
254
+ @p.add_result(Result.new(2, 1, 'W', :opponent => 13, :score => 'W', :colour => 'B'))
255
+ @p.add_result(Result.new(3, 1, 'W', :opponent => 7, :score => 'D', :colour => 'W'))
256
+ end
257
+
258
+ it "should find and remove a result by round number" do
259
+ result = @p.remove_result(1)
260
+ result.inspect.should == "R1P1O37WWR"
261
+ @p.results.size.should == 2
262
+ @p.results.map(&:round).join("|").should == "2|3"
263
+ end
264
+ end
265
+
250
266
  context "merge" do
251
267
  before(:each) do
252
268
  @p1 = Player.new('Mark', 'Orr', 1, :id => 1350)
data/spec/result_spec.rb CHANGED
@@ -8,7 +8,7 @@ module ICU
8
8
  lambda { Result.new(3, 5, 'W', :opponent => 11, :colour => 'W') }.should_not raise_error
9
9
  end
10
10
  end
11
-
11
+
12
12
  context "round number" do
13
13
  it "should be a positive integer" do
14
14
  lambda { Result.new(-2, 2, 0) }.should raise_error(/invalid positive integer/)
@@ -17,7 +17,7 @@ module ICU
17
17
  Result.new(' 3 ', 2, 0).round.should == 3
18
18
  end
19
19
  end
20
-
20
+
21
21
  context "player number" do
22
22
  it "should be an integer" do
23
23
  lambda { Result.new(1, ' ', 0) }.should raise_error(/invalid integer/)
@@ -27,7 +27,7 @@ module ICU
27
27
  Result.new(1, " 9 ", 0).player.should == 9
28
28
  end
29
29
  end
30
-
30
+
31
31
  context "score" do
32
32
  [1, 1.0, 'W', 'w', '+'].each do |score|
33
33
  it "should be 'W' for #{score}, #{score.class}" do
@@ -55,7 +55,7 @@ module ICU
55
55
  Result.new(1, 2, 'D').points.should == 0.5
56
56
  end
57
57
  end
58
-
58
+
59
59
  context "colour" do
60
60
  it "should be 'W' or 'B' or nil (unknown)" do
61
61
  Result.new(4, 1, 0).colour.should be_nil
@@ -66,7 +66,7 @@ module ICU
66
66
  lambda { Result.new(4, 1, 0, :colour => 'red') }.should raise_error(/invalid colour/)
67
67
  end
68
68
  end
69
-
69
+
70
70
  context "opponent number" do
71
71
  it "should be nil (the default) or an integer" do
72
72
  Result.new(4, 1, 0).opponent.should be_nil
@@ -78,27 +78,27 @@ module ICU
78
78
  Result.new(4, 1, 0, :opponent => ' 10 ').opponent.should == 10
79
79
  lambda { Result.new(4, 1, 0, :opponent => 'X') }.should raise_error(/invalid opponent number/)
80
80
  end
81
-
81
+
82
82
  it "should be different to player number" do
83
83
  lambda { Result.new(4, 1, 0, :opponent => 1) }.should raise_error(/opponent .* player .* different/)
84
84
  end
85
85
  end
86
-
86
+
87
87
  context "rateable flag" do
88
88
  it "should default to false if there is no opponent" do
89
89
  Result.new(4, 1, 0).rateable.should be_false
90
90
  end
91
-
91
+
92
92
  it "should default to true if there is an opponent" do
93
93
  Result.new(4, 1, 0, :opponent => 10).rateable.should be_true
94
94
  end
95
-
95
+
96
96
  it "should change if an opponent is added" do
97
97
  r = Result.new(4, 1, 0)
98
98
  r.opponent = 5;
99
99
  r.rateable.should be_true
100
100
  end
101
-
101
+
102
102
  it "should be settable to false from the constructor" do
103
103
  Result.new(4, 1, 0, :opponent => 10, :rateable => false).rateable.should be_false
104
104
  end
@@ -108,43 +108,43 @@ module ICU
108
108
  r.rateable= false
109
109
  r.rateable.should be_false
110
110
  end
111
-
111
+
112
112
  it "should not be settable to true if there is no opponent" do
113
113
  r = Result.new(4, 1, 0)
114
114
  r.rateable= true
115
115
  r.rateable.should be_false
116
116
  end
117
117
  end
118
-
118
+
119
119
  context "reversed result" do
120
120
  it "should be nil if there is no opponent" do
121
121
  Result.new(4, 1, 0).reverse.should be_nil
122
122
  end
123
-
123
+
124
124
  it "should have player and opponent swapped" do
125
125
  r = Result.new(4, 1, 0, :opponent => 2).reverse
126
126
  r.player.should == 2
127
127
  r.opponent.should == 1
128
128
  end
129
-
129
+
130
130
  it "should have reversed result" do
131
131
  Result.new(4, 1, 0, :opponent => 2).reverse.score.should == 'W'
132
132
  Result.new(4, 1, 1, :opponent => 2).reverse.score.should == 'L'
133
133
  Result.new(4, 1, '=', :opponent => 2).reverse.score.should == 'D'
134
134
  end
135
-
135
+
136
136
  it "should preserve rateability" do
137
137
  Result.new(4, 1, 0, :opponent => 2).reverse.rateable.should be_true
138
138
  Result.new(4, 1, 0, :opponent => 2, :rateable => false).reverse.rateable.should be_false
139
139
  end
140
140
  end
141
-
141
+
142
142
  context "renumber the player numbers in a result" do
143
143
  before(:each) do
144
144
  @r1 = Result.new(1, 4, 0)
145
145
  @r2 = Result.new(2, 3, 1, :opponent => 4, :color => 'B')
146
146
  end
147
-
147
+
148
148
  it "should renumber successfully if the map has the relevant player numbers" do
149
149
  map = { 4 => 1, 3 => 2 }
150
150
  @r1.renumber(map).player.should == 1
@@ -152,50 +152,53 @@ module ICU
152
152
  @r1.opponent.should be_nil
153
153
  @r2.opponent.should == 1
154
154
  end
155
-
155
+
156
156
  it "should raise exception if a player number is not in the map" do
157
157
  lambda { @r1.renumber({ 5 => 1, 3 => 2 }) }.should raise_error(/player.*4.*not found/)
158
158
  end
159
159
  end
160
-
161
- context "loose equality" do
160
+
161
+ context "equality" do
162
162
  before(:each) do
163
+ @r = Result.new(1, 1, 0, :opponent => 2, :colour => 'W')
163
164
  @r1 = Result.new(1, 1, 0, :opponent => 2, :colour => 'W')
164
- @r2 = Result.new(1, 1, 0, :opponent => 2, :colour => 'W')
165
+ @r2 = Result.new(1, 1, 0, :opponent => 2, :colour => 'W', :rateable => false)
165
166
  @r3 = Result.new(2, 1, 0, :opponent => 2, :colour => 'W')
166
- @r4 = Result.new(1, 3, 0, :opponent => 2, :colour => 'W')
167
- @r5 = Result.new(1, 1, 1, :opponent => 2, :colour => 'W')
168
- @r6 = Result.new(1, 1, 0, :opponent => 3, :colour => 'W')
169
- @r7 = Result.new(1, 1, 0, :opponent => 2, :colour => 'B')
170
- end
171
-
172
- it "should be equal if the round, player numbers, result and colour all match" do
173
- (@r1 == @r1).should be_true
174
- (@r1 == @r2).should be_true
175
- end
176
-
177
- it "should not be equal if the round, player numbers, result or colour do not match" do
178
- (@r1 == @r3).should be_false
179
- (@r1 == @r4).should be_false
180
- (@r1 == @r5).should be_false
181
- (@r1 == @r6).should be_false
182
- (@r1 == @r7).should be_false
167
+ @r4 = Result.new(1, 1, 0, :opponent => 2, :colour => 'B')
168
+ @r5 = Result.new(2, 1, 1, :opponent => 3, :colour => 'B')
169
+ end
170
+
171
+ it "should only be equal if everything is the same" do
172
+ @r.eql?(@r).should be_true
173
+ @r.eql?(@r1).should be_true
174
+ @r.eql?(@r2).should be_false
175
+ @r.eql?(@r3).should be_false
176
+ @r.eql?(@r4).should be_false
177
+ @r.eql?(@r5).should be_false
178
+ end
179
+
180
+ it "exceptions are allowed" do
181
+ @r.eql?(@r2, :except => :rateable).should be_true
182
+ @r.eql?(@r3, :except => "round").should be_true
183
+ @r.eql?(@r4, :except => :colour).should be_true
184
+ @r.eql?(@r5, :except => [:colour, :round, :score, "opponent"]).should be_true
183
185
  end
184
186
  end
185
-
186
- context "strict equality" do
187
+
188
+ context "equality documentation example" do
187
189
  before(:each) do
188
- @r1 = Result.new(1, 1, 0, :opponent => 2, :colour => 'W')
189
- @r2 = Result.new(1, 1, 0, :opponent => 2, :colour => 'W')
190
- @r3 = Result.new(1, 1, 0, :opponent => 2, :colour => 'W', :rateable => false)
191
- @r4 = Result.new(2, 1, 0, :opponent => 2, :colour => 'W')
190
+ @r = ICU::Result.new(1, 1, 'W', :opponent => 2)
191
+ @r1 = ICU::Result.new(1, 1, 'W', :opponent => 2)
192
+ @r2 = ICU::Result.new(1, 1, 'W', :opponent => 2, :rateable => false)
193
+ @r3 = ICU::Result.new(1, 1, 'L', :opponent => 2, :rateable => false)
192
194
  end
193
-
194
- it "should only be equal if everything is the same" do
195
- @r1.eql?(@r1).should be_true
196
- @r1.eql?(@r2).should be_true
197
- @r1.eql?(@r3).should be_false
198
- @r1.eql?(@r4).should be_false
195
+
196
+ it "should be correct" do
197
+ @r.eql?(@r1).should be_true
198
+ @r.eql?(@r2).should be_false
199
+ @r.eql?(@r3).should be_false
200
+ @r.eql?(@r2, :except => :rateable).should be_true
201
+ @r.eql?(@r3, :except => [:rateable, :score]).should be_true
199
202
  end
200
203
  end
201
204
  end
@@ -32,8 +32,9 @@ module ICU
32
32
  042 2009-11-09
33
33
  001 10 Fischer,Bobby 1.5 1 30 w = 20 b 1
34
34
  001 20 Kasparov,Garry 1.0 2 30 b 1 10 w 0
35
- 001 30 Orr,Mark 0.5 3 10 b = 20 w 0
35
+ 001 30 Orr,Mark 0.5 3 10 b = 20 w 0 #
36
36
  EOS
37
+ @s.sub!(/#/, '')
37
38
  end
38
39
 
39
40
  it "should serialize to Krause" do
@@ -324,7 +325,7 @@ EOS
324
325
  it "can be added one at a time" do
325
326
  @t.add_result(Result.new(1, 1, 'W', :opponent => 2))
326
327
  @t.add_result(Result.new(2, 2, 'D', :opponent => 3))
327
- @t.add_result(Result.new(3, 3, 'L', :opponent => 1))
328
+ @t.add_result(Result.new(3, 3, 'L', :opponent => 1, :rateable => false))
328
329
  @mark.results.size.should == 2
329
330
  @mark.points.should == 2.0
330
331
  @gary.results.size.should == 2
@@ -333,10 +334,11 @@ EOS
333
334
  @boby.points.should == 0.5
334
335
  end
335
336
 
336
- it "asymmetric results cannot be added" do
337
+ it "results with asymmetric scores cannot be added unless both results are unrateable" do
337
338
  @t.add_result(Result.new(1, 1, 'W', :opponent => 2))
338
339
  lambda { @t.add_result(Result.new(1, 2, 'D', :opponent => 1)) }.should raise_error(/result.*match/)
339
340
  lambda { @t.add_result(Result.new(1, 2, 'L', :opponent => 1, :rateable => false)) }.should raise_error(/result.*match/)
341
+ lambda { @t.add_result(Result.new(3, 3, 'L', :opponent => 1, :rateable => false)) }.should_not raise_error
340
342
  end
341
343
 
342
344
  it "should have a defined player" do
@@ -350,6 +352,13 @@ EOS
350
352
  it "should be consistent with the tournament's number of rounds" do
351
353
  lambda { @t.add_result(Result.new(4, 1, 'W', :opponent => 2)) }.should raise_error(/round/)
352
354
  end
355
+
356
+ it "documentation example should ne correct" do
357
+ @t.add_result(ICU::Result.new(3, 2, 'L', :opponent => 1, :rateable => false))
358
+ @t.add_result(ICU::Result.new(3, 1, 'L', :opponent => 2, :rateable => false))
359
+ @t.player(1).results.first.points.should == 0.0
360
+ @t.player(2).results.first.points.should == 0.0
361
+ end
353
362
  end
354
363
 
355
364
  context "finding players" do
@@ -495,6 +504,26 @@ EOS
495
504
  @t.player(2).id = 1350
496
505
  @t.invalid.should match(/duplicate.*ICU/)
497
506
  end
507
+
508
+ it "should allow players to have no results" do
509
+ (1..3).each { |r| @t.player(1).remove_result(r) }
510
+ @t.invalid.should be_false
511
+ end
512
+
513
+ it "should not allow asymmetric scores for rateable results" do
514
+ @t.player(1).find_result(1).score = 'L'
515
+ @t.invalid.should match(/result.*reverse/)
516
+ end
517
+
518
+ it "should allow asymmetric scores for unrateable results" do
519
+ @t.player(1).find_result(1).score = 'L'
520
+ (1..2).each do |p|
521
+ r = @t.player(p).find_result(1)
522
+ r.rateable = false
523
+ r.score = 'L'
524
+ end
525
+ @t.invalid.should be_false
526
+ end
498
527
  end
499
528
 
500
529
  context "renumbering" do
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: icu_tournament
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 1.4.3
5
+ version: 1.5.0
6
6
  platform: ruby
7
7
  authors:
8
8
  - Mark Orr
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-06-29 00:00:00 Z
13
+ date: 2011-07-01 00:00:00 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: dbf
@@ -172,7 +172,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
172
172
  requirements:
173
173
  - - ">="
174
174
  - !ruby/object:Gem::Version
175
- hash: 1676868114987793590
175
+ hash: 3338096865289385673
176
176
  segments:
177
177
  - 0
178
178
  version: "0"
@@ -181,7 +181,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
181
181
  requirements:
182
182
  - - ">="
183
183
  - !ruby/object:Gem::Version
184
- hash: 1676868114987793590
184
+ hash: 3338096865289385673
185
185
  segments:
186
186
  - 0
187
187
  version: "0"