icu_tournament 1.3.5 → 1.3.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,92 @@
1
+ module ICU
2
+ #
3
+ # This class is used to recognise the names of tie break rules. The class method _identify_
4
+ # takes a string as it's only argument and returns a new intstance if it recognises a
5
+ # tie break rule from the string, or _nil_ otherwise. An instance has three read-only methods:
6
+ # _id_ (a shoet symbolic name), _code_ (a two letter code) and _name_ (the full name).
7
+ # For example:
8
+ #
9
+ # ICU::TieBreak.identify("no such rule") # => nil
10
+ # tb = ICU::TieBreak.identify("Neustadlt")
11
+ # tb.id # => :neustadtl
12
+ # tb.code # => "SB"
13
+ # tb.name # => "Sonneborn-Berger"
14
+ #
15
+ # The method is case insensitive and can cope with extraneous white space and,
16
+ # to a limited extent, name variations and spelling mistakes:
17
+ #
18
+ # ICU::TieBreak.identify("SB").name # => "Sonneborn-Berger"
19
+ # ICU::TieBreak.identify("NESTADL").name # => "Sonneborn-Berger"
20
+ # ICU::TieBreak.identify(" wins ").name # => "Number of wins"
21
+ # ICU::TieBreak.identify(:sum_ratings).name # => "Sum of opponent's ratings"
22
+ # ICU::TieBreak.identify("median").name # => "Harkness"
23
+ # ICU::TieBreak.identify("MODIFIED").name # => "Modified median"
24
+ # ICU::TieBreak.identify("Modified\nMedian").name # => "Modified median"
25
+ # ICU::TieBreak.identify("\tbuccholts\t").name # => "Buchholz"
26
+ # ICU::TieBreak.identify("progressive\r\n").name # => "Sum of progressive scores"
27
+ # ICU::TieBreak.identify("SumOfCumulative").name # => "Sum of progressive scores"
28
+ #
29
+ # The full list of supported tie break rules is:
30
+ #
31
+ # * Buchholz (:buchholz, "BH"): sum of opponents' scores
32
+ # * Harkness (:harkness, "HK"): like Buchholz except the highest and lowest opponents' scores are discarded (or two highest and lowest if 9 rounds or more)
33
+ # * Modified median (:modified_median, "MM"): same as Harkness except only lowest (or highest) score(s) are discarded for players with more (or less) than 50%
34
+ # * Number of blacks (:blacks, "NB") number of blacks
35
+ # * Number of wins (:wins, "NW") number of wins
36
+ # * Player's name (:name, "PN"): alphabetical by name
37
+ # * Sonneborn-Berger (:neustadtl, "SB"): sum of scores of players defeated plus half sum of scores of players drawn against
38
+ # * Sum of opponents' ratings (:ratings, "SR"): sum of opponents ratings (FIDE ratings are used in preference to local ratings if available)
39
+ # * Sum of progressive scores (:progressive, "SP"): sum of running score for each round
40
+ #
41
+ # An array of all supported TieBreak instances (ordered by name) is returned by the class method _rules_.
42
+ #
43
+ # rules = ICU::TieBreak.rules
44
+ # rules.size # => 9
45
+ # rules.first.name # => "Buchholz"
46
+ #
47
+ # Note that this class only deals with the recognition of tie break names, not the calculation of tie break scores.
48
+ # The latter is currently implemented in the ICU::Tournament class.
49
+ #
50
+ class TieBreak
51
+ attr_reader :id, :code, :name
52
+ private_class_method :new
53
+
54
+ RULES =
55
+ {
56
+ :blacks => ["NB", "Number of blacks", %r{(number[-_ ]?of[-_ ])?blacks?}],
57
+ :buchholz => ["BH", "Buchholz", %r{^buc{1,2}h{1,2}olt?[zs]}],
58
+ :harkness => ["HK", "Harkness", %r{^(harkness?|median)$}],
59
+ :modified_median => ["MM", "Modified median", %r{^modified([-_ ]?median)?$}],
60
+ :name => ["PN", "Player's name", %r{^(player('?s)?[-_ ]?)?name$}],
61
+ :neustadtl => ["SB", "Sonneborn-Berger", %r{^(sonn?eborn[-_ ]?berger|n[eu]{1,2}sta[dtl]{2,3})}],
62
+ :progressive => ["SP", "Sum of progressive scores", %r{^(sum[-_ ]?(of[-_ ]?)?)?(progressive|cumulative)([-_ ]?scores?)?}],
63
+ :ratings => ["SR", "Sum of opponent's ratings", %r{(sum[-_ ]?(of[-_ ]?)?)?(opponents'?[-_ ]?)?ratings}],
64
+ :wins => ["NW", "Number of wins", %r{(number[-_ ]?of[-_ ])?wins?}],
65
+ }
66
+
67
+ # Given a string, return the TieBreak rule it is recognised as, or return nil.
68
+ def self.identify(str)
69
+ return nil unless str
70
+ str = str.to_s.gsub(/\s+/, ' ').strip.downcase
71
+ return nil if str.length <= 1 || str.length == 3
72
+ RULES.each_pair { |id, rule| return new(id, rule[0], rule[1]) if str.upcase == rule[0] || str.match(rule[2]) }
73
+ nil
74
+ end
75
+
76
+ # Return an array of all tie break rules, ordered by name.
77
+ def self.rules
78
+ RULES.keys.sort_by{ |id| RULES[id][1] }.inject([]) do |rules, id|
79
+ rules << new(id, RULES[id][0], RULES[id][1])
80
+ end
81
+ end
82
+
83
+ # :enddoc:
84
+ private
85
+
86
+ def initialize(id, code, name)
87
+ @id = id
88
+ @code = code
89
+ @name = name
90
+ end
91
+ end
92
+ end
@@ -105,17 +105,7 @@ module ICU
105
105
  # t.tie_breaks = [:buchholz, :neustadtl, :blacks, :wins]
106
106
  # t.tie_breaks = [] # reset to the default
107
107
  #
108
- # The full list of supported methods is:
109
- #
110
- # * _Buchholz_: sum of opponents' scores
111
- # * _Harkness_ (or _median_): like Buchholz except the highest and lowest opponents' scores are discarded (or two highest and lowest if 9 rounds or more)
112
- # * _modified_median_: same as Harkness except only lowest (or highest) score(s) are discarded for players with more (or less) than 50%
113
- # * _Neustadtl_ (or _Sonneborn-Berger_): sum of scores of players defeated plus half sum of scores of players drawn against
114
- # * _progressive_ (or _cumulative_): sum of running score for each round
115
- # * _ratings_: sum of opponents ratings (FIDE ratings are used in preference to local ratings if available)
116
- # * _blacks_: number of blacks
117
- # * _wins_: number of wins
118
- # * _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)
108
+ # See ICU::TieBreak for the full list of supported tie break methods.
119
109
  #
120
110
  # The return value from _rerank_ is the tournament object itself, to allow chaining, for example:
121
111
  #
@@ -226,28 +216,14 @@ module ICU
226
216
  @teams.find{ |t| t.matches(name) }
227
217
  end
228
218
 
229
- # Set the tie break methods.
219
+ # Canonicalise the names in the tie break array.
230
220
  def tie_breaks=(tie_breaks)
231
221
  raise "argument error - always set tie breaks to an array" unless tie_breaks.class == Array
232
- # Canonicalise the tie break method names.
233
- tie_breaks.map! do |m|
234
- m = m.to_s if m.class == Symbol
235
- m = m.downcase.gsub(/[-\s]/, '_') if m.class == String
236
- case m
237
- when true then 'name'
238
- when 'sonneborn_berger' then 'neustadtl'
239
- when 'modified_median' then 'modified'
240
- when 'median' then 'harkness'
241
- when 'cumulative' then 'progressive'
242
- else m
243
- end
222
+ @tie_breaks = tie_breaks.map do |str|
223
+ tb = ICU::TieBreak.identify(str)
224
+ raise "invalid tie break method '#{str}'" unless tb
225
+ tb.id
244
226
  end
245
-
246
- # Check they're all valid.
247
- tie_breaks.each { |m| raise "invalid tie break method '#{m}'" unless m.to_s.match(/^(blacks|buchholz|harkness|modified|name|neustadtl|progressive|ratings|wins)$/) }
248
-
249
- # Finally set them.
250
- @tie_breaks = tie_breaks;
251
227
  end
252
228
 
253
229
  # Add a new player to the tournament. Must have a unique player number.
@@ -508,7 +484,7 @@ module ICU
508
484
  end
509
485
  end
510
486
 
511
- # Return an array of tie break methods and an array of tie break orders (+1 for asc, -1 for desc).
487
+ # Return an array of tie break rules and an array of tie break orders (+1 for asc, -1 for desc).
512
488
  # The first and most important method is always "score", the last and least important is always "name".
513
489
  def tie_break_data
514
490
 
@@ -516,19 +492,19 @@ module ICU
516
492
  methods, order, data = Array.new, Hash.new, Hash.new
517
493
 
518
494
  # Score is always the most important.
519
- methods << 'score'
520
- order['score'] = -1
495
+ methods << :score
496
+ order[:score] = -1
521
497
 
522
498
  # Add the configured methods.
523
499
  tie_breaks.each do |m|
524
500
  methods << m
525
- order[m] = m == 'name' ? 1 : -1
501
+ order[m] = m == :name ? 1 : -1
526
502
  end
527
503
 
528
504
  # Name is included as the last and least important tie breaker unless it's already been added.
529
- unless methods.include?('name')
530
- methods << 'name'
531
- order['name'] = 1
505
+ unless methods.include?(:name)
506
+ methods << :name
507
+ order[:name] = 1
532
508
  end
533
509
 
534
510
  # We'll need the number of rounds.
@@ -537,7 +513,7 @@ module ICU
537
513
  # Pre-calculate some scores that are not in themselves tie break scores
538
514
  # but are needed in the calculation of some of the actual tie-break scores.
539
515
  pre_calculated = Array.new
540
- pre_calculated << 'opp-score' # sum scores where a non-played games counts 0.5
516
+ pre_calculated << :opp_score # sum scores where a non-played games counts 0.5
541
517
  pre_calculated.each do |m|
542
518
  data[m] = Hash.new
543
519
  @player.values.each { |p| data[m][p.num] = tie_break_score(data, m, p, rounds) }
@@ -557,20 +533,20 @@ module ICU
557
533
  # Return a tie break score for a given player and a given tie break method.
558
534
  def tie_break_score(hash, method, player, rounds)
559
535
  case method
560
- when 'score' then player.points
561
- when 'wins' then player.results.inject(0) { |t,r| t + (r.opponent && r.score == 'W' ? 1 : 0) }
562
- when 'blacks' then player.results.inject(0) { |t,r| t + (r.opponent && r.colour == 'B' ? 1 : 0) }
563
- when 'buchholz' then player.results.inject(0.0) { |t,r| t + (r.opponent ? hash['opp-score'][r.opponent] : 0.0) }
564
- when 'neustadtl' then player.results.inject(0.0) { |t,r| t + (r.opponent ? hash['opp-score'][r.opponent] * r.points : 0.0) }
565
- when 'opp-score' then player.results.inject(0.0) { |t,r| t + (r.opponent ? r.points : 0.5) } + (rounds - player.results.size) * 0.5
566
- when 'progressive' then (1..rounds).inject(0.0) { |t,n| r = player.find_result(n); s = r ? r.points : 0.0; t + s * (rounds + 1 - n) }
567
- when 'ratings' then player.results.inject(0) { |t,r| t + (r.opponent && (@player[r.opponent].fide_rating || @player[r.opponent].rating) ? (@player[r.opponent].fide_rating || @player[r.opponent].rating) : 0) }
568
- when 'harkness', 'modified'
569
- scores = player.results.map{ |r| r.opponent ? hash['opp-score'][r.opponent] : 0.0 }.sort
536
+ when :score then player.points
537
+ when :wins then player.results.inject(0) { |t,r| t + (r.opponent && r.score == 'W' ? 1 : 0) }
538
+ when :blacks then player.results.inject(0) { |t,r| t + (r.opponent && r.colour == 'B' ? 1 : 0) }
539
+ when :buchholz then player.results.inject(0.0) { |t,r| t + (r.opponent ? hash[:opp_score][r.opponent] : 0.0) }
540
+ when :neustadtl then player.results.inject(0.0) { |t,r| t + (r.opponent ? hash[:opp_score][r.opponent] * r.points : 0.0) }
541
+ when :opp_score then player.results.inject(0.0) { |t,r| t + (r.opponent ? r.points : 0.5) } + (rounds - player.results.size) * 0.5
542
+ when :progressive then (1..rounds).inject(0.0) { |t,n| r = player.find_result(n); s = r ? r.points : 0.0; t + s * (rounds + 1 - n) }
543
+ when :ratings then player.results.inject(0) { |t,r| t + (r.opponent && (@player[r.opponent].fide_rating || @player[r.opponent].rating) ? (@player[r.opponent].fide_rating || @player[r.opponent].rating) : 0) }
544
+ when :harkness, :modified_median
545
+ scores = player.results.map{ |r| r.opponent ? hash[:opp_score][r.opponent] : 0.0 }.sort
570
546
  1.upto(rounds - player.results.size) { scores << 0.0 }
571
547
  half = rounds / 2.0
572
548
  times = rounds >= 9 ? 2 : 1
573
- if method == 'harkness' || player.points == half
549
+ if method == :harkness || player.points == half
574
550
  1.upto(times) { scores.shift; scores.pop }
575
551
  else
576
552
  1.upto(times) { scores.send(player.points > half ? :shift : :pop) }
@@ -100,7 +100,7 @@ module ICU
100
100
  #
101
101
  # You may wish set the tie-break rules before ranking:
102
102
  #
103
- # tournament.tie_breaks = [:buchholz, ::neustadtl]
103
+ # tournament.tie_breaks = [:buchholz, :neustadtl]
104
104
  # spexport = tournament.rerank.renumber.serialize('SwissPerfect')
105
105
  #
106
106
  # See ICU::Tournament for more about tie-breaks.
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ICU
4
4
  class Tournament
5
- VERSION = "1.3.5"
5
+ VERSION = "1.3.6"
6
6
  end
7
7
  end
@@ -3,7 +3,7 @@
3
3
  require 'icu_name'
4
4
 
5
5
  icu_tournament_files = Array.new
6
- icu_tournament_files.concat %w{util federation}
6
+ icu_tournament_files.concat %w{util federation tie_break}
7
7
  icu_tournament_files.concat %w{player result team tournament}
8
8
  icu_tournament_files.concat %w{fcsv krause sp spx}.map{ |f| "tournament_#{f}"}
9
9
 
@@ -0,0 +1,100 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ module ICU
4
+ describe TieBreak do
5
+ context "#identify which rule" do
6
+ it "should recognize Buchholz" do
7
+ TieBreak.identify(:buchholz).id.should == :buchholz
8
+ TieBreak.identify(" BucholtS ").id.should == :buchholz
9
+ TieBreak.identify(" bh ").id.should == :buchholz
10
+ TieBreak.identify(" buccholts ").code.should == "BH"
11
+ end
12
+
13
+ it "should recognize Harkness (Median)" do
14
+ TieBreak.identify(:harkness).id.should == :harkness
15
+ TieBreak.identify("median").id.should == :harkness
16
+ TieBreak.identify(" hARKNES ").id.should == :harkness
17
+ TieBreak.identify("HK").id.should == :harkness
18
+ TieBreak.identify("MEDIAN").code.should == "HK"
19
+ end
20
+
21
+ it "should recognize Modified Median" do
22
+ TieBreak.identify(:modified).id.should == :modified_median
23
+ TieBreak.identify(" modified MEDIAN ").id.should == :modified_median
24
+ TieBreak.identify("MM").code.should == "MM"
25
+ end
26
+
27
+ it "should recognize Number of Blacks" do
28
+ TieBreak.identify(:blacks).id.should == :blacks
29
+ TieBreak.identify("number\tof\tblacks\n").id.should == :blacks
30
+ TieBreak.identify("\tnb\t").id.should == :blacks
31
+ TieBreak.identify("number_blacks").code.should == "NB"
32
+ end
33
+
34
+ it "should recognize Number of Wins" do
35
+ TieBreak.identify(:wins).id.should == :wins
36
+ TieBreak.identify(" number-of-wins ").id.should == :wins
37
+ TieBreak.identify("NUMBER WINS\r\n").id.should == :wins
38
+ TieBreak.identify("nw").code.should == "NW"
39
+ end
40
+
41
+ it "should recognize Player's of Name" do
42
+ TieBreak.identify(:name).id.should == :name
43
+ TieBreak.identify("Player's Name").id.should == :name
44
+ TieBreak.identify("players_name").id.should == :name
45
+ TieBreak.identify("PN").id.should == :name
46
+ TieBreak.identify("PLAYER-NAME").code.should == "PN"
47
+ end
48
+
49
+ it "should recognize Sonneborn-Berger" do
50
+ TieBreak.identify(:sonneborn_berger).id.should == :neustadtl
51
+ TieBreak.identify(:neustadtl).id.should == :neustadtl
52
+ TieBreak.identify(" SONNEBORN\nberger").id.should == :neustadtl
53
+ TieBreak.identify("\t soneborn_berger \t").id.should == :neustadtl
54
+ TieBreak.identify("sb").id.should == :neustadtl
55
+ TieBreak.identify("NESTADL").code.should == "SB"
56
+ end
57
+
58
+ it "should recognize Sum of Progressive Scores" do
59
+ TieBreak.identify(:progressive).id.should == :progressive
60
+ TieBreak.identify("CUMULATIVE").id.should == :progressive
61
+ TieBreak.identify("sum of progressive scores").id.should == :progressive
62
+ TieBreak.identify("SUM-cumulative_SCORE").id.should == :progressive
63
+ TieBreak.identify(:cumulative_score).id.should == :progressive
64
+ TieBreak.identify("SumOfCumulative").id.should == :progressive
65
+ TieBreak.identify("SP").code.should == "SP"
66
+ end
67
+
68
+ it "should recognize Sum of Opponents' Ratings" do
69
+ TieBreak.identify(:ratings).id.should == :ratings
70
+ TieBreak.identify("sum of opponents ratings").id.should == :ratings
71
+ TieBreak.identify("Opponents' Ratings").id.should == :ratings
72
+ TieBreak.identify("SR").id.should == :ratings
73
+ TieBreak.identify("SUMOPPONENTSRATINGS").code.should == "SR"
74
+ end
75
+
76
+ it "should recognize player's name" do
77
+ TieBreak.identify(:name).id.should == :name
78
+ TieBreak.identify(" player's NAME ").id.should == :name
79
+ TieBreak.identify("pn").code.should == "PN"
80
+ end
81
+
82
+ it "should return nil for unrecognized tie breaks" do
83
+ TieBreak.identify("XYZ").should be_nil
84
+ TieBreak.identify(nil).should be_nil
85
+ end
86
+ end
87
+
88
+ context "return an array of tie break rules" do
89
+ before(:each) do
90
+ @rules = TieBreak.rules
91
+ end
92
+
93
+ it "should be an array in a specific order" do
94
+ @rules.size.should == 9
95
+ @rules.first.name.should == "Buchholz"
96
+ @rules.map(&:code).join("|").should == "BH|HK|MM|NB|NW|PN|SB|SR|SP"
97
+ end
98
+ end
99
+ end
100
+ end
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 1
7
7
  - 3
8
- - 5
9
- version: 1.3.5
8
+ - 6
9
+ version: 1.3.6
10
10
  platform: ruby
11
11
  authors:
12
12
  - Mark Orr
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2011-02-19 00:00:00 +00:00
17
+ date: 2011-02-27 00:00:00 +00:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -135,6 +135,7 @@ files:
135
135
  - lib/icu_tournament/player.rb
136
136
  - lib/icu_tournament/result.rb
137
137
  - lib/icu_tournament/team.rb
138
+ - lib/icu_tournament/tie_break.rb
138
139
  - lib/icu_tournament/tournament.rb
139
140
  - lib/icu_tournament/tournament_fcsv.rb
140
141
  - lib/icu_tournament/tournament_krause.rb
@@ -148,6 +149,7 @@ files:
148
149
  - spec/result_spec.rb
149
150
  - spec/spec_helper.rb
150
151
  - spec/team_spec.rb
152
+ - spec/tie_break_spec.rb
151
153
  - spec/tournament_fcsv_spec.rb
152
154
  - spec/tournament_krause_spec.rb
153
155
  - spec/tournament_sp_spec.rb
@@ -194,6 +196,7 @@ test_files:
194
196
  - spec/result_spec.rb
195
197
  - spec/spec_helper.rb
196
198
  - spec/team_spec.rb
199
+ - spec/tie_break_spec.rb
197
200
  - spec/tournament_fcsv_spec.rb
198
201
  - spec/tournament_krause_spec.rb
199
202
  - spec/tournament_sp_spec.rb