icu_tournament 1.2.8 → 1.3.1
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/README.rdoc +1 -1
- data/lib/icu_tournament.rb +1 -1
- data/lib/icu_tournament/federation.rb +6 -0
- data/lib/icu_tournament/player.rb +7 -7
- data/lib/icu_tournament/tournament.rb +11 -10
- data/lib/icu_tournament/tournament_fcsv.rb +14 -12
- data/lib/icu_tournament/tournament_krause.rb +10 -7
- data/lib/icu_tournament/tournament_sp.rb +27 -63
- data/lib/icu_tournament/tournament_spx.rb +397 -0
- data/lib/icu_tournament/version.rb +1 -1
- data/spec/player_spec.rb +50 -36
- data/spec/tournament_fcsv_spec.rb +50 -50
- data/spec/tournament_krause_spec.rb +26 -20
- data/spec/tournament_sp_spec.rb +46 -54
- data/spec/tournament_spx_spec.rb +379 -0
- metadata +7 -4
data/README.rdoc
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
For reading or writing files of chess tournament data. Original project name on github was _chess_icu_.
|
|
4
4
|
|
|
5
|
-
|
|
6
5
|
== Install
|
|
7
6
|
|
|
8
7
|
For ruby 1.9.2 (version 1.1.2 was the last compatible with ruby 1.8.7).
|
|
@@ -27,6 +26,7 @@ The currently supported formats are:
|
|
|
27
26
|
* ICU::Tournament::Krause - the format used by FIDE.
|
|
28
27
|
* ICU::Tournament::ForeignCSV - used by Irish players to report their individual results in foreign tournaments.
|
|
29
28
|
* ICU::Tournament::SwissPerfect - often used by Irish tournament controllers to report results.
|
|
29
|
+
* ICU::Tournament::SPExport - the SwissPerfect text export format.
|
|
30
30
|
|
|
31
31
|
== Writing Files
|
|
32
32
|
|
data/lib/icu_tournament.rb
CHANGED
|
@@ -5,6 +5,6 @@ require 'icu_name'
|
|
|
5
5
|
icu_tournament_files = Array.new
|
|
6
6
|
icu_tournament_files.concat %w{util federation}
|
|
7
7
|
icu_tournament_files.concat %w{player result team tournament}
|
|
8
|
-
icu_tournament_files.concat %w{fcsv krause sp}.map{ |f| "tournament_#{f}"}
|
|
8
|
+
icu_tournament_files.concat %w{fcsv krause sp spx}.map{ |f| "tournament_#{f}"}
|
|
9
9
|
|
|
10
10
|
icu_tournament_files.each { |file| require "icu_tournament/#{file}" }
|
|
@@ -81,6 +81,10 @@ module ICU
|
|
|
81
81
|
matches[0]
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
+
# Return an array of codes and names suitable for creating a federation menu in Rails.
|
|
85
|
+
# ICU::Federation.menu(:order => 'code') # order federations by code (instead of by name)
|
|
86
|
+
# ICU::Federation.menu(:top => 'IRL') # make this federation come first
|
|
87
|
+
# ICU::Federation.menu(:none => 'None') # add a dummy top entry with name "None" and blank code
|
|
84
88
|
def self.menu(opts = {})
|
|
85
89
|
compile
|
|
86
90
|
top, menu = nil, []
|
|
@@ -91,6 +95,7 @@ module ICU
|
|
|
91
95
|
menu
|
|
92
96
|
end
|
|
93
97
|
|
|
98
|
+
# Return an array of sorted federation codes.
|
|
94
99
|
def self.codes
|
|
95
100
|
compile
|
|
96
101
|
@@objects.map(&:code).sort
|
|
@@ -101,6 +106,7 @@ module ICU
|
|
|
101
106
|
@name = name
|
|
102
107
|
end
|
|
103
108
|
|
|
109
|
+
# :enddoc:
|
|
104
110
|
private
|
|
105
111
|
|
|
106
112
|
def self.compile
|
|
@@ -12,9 +12,10 @@ module ICU
|
|
|
12
12
|
#
|
|
13
13
|
# In addition, players have a number of optional attributes which can be specified
|
|
14
14
|
# via setters or in constructor hash options: _id_ (local or national ID), _fide_
|
|
15
|
-
# (FIDE ID), _fed_ (federation), _title_, _rating_
|
|
15
|
+
# (FIDE ID), _fed_ (federation), _title_, _rating_ (local rating), _fide_rating,
|
|
16
|
+
# _rank_ and _dob_ (date of birth).
|
|
16
17
|
#
|
|
17
|
-
# peter = ICU::Player.new('Peter', 'Svidler', 21, :fed => 'rus', :title => 'g', :
|
|
18
|
+
# peter = ICU::Player.new('Peter', 'Svidler', 21, :fed => 'rus', :title => 'g', :fide_rating = 2700)
|
|
18
19
|
# peter.dob = '17th June, 1976'
|
|
19
20
|
# peter.rank = 1
|
|
20
21
|
#
|
|
@@ -80,11 +81,10 @@ module ICU
|
|
|
80
81
|
# fox1.fed # => 'IRL'
|
|
81
82
|
# fox1.gender # => 'M'
|
|
82
83
|
#
|
|
83
|
-
#
|
|
84
84
|
class Player
|
|
85
85
|
extend ICU::Accessor
|
|
86
86
|
attr_integer :num
|
|
87
|
-
attr_positive_or_nil :id, :fide, :rating, :rank
|
|
87
|
+
attr_positive_or_nil :id, :fide, :rating, :fide_rating, :rank
|
|
88
88
|
attr_date_or_nil :dob
|
|
89
89
|
|
|
90
90
|
attr_reader :results, :first_name, :last_name, :fed, :title, :gender
|
|
@@ -94,7 +94,7 @@ module ICU
|
|
|
94
94
|
self.first_name = first_name
|
|
95
95
|
self.last_name = last_name
|
|
96
96
|
self.num = num
|
|
97
|
-
[:id, :fide, :fed, :title, :rating, :rank, :dob, :gender].each do |atr|
|
|
97
|
+
[:id, :fide, :fed, :title, :rating, :fide_rating, :rank, :dob, :gender].each do |atr|
|
|
98
98
|
self.send("#{atr}=", opt[atr]) unless opt[atr].nil?
|
|
99
99
|
end
|
|
100
100
|
@results = []
|
|
@@ -192,7 +192,7 @@ module ICU
|
|
|
192
192
|
def eql?(other)
|
|
193
193
|
return true if equal?(other)
|
|
194
194
|
return false unless self == other
|
|
195
|
-
[:id, :fide, :rating, :title, :gender].each do |m|
|
|
195
|
+
[:id, :fide, :rating, :fide_rating, :title, :gender].each do |m|
|
|
196
196
|
return false if self.send(m) && other.send(m) && self.send(m) != other.send(m)
|
|
197
197
|
end
|
|
198
198
|
true
|
|
@@ -201,7 +201,7 @@ module ICU
|
|
|
201
201
|
# Merge in some of the details of another player.
|
|
202
202
|
def merge(other)
|
|
203
203
|
raise "cannot merge two players that are not equal" unless self == other
|
|
204
|
-
[:id, :fide, :rating, :title, :fed, :gender].each do |m|
|
|
204
|
+
[:id, :fide, :rating, :fide_rating, :title, :fed, :gender].each do |m|
|
|
205
205
|
self.send("#{m}=", other.send(m)) unless self.send(m)
|
|
206
206
|
end
|
|
207
207
|
end
|
|
@@ -95,7 +95,7 @@ module ICU
|
|
|
95
95
|
# t.validate(:rerank => true)
|
|
96
96
|
#
|
|
97
97
|
# Ranking is inconsistent if some but not all players have a rank or if all players
|
|
98
|
-
#
|
|
98
|
+
# but at least one pair of players exist where one has a higher score but a lower rank.
|
|
99
99
|
#
|
|
100
100
|
# To rank the players requires a tie break method to be specified to order players on the same score.
|
|
101
101
|
# The default is alphabetical (by last name then first name). Other methods can be specified by supplying
|
|
@@ -112,7 +112,7 @@ module ICU
|
|
|
112
112
|
# * _modified_median_: same as Harkness except only lowest (or highest) score(s) are discarded for players with more (or less) than 50%
|
|
113
113
|
# * _Neustadtl_ (or _Sonneborn-Berger_): sum of scores of players defeated plus half sum of scores of players drawn against
|
|
114
114
|
# * _progressive_ (or _cumulative_): sum of running score for each round
|
|
115
|
-
# * _ratings_: sum of opponents ratings
|
|
115
|
+
# * _ratings_: sum of opponents ratings (FIDE ratings are used in preference to local ratings if available)
|
|
116
116
|
# * _blacks_: number of blacks
|
|
117
117
|
# * _wins_: number of wins
|
|
118
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)
|
|
@@ -382,7 +382,7 @@ module ICU
|
|
|
382
382
|
# Convenience method to parse a file.
|
|
383
383
|
def self.parse_file!(file, format, opts={})
|
|
384
384
|
type = format.to_s
|
|
385
|
-
raise "Invalid format" unless type.match(/^(SwissPerfect|Krause|ForeignCSV)$/);
|
|
385
|
+
raise "Invalid format" unless type.match(/^(SwissPerfect|SPExport|Krause|ForeignCSV)$/);
|
|
386
386
|
parser = "ICU::Tournament::#{format}".constantize.new
|
|
387
387
|
if type == 'ForeignCSV'
|
|
388
388
|
# Doesn't take options.
|
|
@@ -398,15 +398,16 @@ module ICU
|
|
|
398
398
|
# or if the tournament is unsuitable for serialisation in that format.
|
|
399
399
|
def serialize(format, arg={})
|
|
400
400
|
serializer = case format.to_s.downcase
|
|
401
|
-
when 'krause'
|
|
402
|
-
when 'foreigncsv'
|
|
403
|
-
when '
|
|
404
|
-
when ''
|
|
401
|
+
when 'krause' then ICU::Tournament::Krause.new
|
|
402
|
+
when 'foreigncsv' then ICU::Tournament::ForeignCSV.new
|
|
403
|
+
when 'spexport' then ICU::Tournament::SPExport.new
|
|
404
|
+
when '' then raise "no format supplied"
|
|
405
405
|
else raise "unsupported serialisation format: '#{format}'"
|
|
406
406
|
end
|
|
407
407
|
serializer.serialize(self, arg)
|
|
408
408
|
end
|
|
409
409
|
|
|
410
|
+
# :enddoc:
|
|
410
411
|
private
|
|
411
412
|
|
|
412
413
|
# Check players.
|
|
@@ -500,8 +501,8 @@ module ICU
|
|
|
500
501
|
def check_type(type)
|
|
501
502
|
if type.respond_to?(:validate!)
|
|
502
503
|
type.validate!(self)
|
|
503
|
-
elsif type.to_s.match(/^(ForeignCSV|Krause|SwissPerfect)$/)
|
|
504
|
-
|
|
504
|
+
elsif type.to_s.match(/^(ForeignCSV|Krause|SwissPerfect|SPExport)$/)
|
|
505
|
+
"ICU::Tournament::#{type.to_s}".constantize.new.validate!(self)
|
|
505
506
|
else
|
|
506
507
|
raise "invalid type supplied for validation check"
|
|
507
508
|
end
|
|
@@ -563,7 +564,7 @@ module ICU
|
|
|
563
564
|
when 'neustadtl' then player.results.inject(0.0) { |t,r| t + (r.opponent ? hash['opp-score'][r.opponent] * r.points : 0.0) }
|
|
564
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
|
|
565
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) }
|
|
566
|
-
when 'ratings' then player.results.inject(0) { |t,r| t + (r.opponent && @player[r.opponent].rating ? @player[r.opponent].rating : 0) }
|
|
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) }
|
|
567
568
|
when 'harkness', 'modified'
|
|
568
569
|
scores = player.results.map{ |r| r.opponent ? hash['opp-score'][r.opponent] : 0.0 }.sort
|
|
569
570
|
1.upto(rounds - player.results.size) { scores << 0.0 }
|
|
@@ -97,19 +97,20 @@ module ICU
|
|
|
97
97
|
# ICU::Tournament::ForeignCSV.new.serialize(tournament)
|
|
98
98
|
#
|
|
99
99
|
# You can also build the tournament object from scratch using your own data and then serialize it.
|
|
100
|
-
# For example, here are the commands to reproduce the example above.
|
|
100
|
+
# For example, here are the commands to reproduce the example above. Note that in this format
|
|
101
|
+
# opponents' ratings are FIDE while players' IDs are ICU.
|
|
101
102
|
#
|
|
102
103
|
# t = ICU::Tournament.new("Isle of Man Masters, 2007", '2007-09-22', :rounds => 9)
|
|
103
104
|
# t.site = 'http://www.bcmchess.co.uk/monarch2007/'
|
|
104
|
-
# t.add_player(ICU::Player.new('Anthony', 'Fox', 1, :
|
|
105
|
-
# t.add_player(ICU::Player.new('Peter P.', 'Taylor', 2, :
|
|
106
|
-
# t.add_player(ICU::Player.new('Egozi', 'Nadav', 3, :
|
|
107
|
-
# t.add_player(ICU::Player.new('Peter', 'Cafolla', 4, :
|
|
108
|
-
# t.add_player(ICU::Player.new('Tim R.', 'Spanton', 5, :
|
|
109
|
-
# t.add_player(ICU::Player.new('Alan', 'Grant', 6, :
|
|
110
|
-
# t.add_player(ICU::Player.new('Alan J.', 'Walton', 7, :
|
|
111
|
-
# t.add_player(ICU::Player.new('Bernard', 'Bannink', 8, :
|
|
112
|
-
# t.add_player(ICU::Player.new('Roy', 'Phillips', 9, :
|
|
105
|
+
# t.add_player(ICU::Player.new('Anthony', 'Fox', 1, :fide_rating => 2100, :fed => 'IRL', :id => 456))
|
|
106
|
+
# t.add_player(ICU::Player.new('Peter P.', 'Taylor', 2, :fide_rating => 2209, :fed => 'ENG'))
|
|
107
|
+
# t.add_player(ICU::Player.new('Egozi', 'Nadav', 3, :fide_rating => 2205, :fed => 'ISR'))
|
|
108
|
+
# t.add_player(ICU::Player.new('Peter', 'Cafolla', 4, :fide_rating => 2048, :fed => 'IRL'))
|
|
109
|
+
# t.add_player(ICU::Player.new('Tim R.', 'Spanton', 5, :fide_rating => 1982, :fed => 'ENG'))
|
|
110
|
+
# t.add_player(ICU::Player.new('Alan', 'Grant', 6, :fide_rating => 2223, :fed => 'SCO'))
|
|
111
|
+
# t.add_player(ICU::Player.new('Alan J.', 'Walton', 7, :fide_rating => 2223, :fed => 'ENG'))
|
|
112
|
+
# t.add_player(ICU::Player.new('Bernard', 'Bannink', 8, :fide_rating => 2271, :fed => 'NED', :title => 'FM'))
|
|
113
|
+
# t.add_player(ICU::Player.new('Roy', 'Phillips', 9, :fide_rating => 2271, :fed => 'MAU'))
|
|
113
114
|
# t.add_result(ICU::Result.new(1, 1, 'L', :opponent => 2, :colour => 'B'))
|
|
114
115
|
# t.add_result(ICU::Result.new(2, 1, 'D', :opponent => 3, :colour => 'W'))
|
|
115
116
|
# t.add_result(ICU::Result.new(3, 1, 'D', :opponent => 4, :colour => 'B'))
|
|
@@ -221,7 +222,7 @@ module ICU
|
|
|
221
222
|
o = t.player(r.opponent)
|
|
222
223
|
data << o.last_name
|
|
223
224
|
data << o.first_name
|
|
224
|
-
data << o.
|
|
225
|
+
data << o.fide_rating
|
|
225
226
|
data << o.title
|
|
226
227
|
data << o.fed
|
|
227
228
|
else
|
|
@@ -250,6 +251,7 @@ module ICU
|
|
|
250
251
|
end
|
|
251
252
|
end
|
|
252
253
|
|
|
254
|
+
# :enddoc:
|
|
253
255
|
private
|
|
254
256
|
|
|
255
257
|
def event
|
|
@@ -308,7 +310,7 @@ module ICU
|
|
|
308
310
|
@tournament.add_result(result)
|
|
309
311
|
else
|
|
310
312
|
result.colour = @r[2]
|
|
311
|
-
opponent = Player.new(@r[4], @r[3], @tournament.players.size + 1, :
|
|
313
|
+
opponent = Player.new(@r[4], @r[3], @tournament.players.size + 1, :fide_rating => @r[5], :title => @r[6], :fed => @r[7])
|
|
312
314
|
raise "opponent must have a federation" unless opponent.fed
|
|
313
315
|
old_player = @tournament.find_player(opponent)
|
|
314
316
|
if old_player
|
|
@@ -48,13 +48,15 @@ module ICU
|
|
|
48
48
|
# daffy.fide # => nil
|
|
49
49
|
# daffy.dob # => "1937-04-17"
|
|
50
50
|
#
|
|
51
|
-
# By default, ID numbers in the input are interpreted as local IDs. If, instead, they should be interpreted as
|
|
52
|
-
# FIDE IDs, add the following option:
|
|
51
|
+
# By default, ID numbers and ratings in the input are interpreted as local IDs and ratings. If, instead, they should be interpreted as
|
|
52
|
+
# FIDE IDs and ratings, add the following option:
|
|
53
53
|
#
|
|
54
54
|
# tournament = parser.parse_file('tournament.tab', :fide => true)
|
|
55
55
|
# daffy = tournament.player(2)
|
|
56
56
|
# daffy.id # => nil
|
|
57
57
|
# daffy.fide # => 7654321
|
|
58
|
+
# daffy.rating # => nil
|
|
59
|
+
# daffy.fide_rating # => 2200
|
|
58
60
|
#
|
|
59
61
|
# If the ranking numbers are missing from the file or inconsistent (e.g. player A is ranked above player B
|
|
60
62
|
# but has less points than player B) they are recalculated as a side effect of the parse.
|
|
@@ -82,11 +84,11 @@ module ICU
|
|
|
82
84
|
#
|
|
83
85
|
# krause = tournament.serialize('Krause')
|
|
84
86
|
#
|
|
85
|
-
# By default, local (ICU) IDs are used for the serialization, but both methods accept an option that
|
|
86
|
-
# causes FIDE IDs to be used instead:
|
|
87
|
+
# By default, local (ICU) IDs and ratings are used for the serialization, but both methods accept an option that
|
|
88
|
+
# causes FIDE IDs and ratings to be used instead:
|
|
87
89
|
#
|
|
88
90
|
# krause = parser.serialize(tournament, :fide => true)
|
|
89
|
-
# krause =
|
|
91
|
+
# krause = tournament.serialize('Krause', :fide => true)
|
|
90
92
|
#
|
|
91
93
|
# The following lists Krause data identification numbers, their description and, where available, their corresponding
|
|
92
94
|
# attributes in an ICU::Tournament instance.
|
|
@@ -241,6 +243,7 @@ module ICU
|
|
|
241
243
|
# None.
|
|
242
244
|
end
|
|
243
245
|
|
|
246
|
+
# :enddoc:
|
|
244
247
|
private
|
|
245
248
|
|
|
246
249
|
def set_name
|
|
@@ -263,12 +266,12 @@ module ICU
|
|
|
263
266
|
{
|
|
264
267
|
:gender => @data[5, 1],
|
|
265
268
|
:title => @data[6, 3],
|
|
266
|
-
:rating => @data[44, 4],
|
|
267
269
|
:fed => @data[49, 3],
|
|
268
270
|
:dob => @data[65, 10],
|
|
269
271
|
:rank => @data[81, 4],
|
|
270
272
|
}
|
|
271
273
|
opt[arg[:fide] ? :fide : :id] = @data[53, 11]
|
|
274
|
+
opt[arg[:fide] ? :fide_rating : :rating] = @data[44, 4]
|
|
272
275
|
player = Player.new(nam.first, nam.last, num, opt)
|
|
273
276
|
@tournament.add_player(player)
|
|
274
277
|
|
|
@@ -337,7 +340,7 @@ module ICU
|
|
|
337
340
|
krause << sprintf(' %1s', case @gender; when 'M' then 'm'; when 'F' then 'w'; else ''; end)
|
|
338
341
|
krause << sprintf(' %2s', case @title; when nil then ''; when 'IM' then 'm'; when 'WIM' then 'wm'; else @title[0, @title.length-1].downcase; end)
|
|
339
342
|
krause << sprintf(' %-33s', "#{@last_name},#{@first_name}")
|
|
340
|
-
krause << sprintf(' %4s', @rating)
|
|
343
|
+
krause << sprintf(' %4s', arg[:fide] ? @fide_rating : @rating)
|
|
341
344
|
krause << sprintf(' %3s', @fed)
|
|
342
345
|
krause << sprintf(' %11s', arg[:fide] ? @fide : @id)
|
|
343
346
|
krause << sprintf(' %10s', @dob)
|
|
@@ -35,22 +35,17 @@ module ICU
|
|
|
35
35
|
# If no start date is supplied it will default to 2000-01-01, and can be reset later.
|
|
36
36
|
#
|
|
37
37
|
# tournament = parser.parse_file('champs.zip')
|
|
38
|
-
# tournament.start
|
|
38
|
+
# tournament.start # => '2000-01-01'
|
|
39
39
|
# tournament.start = '2010-07-03'
|
|
40
40
|
#
|
|
41
|
-
# SwissPerfect files have slots for both local and international IDs and these, if present
|
|
42
|
-
# and if
|
|
43
|
-
#
|
|
44
|
-
# By default, the parser extracts the local rating from the SwissPerfect files and not the international one.
|
|
45
|
-
# If international ratings are required instead, set the _rating_ option to "intl". For example:
|
|
41
|
+
# SwissPerfect files have slots for both local and international IDs and ratings and these, if present
|
|
42
|
+
# (and if integers) are copied to the _id_, _fide_, _rating_ and _fide_rating_ attributes.
|
|
46
43
|
#
|
|
47
44
|
# tournament = parser.parse_file('ncc', :start => '2010-05-08')
|
|
48
|
-
# tournament.player(2).id
|
|
49
|
-
# tournament.player(2).fide
|
|
50
|
-
# tournament.player(2).rating
|
|
51
|
-
#
|
|
52
|
-
# tournament = parser.parse_file('ncc', :start => '2010-05-08', :rating => 'intl')
|
|
53
|
-
# tournament.player(2).rating # => 2530 (FIDE rating)
|
|
45
|
+
# tournament.player(2).id # => 12379 (ICU ID)
|
|
46
|
+
# tournament.player(2).fide # => 1205064 (FIDE ID)
|
|
47
|
+
# tournament.player(2).rating # => 2556 (ICU rating)
|
|
48
|
+
# tournament.player(2).fide_rating # => 2530 (FIDE rating)
|
|
54
49
|
#
|
|
55
50
|
# By default, the parse will fail completely if the ".trn" file contains any invalid federations (see ICU::Federation).
|
|
56
51
|
# There are two alternative behaviours controlled by setting the _fed_ option:
|
|
@@ -62,55 +57,23 @@ module ICU
|
|
|
62
57
|
#
|
|
63
58
|
# Because the data is in three parts, some of which are in a legacy binary format, serialization to this format is
|
|
64
59
|
# not supported. Instead, a method is provided to serialize any tournament type into the text export format of
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
# No Name Loc Id Total 1 2 3
|
|
68
|
-
#
|
|
69
|
-
# 1 Griffiths, Ryan-Rhys 6897 3 4:W 2:W 3:W
|
|
70
|
-
# 2 Flynn, Jamie 5226 2 3:W 1:L 4:W
|
|
71
|
-
# 3 Hulleman, Leon 6409 1 2:L 4:W 1:L
|
|
72
|
-
# 4 Dunne, Thomas 10914 0 1:L 3:L 2:L
|
|
73
|
-
#
|
|
74
|
-
# This format is important in Irish chess, as it's the format used to submit results to the <em>MicroSoft Access</em>
|
|
75
|
-
# implementation of the ICU ratings database.
|
|
76
|
-
#
|
|
77
|
-
# swiss_perfect = tournament.serialize('SwissPerfect')
|
|
78
|
-
#
|
|
79
|
-
# The order of players in the serialized output is always by player number and as a side effect of serialization,
|
|
80
|
-
# the player numbers will be adjusted to ensure they range from 1 to the total number of players (i.e. renumbered
|
|
81
|
-
# in order). If you would prefer rank-order instead, then you must first renumber the players by rank (the default
|
|
82
|
-
# renumbering method) before serializing. For example:
|
|
83
|
-
#
|
|
84
|
-
# swiss_perfect = tournament.renumber.serialize('SwissPerfect')
|
|
85
|
-
#
|
|
86
|
-
# There should be no need to explicitly rank the tournament first, as that information is already present in
|
|
87
|
-
# SwissPerfect files (i.e. each player should already have a rank after the files have been parsed).
|
|
88
|
-
# Additionally, the tie break rules used for the tournament are available from the _tie_break_ method,
|
|
89
|
-
# for example:
|
|
90
|
-
#
|
|
91
|
-
# tournament.tie_breaks # => [:buchholz, :harkness]
|
|
92
|
-
#
|
|
93
|
-
# Should you wish to rank the tournament using a different set of tie-break rules, you can do something like the following:
|
|
94
|
-
#
|
|
95
|
-
# tournament.tie_breaks = [:wins, :blacks]
|
|
96
|
-
# swiss_perfect = tournament.rerank.renumber.serialize('SwissPerfect')
|
|
97
|
-
#
|
|
98
|
-
# See ICU::Tournament for more about tie-breaks.
|
|
60
|
+
# SwissPerfect (see ICU::Tournament::SPExport).
|
|
99
61
|
#
|
|
100
62
|
class SwissPerfect
|
|
101
63
|
attr_reader :error
|
|
102
64
|
|
|
103
65
|
TRN = {
|
|
104
|
-
:dob
|
|
105
|
-
:fed
|
|
106
|
-
:first_name
|
|
107
|
-
:gender
|
|
108
|
-
:id
|
|
109
|
-
:fide
|
|
110
|
-
:last_name
|
|
111
|
-
:num
|
|
112
|
-
:rank
|
|
113
|
-
:rating
|
|
66
|
+
:dob => "BIRTH_DATE",
|
|
67
|
+
:fed => "FEDER",
|
|
68
|
+
:first_name => "FIRSTNAME",
|
|
69
|
+
:gender => "SEX",
|
|
70
|
+
:id => "LOC_ID",
|
|
71
|
+
:fide => "INTL_ID",
|
|
72
|
+
:last_name => "SURNAME",
|
|
73
|
+
:num => "ID",
|
|
74
|
+
:rank => "ORDER",
|
|
75
|
+
:rating => "LOC_RTG",
|
|
76
|
+
:fide_rating => "INTL_RTG",
|
|
114
77
|
} # not used: ABSENT BOARD CLUB FORB_PAIRS LATE_ENTRY LOC_RTG2 MEMO TEAM TECH_SCORE WITHDRAWAL (START_NO, BONUS used below)
|
|
115
78
|
|
|
116
79
|
SCO = %w{ROUND WHITE BLACK W_SCORE B_SCORE W_TYPE B_TYPE} # not used W_SUBSCO, B_SUBSCO
|
|
@@ -175,6 +138,7 @@ module ICU
|
|
|
175
138
|
# None.
|
|
176
139
|
end
|
|
177
140
|
|
|
141
|
+
# :enddoc:
|
|
178
142
|
private
|
|
179
143
|
|
|
180
144
|
def get_files(file, arg)
|
|
@@ -280,15 +244,15 @@ module ICU
|
|
|
280
244
|
@start_no[r.attributes["ID"]] = r.attributes["START_NO"]
|
|
281
245
|
TRN.inject(Hash.new) do |hash, pair|
|
|
282
246
|
key = pair[1]
|
|
283
|
-
key = key[arg[pair[0]].to_s == 'intl' ? 1 : 0] if key.class == Array
|
|
284
247
|
val = r.attributes[key]
|
|
285
248
|
case pair[0]
|
|
286
|
-
when :fed
|
|
287
|
-
when :gender
|
|
288
|
-
when :id
|
|
289
|
-
when :fide
|
|
290
|
-
when :rating
|
|
291
|
-
when :
|
|
249
|
+
when :fed then val = val && val.match(/^[A-Z]{3}$/i) ? val.upcase : nil
|
|
250
|
+
when :gender then val = val.to_i > 0 ? %w(M F)[val.to_i-1] : nil
|
|
251
|
+
when :id then val = val.to_i > 0 ? val : nil
|
|
252
|
+
when :fide then val = val.to_i > 0 ? val : nil
|
|
253
|
+
when :rating then val = val.to_i > 0 ? val : nil
|
|
254
|
+
when :fide_rating then val = val.to_i > 0 ? val : nil
|
|
255
|
+
when :title then val = val.to_i > 0 ? %w(GM WGM IM WIM FM WFM)[val.to_i-1] : nil
|
|
292
256
|
end
|
|
293
257
|
if pair[0] == :fed && val && arg[:fed]
|
|
294
258
|
val = nil if arg[:fed].to_s == 'ignore'
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
module ICU
|
|
2
|
+
class Tournament
|
|
3
|
+
#
|
|
4
|
+
# The SWissPerfect export format used to be important in Irish chess as it was used to submit
|
|
5
|
+
# results to the ICU's first computerised ratings system, a <em>MicroSoft Access</em> database.
|
|
6
|
+
# As a text based format, it was easier to manipulate than the full binary formats of SwissPerfect.
|
|
7
|
+
# Here is an illustrative example of this format:
|
|
8
|
+
#
|
|
9
|
+
# No Name Feder Intl Id Loc Id Rtg Loc Title Total 1 2 3
|
|
10
|
+
#
|
|
11
|
+
# 1 Duck, Daffy IRL 12345 2200 im 2 0:= 3:W 2:D
|
|
12
|
+
# 2 Mouse, Minerva 1234568 1900 1.5 3:D 0:= 1:D
|
|
13
|
+
# 3 Mouse, Mickey USA 1234567 gm 1 2:D 1:L 0:=
|
|
14
|
+
#
|
|
15
|
+
# The format does not record either the name nor the start date of the tournament.
|
|
16
|
+
# Player colours are also missing. When parsing data in this format it is necessary
|
|
17
|
+
# to specify name and start date explicitly:
|
|
18
|
+
#
|
|
19
|
+
# parser = ICU::Tournament::SPExport.new
|
|
20
|
+
# tournament = parser.parse_file('sample.txt', :name => 'Mickey Mouse Masters', :start => '2011-02-06')
|
|
21
|
+
#
|
|
22
|
+
# tournament.name # => "Mickey Mouse Masters"
|
|
23
|
+
# tournament.start # => "2011-02-06"
|
|
24
|
+
# tournament.rounds # => 3
|
|
25
|
+
# tournament.player(1).name # => "Duck, Daffy"
|
|
26
|
+
# tournament.player(2).points # => 1.5
|
|
27
|
+
# tournament.player(3).fed # => "USA"
|
|
28
|
+
#
|
|
29
|
+
# See ICU::Tournament for further details about the object returned.
|
|
30
|
+
#
|
|
31
|
+
# The SwissPerfect application offers a number of choices when exporting a tournament cross table,
|
|
32
|
+
# one of which is the column separator. The ICU::Tournament::SPExport parser can only handle data
|
|
33
|
+
# with tab separators but is able to cope with any other configuration choices. For example, if
|
|
34
|
+
# some of the optional columns are missing or if the data is not formatted with space padding.
|
|
35
|
+
#
|
|
36
|
+
# To serialize an ICU::Tournament instance to the format, use the _serialize_ method of
|
|
37
|
+
# the appropriate parser:
|
|
38
|
+
#
|
|
39
|
+
# parser = ICU::Tournament::Krause.new
|
|
40
|
+
# spexport = parser.serialize(tournament)
|
|
41
|
+
#
|
|
42
|
+
# or use the _serialize_ method of the instance with the appropraie format name:
|
|
43
|
+
#
|
|
44
|
+
# spexport = tournament.serialize('SPExport')
|
|
45
|
+
#
|
|
46
|
+
# In either case the method returns a string representation of the tourament in SwissPerfect export
|
|
47
|
+
# format with tab separators, space padding and (by default) the local player ID and total score
|
|
48
|
+
# optional columns:
|
|
49
|
+
#
|
|
50
|
+
# No Name Loc Id Total 1 2 3
|
|
51
|
+
#
|
|
52
|
+
# 1 Griffiths, Ryan-Rhys 6897 3 4:W 2:W 3:W
|
|
53
|
+
# 2 Flynn, Jamie 5226 2 3:W 1:L 4:W
|
|
54
|
+
# 3 Hulleman, Leon 6409 1 2:L 4:W 1:L
|
|
55
|
+
# 4 Dunne, Thomas 10914 0 1:L 3:L 2:L
|
|
56
|
+
#
|
|
57
|
+
# To change which optional columns are output, use the _columns_ option with an array of the column attribute names.
|
|
58
|
+
# The optional attribute names, together with their column header names in SwissPerfect, are as follows:
|
|
59
|
+
#
|
|
60
|
+
# * _fed_: Feder
|
|
61
|
+
# * _fide_: Intl Id
|
|
62
|
+
# * _id_: Loc Id
|
|
63
|
+
# * _fide_: ting_ (Rtg
|
|
64
|
+
# * _rating_: Loc
|
|
65
|
+
# * _title_: Title
|
|
66
|
+
# * _points_: Total
|
|
67
|
+
#
|
|
68
|
+
# So, for example, to omitt the optional columns completely, supply an empty array of column names:
|
|
69
|
+
#
|
|
70
|
+
# tournament.serialize('SPExport', :columns => [])
|
|
71
|
+
#
|
|
72
|
+
# No Name 1 2 3
|
|
73
|
+
#
|
|
74
|
+
# 1 Griffiths, Ryan-Rhys 4:W 2:W 3:W
|
|
75
|
+
# 2 Flynn, Jamie 3:W 1:L 4:W
|
|
76
|
+
# 3 Hulleman, Leon 2:L 4:W 1:L
|
|
77
|
+
# 4 Dunne, Thomas 1:L 3:L 2:L
|
|
78
|
+
#
|
|
79
|
+
# Or supply whatever columns you want, for example:
|
|
80
|
+
#
|
|
81
|
+
# tournament.serialize('SPExport', :columns => [:fide, :fide_rating])
|
|
82
|
+
#
|
|
83
|
+
# Note that the column order in the serialised string is the same as it is in the SwissPerfect application.
|
|
84
|
+
# The order of column names in the _columns_ hash has no effect.
|
|
85
|
+
#
|
|
86
|
+
# The default, when you leave out the _columns_ option is equivalent to:
|
|
87
|
+
#
|
|
88
|
+
# tournament.serialize('SPExport', :columns => [:id, :points])
|
|
89
|
+
#
|
|
90
|
+
# The order of players in the serialized output is always by player number and as a side effect of serialization,
|
|
91
|
+
# the player numbers will be adjusted to ensure they range from 1 to the total number of players maintaining the
|
|
92
|
+
# original order. If you would prefer rank-order instead, then you must first renumber the players by rank (the
|
|
93
|
+
# default renumbering method) before serializing. For example:
|
|
94
|
+
#
|
|
95
|
+
# spexport = tournament.renumber(:rank).serialize('SPExport')
|
|
96
|
+
#
|
|
97
|
+
# Or equivalently, since renumbering by rank is the default, just:
|
|
98
|
+
#
|
|
99
|
+
# spexport = tournament.renumber.serialize('SPExport')
|
|
100
|
+
#
|
|
101
|
+
# You may wish set the tie-break rules before ranking:
|
|
102
|
+
#
|
|
103
|
+
# tournament.tie_breaks = [:buchholz, ::neustadtl]
|
|
104
|
+
# spexport = tournament.rerank.renumber.serialize('SwissPerfect')
|
|
105
|
+
#
|
|
106
|
+
# See ICU::Tournament for more about tie-breaks.
|
|
107
|
+
#
|
|
108
|
+
class SPExport
|
|
109
|
+
attr_reader :error
|
|
110
|
+
|
|
111
|
+
# Parse SwissPerfect export data returning a Tournament on success or raising an exception on error.
|
|
112
|
+
def parse!(spx, arg={})
|
|
113
|
+
@tournament = init_tournament(arg)
|
|
114
|
+
@lineno = 0
|
|
115
|
+
@header = nil
|
|
116
|
+
@results = Array.new
|
|
117
|
+
spx = ICU::Util.to_utf8(spx) unless arg[:is_utf8]
|
|
118
|
+
|
|
119
|
+
# Process each line.
|
|
120
|
+
spx.each_line do |line|
|
|
121
|
+
@lineno += 1
|
|
122
|
+
line.strip! # remove leading and trailing white space
|
|
123
|
+
next if line == '' # skip blank lines
|
|
124
|
+
|
|
125
|
+
if @header
|
|
126
|
+
process_player(line)
|
|
127
|
+
else
|
|
128
|
+
process_header(line)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Now that all players are present, add the results to the tournament.
|
|
133
|
+
@results.each do |r|
|
|
134
|
+
lineno, player, data, result = r
|
|
135
|
+
begin
|
|
136
|
+
@tournament.add_result(result)
|
|
137
|
+
rescue => err
|
|
138
|
+
raise "line #{lineno}, player #{player}, result '#{data}': #{err.message}"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Finally, exercise the tournament object's internal validation, reranking if neccessary.
|
|
143
|
+
@tournament.validate!(:rerank => true)
|
|
144
|
+
|
|
145
|
+
@tournament
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Parse SwissPerfect export text returning a Tournament on success or a nil on failure.
|
|
149
|
+
# In the case of failure, an error message can be retrived via the <em>error</em> method.
|
|
150
|
+
def parse(spx, arg={})
|
|
151
|
+
begin
|
|
152
|
+
parse!(spx, arg)
|
|
153
|
+
rescue => ex
|
|
154
|
+
@error = ex.message
|
|
155
|
+
nil
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Same as <em>parse!</em> except the input is a file name rather than file contents.
|
|
160
|
+
def parse_file!(file, arg={})
|
|
161
|
+
spx = ICU::Util.read_utf8(file)
|
|
162
|
+
arg[:is_utf8] = true
|
|
163
|
+
parse!(spx, arg)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Same as <em>parse</em> except the input is a file name rather than file contents.
|
|
167
|
+
def parse_file(file, arg={})
|
|
168
|
+
begin
|
|
169
|
+
parse_file!(file, arg)
|
|
170
|
+
rescue => ex
|
|
171
|
+
@error = ex.message
|
|
172
|
+
nil
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Serialise a tournament to SwissPerfect text export format.
|
|
177
|
+
def serialize(t, arg={})
|
|
178
|
+
t.validate!(:type => self)
|
|
179
|
+
|
|
180
|
+
# Ensure a nice set of player numbers and get the number of rounds.
|
|
181
|
+
t.renumber(:order)
|
|
182
|
+
rounds = t.last_round
|
|
183
|
+
|
|
184
|
+
# Optional columns.
|
|
185
|
+
optional = arg[:columns] if arg.instance_of?(Hash) && arg[:columns].instance_of?(Array)
|
|
186
|
+
optional = [:id, :points] unless optional
|
|
187
|
+
|
|
188
|
+
# Columns identifiers in SwissPerfect order.
|
|
189
|
+
columns = Array.new
|
|
190
|
+
columns.push(:num)
|
|
191
|
+
columns.push(:name)
|
|
192
|
+
[:fed, :fide, :id, :fide_rating, :rating, :title, :points].each { |x| columns.push(x) if optional.include?(x) }
|
|
193
|
+
|
|
194
|
+
# SwissPerfect headers for each column (other than the rounds, which are treated separately).
|
|
195
|
+
header = Hash.new
|
|
196
|
+
columns.each do |col|
|
|
197
|
+
header[col] = case col
|
|
198
|
+
when :num then "No"
|
|
199
|
+
when :name then "Name"
|
|
200
|
+
when :fed then "Feder"
|
|
201
|
+
when :fide then "Intl Id"
|
|
202
|
+
when :id then "Loc Id"
|
|
203
|
+
when :fide_rating then "Rtg"
|
|
204
|
+
when :rating then "Loc"
|
|
205
|
+
when :title then "Title"
|
|
206
|
+
when :points then "Total"
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Widths and formats for each column.
|
|
211
|
+
width = Hash.new
|
|
212
|
+
format = Hash.new
|
|
213
|
+
columns.each do |col|
|
|
214
|
+
width[col] = t.players.inject(header[col].length) { |l, p| p.send(col).to_s.length > l ? p.send(col).to_s.length : l }
|
|
215
|
+
format[col] = "%-#{width[col]}s"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# The header, followed by a blank line.
|
|
219
|
+
formats = columns.map{ |col| format[col] }
|
|
220
|
+
(1..rounds).each { |r| formats << "%#{width[:num]}d " % r }
|
|
221
|
+
sp = formats.join("\t") % columns.map{ |col| header[col] }
|
|
222
|
+
sp << "\r\n\r\n"
|
|
223
|
+
|
|
224
|
+
# The round formats for players are slightly different to those for the header.
|
|
225
|
+
formats.pop(rounds)
|
|
226
|
+
(1..rounds).each{ |r| formats << "%#{2+width[:num]}s" }
|
|
227
|
+
|
|
228
|
+
# Serialize the formats already.
|
|
229
|
+
formats = formats.join("\t") + "\r\n"
|
|
230
|
+
|
|
231
|
+
# Now add a line for each player.
|
|
232
|
+
t.players.each { |p| sp << p.to_sp_text(rounds, columns, formats) }
|
|
233
|
+
|
|
234
|
+
# And return the whole lot.
|
|
235
|
+
sp
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Additional tournament validation rules for this specific type.
|
|
239
|
+
def validate!(t)
|
|
240
|
+
# None.
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# :enddoc:
|
|
244
|
+
private
|
|
245
|
+
|
|
246
|
+
def init_tournament(arg)
|
|
247
|
+
raise "tournament name missing" unless arg[:name]
|
|
248
|
+
raise "tournament start date missing" unless arg[:start]
|
|
249
|
+
Tournament.new(arg[:name], arg[:start])
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def process_header(line)
|
|
253
|
+
raise "header should always start with 'No'" unless line.match(/^No\s/)
|
|
254
|
+
items = line.split(/\t/).map(&:strip)
|
|
255
|
+
raise "header requires tab separators" unless items.size > 2
|
|
256
|
+
@header = Hash.new
|
|
257
|
+
@rounds = 1
|
|
258
|
+
items.each_with_index do |item, i|
|
|
259
|
+
key = case item
|
|
260
|
+
when 'No' then :num
|
|
261
|
+
when 'Name' then :name
|
|
262
|
+
when 'Total' then :total
|
|
263
|
+
when 'Loc Id' then :id
|
|
264
|
+
when 'Intl Id' then :fide
|
|
265
|
+
when 'Title' then :title
|
|
266
|
+
when 'Feder' then :fed
|
|
267
|
+
when 'Loc' then :rating
|
|
268
|
+
when 'Rtg' then :fide_rating
|
|
269
|
+
when /^[1-9]\d*$/
|
|
270
|
+
round = item.to_i
|
|
271
|
+
@rounds = round if round > @rounds
|
|
272
|
+
round
|
|
273
|
+
else nil
|
|
274
|
+
end
|
|
275
|
+
@header[key] = i if key
|
|
276
|
+
end
|
|
277
|
+
raise "header is missing 'No'" unless @header[:num]
|
|
278
|
+
raise "header is missing 'Name'" unless @header[:name]
|
|
279
|
+
(1..@rounds).each { |r| raise "header is missing round #{r}" unless @header[r] }
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def process_player(line)
|
|
283
|
+
items = line.split(/\t/).map(&:strip)
|
|
284
|
+
raise "line #{@lineno} has too few items" unless items.size > 2
|
|
285
|
+
|
|
286
|
+
# Player details.
|
|
287
|
+
num = items[@header[:num]]
|
|
288
|
+
name = Name.new(items[@header[:name]])
|
|
289
|
+
opt = Hash.new
|
|
290
|
+
[:fed, :title, :id, :fide, :rating, :fide_rating].each do |key|
|
|
291
|
+
if @header[key]
|
|
292
|
+
val = items[@header[key]]
|
|
293
|
+
opt[key] = val unless val.nil? || val == ''
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Create the player and add it to the tournament.
|
|
298
|
+
player = Player.new(name.first, name.last, num, opt)
|
|
299
|
+
@tournament.add_player(player)
|
|
300
|
+
|
|
301
|
+
# Save the results for later processing.
|
|
302
|
+
points = items[@header[:total]] if @header[:total]
|
|
303
|
+
points = nil if points == ''
|
|
304
|
+
points = points.to_f if points
|
|
305
|
+
total = 0.0;
|
|
306
|
+
(1..@rounds).each do |r|
|
|
307
|
+
total+= process_result(r, player.num, items[@header[r]])
|
|
308
|
+
end
|
|
309
|
+
total = points if points && fix_invisible_bonuses(player.num, points - total)
|
|
310
|
+
raise "declared points total (#{points}) does not agree with total from summed results (#{total})" if points && points != total
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def process_result(round, player_num, data)
|
|
314
|
+
raise "illegal result (#{data})" unless data.match(/^(0|[1-9]\d*)?:([-+=LWD])?$/i)
|
|
315
|
+
opponent = $1.to_i
|
|
316
|
+
score = $2 || 'L'
|
|
317
|
+
options = Hash.new
|
|
318
|
+
options[:opponent] = opponent unless opponent == 0
|
|
319
|
+
options[:rateable] = false unless score && score.match(/^(W|L|D)$/i)
|
|
320
|
+
result = Result.new(round, player_num, score, options)
|
|
321
|
+
@results << [@lineno, player_num, data, result]
|
|
322
|
+
result.points
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def fix_invisible_bonuses(player_num, difference)
|
|
326
|
+
# We don't need to fix it if it's not broken.
|
|
327
|
+
return false if difference == 0.0
|
|
328
|
+
# We can't fix a summed total that is greater than the declared total.
|
|
329
|
+
return false if difference < 0.0
|
|
330
|
+
# Get the player's results objects from the temporary store.
|
|
331
|
+
results = @results.select{ |r| r[1] == player_num }.map{ |r| r.last }
|
|
332
|
+
# Get all losses and draws that don't have opponents (because their scores can be harmlessly altered).
|
|
333
|
+
losses = results.reject{ |r| r.opponent || r.score != 'L' }.sort{ |a,b| a.round <=> b.round }
|
|
334
|
+
draws = results.reject{ |r| r.opponent || r.score != 'D' }.sort{ |a,b| a.round <=> b.round }
|
|
335
|
+
# Give up unless these results have enough capacity to accomodate the points difference.
|
|
336
|
+
return false unless difference <= 1.0 * losses.size + 0.5 * draws.size
|
|
337
|
+
# Start promoting losses to draws.
|
|
338
|
+
losses.each do |loss|
|
|
339
|
+
loss.score = 'D'
|
|
340
|
+
difference -= 0.5
|
|
341
|
+
break if difference == 0.0
|
|
342
|
+
end
|
|
343
|
+
# If that's not enough, start promoting draws to wins.
|
|
344
|
+
if difference > 0.0
|
|
345
|
+
draws.each do |draw|
|
|
346
|
+
draw.score = 'W'
|
|
347
|
+
difference -= 0.5
|
|
348
|
+
break if difference == 0.0
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
# And if that's not enough, start promoting losses to wins.
|
|
352
|
+
if difference > 0.0
|
|
353
|
+
losses.each do |loss|
|
|
354
|
+
loss.score = 'W'
|
|
355
|
+
difference -= 0.5
|
|
356
|
+
break if difference == 0.0
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
# Signal success.
|
|
360
|
+
return true
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
class Player
|
|
366
|
+
# Format a player's record as it would appear in an SP export file.
|
|
367
|
+
def to_sp_text(rounds, columns, formats)
|
|
368
|
+
values = columns.inject([]) do |vals,col|
|
|
369
|
+
val = send(col).to_s
|
|
370
|
+
val.sub!(/\.0/, '') if col == :points
|
|
371
|
+
vals << val
|
|
372
|
+
end
|
|
373
|
+
(1..rounds).each do |r|
|
|
374
|
+
result = find_result(r)
|
|
375
|
+
values << (result ? result.to_sp_text : " : ")
|
|
376
|
+
end
|
|
377
|
+
formats % values
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
class Result
|
|
382
|
+
# Format a player's result as it would appear in an SP export file.
|
|
383
|
+
def to_sp_text
|
|
384
|
+
sp = opponent ? opponent.to_s : '0'
|
|
385
|
+
sp << ':'
|
|
386
|
+
if rateable
|
|
387
|
+
sp << score
|
|
388
|
+
else
|
|
389
|
+
sp << case score
|
|
390
|
+
when 'W' then '+'
|
|
391
|
+
when 'L' then '-'
|
|
392
|
+
else '='
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
end
|