icu_tournament 1.1.2 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +3 -8
- data/lib/icu_tournament.rb +4 -1
- data/lib/icu_tournament/federation.rb +66 -72
- data/lib/icu_tournament/player.rb +97 -103
- data/lib/icu_tournament/result.rb +82 -89
- data/lib/icu_tournament/team.rb +45 -52
- data/lib/icu_tournament/tournament.rb +153 -163
- data/lib/icu_tournament/tournament_fcsv.rb +123 -129
- data/lib/icu_tournament/tournament_krause.rb +107 -113
- data/lib/icu_tournament/tournament_sp.rb +93 -99
- data/lib/icu_tournament/util.rb +22 -41
- data/lib/icu_tournament/version.rb +3 -1
- metadata +23 -26
- data/lib/icu_tournament/name.rb +0 -274
- data/spec/name_spec.rb +0 -208
data/README.rdoc
CHANGED
@@ -5,15 +5,12 @@ For reading or writing files of chess tournament data. Original project name on
|
|
5
5
|
|
6
6
|
== Install
|
7
7
|
|
8
|
-
|
8
|
+
For ruby 1.9.2 (version 1.1.2 was the last compatible with ruby 1.8.7).
|
9
9
|
|
10
|
-
|
10
|
+
gem install icu_tournament
|
11
11
|
|
12
|
+
For name canonicalisation, _icu_name_ is required.
|
12
13
|
For handling SwissPerfect files the _dbf_, _inifile_ and _rubyzip_ gems are required.
|
13
|
-
For Ruby prior to version 1.9 the _fastercsv_ gem is needed to handle CSV files.
|
14
|
-
|
15
|
-
Tested with ruby 1.8.7, 1.9.2.
|
16
|
-
|
17
14
|
|
18
15
|
== Usage
|
19
16
|
|
@@ -31,7 +28,6 @@ The currently supported formats are:
|
|
31
28
|
* ICU::Tournament::ForeignCSV - used by Irish players to report their individual results in foreign tournaments.
|
32
29
|
* ICU::Tournament::SwissPerfect - often used by Irish tournament controllers to report results.
|
33
30
|
|
34
|
-
|
35
31
|
== Writing Files
|
36
32
|
|
37
33
|
Here's how the 1972 Fischer-Spassky match could be formatted to Krause. First a tournament object is created
|
@@ -60,7 +56,6 @@ Then finally, to create the file:
|
|
60
56
|
|
61
57
|
open('match.txt', 'w') { |f| f.puts @t.serialize('Krause') }
|
62
58
|
|
63
|
-
|
64
59
|
== Reading Files
|
65
60
|
|
66
61
|
Suppose you have a tournament file in Krause format. Parse it into a tournament object like this:
|
data/lib/icu_tournament.rb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
# :enddoc:
|
2
2
|
|
3
|
+
require 'rubygems'
|
4
|
+
require 'icu_name'
|
5
|
+
|
3
6
|
icu_tournament_files = Array.new
|
4
|
-
icu_tournament_files.concat %w{util
|
7
|
+
icu_tournament_files.concat %w{util federation}
|
5
8
|
icu_tournament_files.concat %w{player result team tournament}
|
6
9
|
icu_tournament_files.concat %w{fcsv krause sp}.map{ |f| "tournament_#{f}"}
|
7
10
|
|
@@ -1,73 +1,67 @@
|
|
1
1
|
module ICU
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
fed.
|
21
|
-
|
22
|
-
If the string is
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
ICU::Federation.find('
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
ICU::Federation.find('
|
39
|
-
|
40
|
-
The method
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
ICU::Federation.menu(:
|
54
|
-
|
55
|
-
To
|
56
|
-
|
57
|
-
ICU::Federation.menu(:
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
ICU::Federation.menu(:none => 'None') # => [['None', ''], ['Afghanistan', 'AFG], ...]
|
62
|
-
|
63
|
-
The "None" option's code is the empty string and it come above the "top" option if both are specified.
|
64
|
-
|
65
|
-
=end
|
66
|
-
|
2
|
+
#
|
3
|
+
# This class can be used to map a string into an object representing a chess federation.
|
4
|
+
# In FIDE, chess federations are generally either referred to by their full names such as
|
5
|
+
# _Ireland_ or _Russia_ or by three letter codes such as _IRL_ or _RUS_. The three letter
|
6
|
+
# codes are mostly the same as those found in the international standard known as
|
7
|
+
# {ISO 3166-1 alpha-3}[http://en.wikipedia.org/wiki/ISO_3166-1_alpha-3], but with
|
8
|
+
# some differences (e.g. for England, Scotland and Wales).
|
9
|
+
#
|
10
|
+
# You cannot directly create instances of this class using _new_. Instead, you supply
|
11
|
+
# a string to the class method _find_ and, if the string supplied uniguely identifies a
|
12
|
+
# federation, an instance is returned which responds to _name_ and _code_.
|
13
|
+
#
|
14
|
+
# fed = ICU::Federation.find('IRL')
|
15
|
+
# fed.name # => "Ireland"
|
16
|
+
# fed.code # => "IRL"
|
17
|
+
#
|
18
|
+
# If the string is not sufficient to identify a federation, the _find_ method returns _nil_.
|
19
|
+
#
|
20
|
+
# fed = ICU::Federation.find('ZYX') # => nil
|
21
|
+
#
|
22
|
+
# If the string is three letters long and matches (case insenstively) one of the unique
|
23
|
+
# federation codes, then the instance corresponding to that federation is returned.
|
24
|
+
#
|
25
|
+
# ICU::Federation.find('rUs').code # => "RUS"
|
26
|
+
#
|
27
|
+
# If the string is more than three letters long and if it is a substring (case insensitive)
|
28
|
+
# of exactly one federation name, then that federation is returned.
|
29
|
+
#
|
30
|
+
# ICU::Federation.find('ongoli').name # => "Mongolia"
|
31
|
+
#
|
32
|
+
# In all other cases, nil is returned. In the following example, the string matches more than one federation.
|
33
|
+
#
|
34
|
+
# ICU::Federation.find('land') # => nil
|
35
|
+
#
|
36
|
+
# The method is not fooled by irrelevant white space.
|
37
|
+
#
|
38
|
+
# ICU::Federation.find(' united states ').code # => 'USA'
|
39
|
+
#
|
40
|
+
# The class method _menu_ will return an array of two-element arrays each of which contain a name
|
41
|
+
# and a code.
|
42
|
+
#
|
43
|
+
# ICU::Federation.menu # => [['Afghanistan', 'AFG'], ['Albania', 'ALB], ...]
|
44
|
+
#
|
45
|
+
# Such an array could be used, for example, as the basis of a selection menu in a web application.
|
46
|
+
# Various options are available to alter the array returned. Use the _:order_ option to order by code
|
47
|
+
# instead of the default (by country name).
|
48
|
+
#
|
49
|
+
# ICU::Federation.menu(:order => 'code') # => [..., ['Ireland', 'IRL'], ['Iraq', 'IRQ], ...]
|
50
|
+
#
|
51
|
+
# To put one country at the top (followed by the rest, in order) supply the country's code with the _:top_ option:
|
52
|
+
#
|
53
|
+
# ICU::Federation.menu(:top => 'IRL') # => [['Ireland', 'IRL'], ['Afghanistan', 'AFG], ...]
|
54
|
+
#
|
55
|
+
# To supply an extra "None" item at the top, specify its label with the _:none_ option:
|
56
|
+
#
|
57
|
+
# ICU::Federation.menu(:none => 'None') # => [['None', ''], ['Afghanistan', 'AFG], ...]
|
58
|
+
#
|
59
|
+
# The "None" option's code is the empty string and it come above the "top" option if both are specified.
|
60
|
+
#
|
67
61
|
class Federation
|
68
62
|
attr_reader :code, :name
|
69
63
|
private_class_method :new
|
70
|
-
|
64
|
+
|
71
65
|
# Given a code, name or part of a name, return the corresponding federation instance.
|
72
66
|
# If there is no match or more than one match, _nil_ is returned.
|
73
67
|
def self.find(str=nil)
|
@@ -86,7 +80,7 @@ The "None" option's code is the empty string and it come above the "top" option
|
|
86
80
|
return nil unless matches.length == 1
|
87
81
|
matches[0]
|
88
82
|
end
|
89
|
-
|
83
|
+
|
90
84
|
def self.menu(opts = {})
|
91
85
|
compile unless @@objects;
|
92
86
|
top, menu = nil, []
|
@@ -96,14 +90,14 @@ The "None" option's code is the empty string and it come above the "top" option
|
|
96
90
|
menu.unshift([opts[:none], '']) if opts[:none]
|
97
91
|
menu
|
98
92
|
end
|
99
|
-
|
93
|
+
|
100
94
|
def initialize(code, name) # :nodoc: because new is private
|
101
95
|
@code = code
|
102
96
|
@name = name
|
103
97
|
end
|
104
|
-
|
98
|
+
|
105
99
|
private
|
106
|
-
|
100
|
+
|
107
101
|
def self.compile
|
108
102
|
return if @@objects
|
109
103
|
@@names = Hash.new
|
@@ -118,10 +112,10 @@ The "None" option's code is the empty string and it come above the "top" option
|
|
118
112
|
end
|
119
113
|
end
|
120
114
|
end
|
121
|
-
|
115
|
+
|
122
116
|
# The data structures compiled.
|
123
117
|
@@objects, @@codes, @@names = nil, nil, nil
|
124
|
-
|
118
|
+
|
125
119
|
# An array of data that gets compiled into other data structures.
|
126
120
|
@@data =
|
127
121
|
[
|
@@ -1,100 +1,94 @@
|
|
1
1
|
module ICU
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
bobby
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
john1
|
54
|
-
john2
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
mark1
|
64
|
-
mark2
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
fox1.fed # => 'IRL'
|
85
|
-
fox1.gender # => 'M'
|
86
|
-
|
87
|
-
=end
|
88
|
-
|
2
|
+
#
|
3
|
+
# A player in a tournament must have a first name, a last name and a number
|
4
|
+
# which is unique in the tournament but otherwise arbitary.
|
5
|
+
#
|
6
|
+
# bobby = ICU::Player.new('robert j', 'fischer', 17)
|
7
|
+
#
|
8
|
+
# Names are automatically cannonicalised (tidied up).
|
9
|
+
#
|
10
|
+
# bobby.first_name # => 'Robert J.'
|
11
|
+
# bobby.last_name # => 'Fischer'
|
12
|
+
#
|
13
|
+
# In addition, players have a number of optional attributes which can be specified
|
14
|
+
# via setters or in constructor hash options: _id_ (local or national ID), _fide_
|
15
|
+
# (FIDE ID), _fed_ (federation), _title_, _rating_, _rank_ and _dob_ (date of birth).
|
16
|
+
#
|
17
|
+
# peter = ICU::Player.new('Peter', 'Svidler', 21, :fed => 'rus', :title => 'g', :rating = 2700)
|
18
|
+
# peter.dob = '17th June, 1976'
|
19
|
+
# peter.rank = 1
|
20
|
+
#
|
21
|
+
# Some of these values will also be canonicalised to some extent. For example,
|
22
|
+
# date of birth will be turned into _yyyy-mm-dd_ format, the chess title will be two
|
23
|
+
# to three capital letters always ending in _M_ and the federation, if it's three
|
24
|
+
# letters long, will be upcased.
|
25
|
+
#
|
26
|
+
# peter.dob # => 1976-07-17
|
27
|
+
# peter.title # => 'GM'
|
28
|
+
# peter.fed # => 'RUS'
|
29
|
+
#
|
30
|
+
# It is preferable to add results (ICU::Result) to a player via the tournament (ICU::Tournament) object's
|
31
|
+
# _add_result_ method rather than the method of the same name belonging to player instances. Doing so
|
32
|
+
# allows mirrored results to be added to both players with one call (e.g. one player won, so the
|
33
|
+
# other lost). A player's results can later be retieved via the _results_ accessor.
|
34
|
+
#
|
35
|
+
# Total scores is available via the _points_ method.
|
36
|
+
#
|
37
|
+
# peter.points # => 5.5
|
38
|
+
#
|
39
|
+
# A player can have up to two ID numbers (both positive integers or nil): _id_ (local or national ID,
|
40
|
+
# such as ICU number) and _fide_ (FIDE ID).
|
41
|
+
#
|
42
|
+
# peter.id = 16790 # ICU
|
43
|
+
# peter.fide = 4102142 # FIDE
|
44
|
+
#
|
45
|
+
# Players can be compared to see if they're roughly or exactly the same, which may be useful in detecting duplicates.
|
46
|
+
# If the names match and the federations don't disagree then two players are equal according to the _==_ operator.
|
47
|
+
# The player number is irrelevant.
|
48
|
+
#
|
49
|
+
# john1 = ICU::Player.new('John', 'Smith', 12)
|
50
|
+
# john2 = ICU::Player.new('John', 'Smith', 22, :fed = 'IRL')
|
51
|
+
# john2 = ICU::Player.new('John', 'Smith', 32, :fed = 'ENG')
|
52
|
+
#
|
53
|
+
# john1 == john2 # => true (federations don't disagree because one is unset)
|
54
|
+
# john2 == john3 # => false (federations disagree)
|
55
|
+
#
|
56
|
+
# If, in addition, _rating_, _dob_, _gender_, _id_ and _fide_ do not disagree then two players are equal
|
57
|
+
# according to the stricter criteria of _eql?_.
|
58
|
+
#
|
59
|
+
# mark1 = ICU::Player.new('Mark', 'Orr', 31, :fed = 'IRL', :rating => 2100)
|
60
|
+
# mark2 = ICU::Player.new('Mark', 'Orr', 33, :fed = 'IRL', :rating => 2100, :title => 'IM')
|
61
|
+
# mark3 = ICU::Player.new('Mark', 'Orr', 37, :fed = 'IRL', :rating => 2200, :title => 'IM')
|
62
|
+
#
|
63
|
+
# mark1.eql?(mark2) # => true (ratings agree and titles don't disagree)
|
64
|
+
# mark2.eql?(mark3) # => false (the ratings are not the same)
|
65
|
+
#
|
66
|
+
# The presence of two players in the same tournament that are equal according to _==_ but unequal
|
67
|
+
# according to _eql?__ is likely to indicate a data entry error.
|
68
|
+
#
|
69
|
+
# If two instances represent the same player and are equal according to _==_ then the _id_, _fide_, _rating_,
|
70
|
+
# _title_ and _fed_ attributes of the two can be merged. For example:
|
71
|
+
#
|
72
|
+
# fox1 = ICU::Player.new('Tony', 'Fox', 12, :id => 456)
|
73
|
+
# fox2 = ICU::Player.new('Tony', 'Fox', 21, :rating => 2100, :fed => 'IRL', :gender => 'M')
|
74
|
+
# fox1.merge(fox2)
|
75
|
+
#
|
76
|
+
# Any attributes present in the second player but not in the first are copied to the first.
|
77
|
+
# All other attributes are unaffected.
|
78
|
+
#
|
79
|
+
# fox1.rating # => 2100
|
80
|
+
# fox1.fed # => 'IRL'
|
81
|
+
# fox1.gender # => 'M'
|
82
|
+
#
|
83
|
+
#
|
89
84
|
class Player
|
90
|
-
|
91
85
|
extend ICU::Accessor
|
92
86
|
attr_integer :num
|
93
87
|
attr_positive_or_nil :id, :fide, :rating, :rank
|
94
88
|
attr_date_or_nil :dob
|
95
|
-
|
89
|
+
|
96
90
|
attr_reader :results, :first_name, :last_name, :fed, :title, :gender
|
97
|
-
|
91
|
+
|
98
92
|
# Constructor. Must supply both names and a unique number for the tournament.
|
99
93
|
def initialize(first_name, last_name, num, opt={})
|
100
94
|
self.first_name = first_name
|
@@ -105,33 +99,33 @@ All other attributes are unaffected.
|
|
105
99
|
end
|
106
100
|
@results = []
|
107
101
|
end
|
108
|
-
|
102
|
+
|
109
103
|
# Canonicalise and set the first name(s).
|
110
104
|
def first_name=(first_name)
|
111
105
|
name = Name.new(first_name, 'Last')
|
112
106
|
raise "invalid first name" unless name.first.length > 0
|
113
107
|
@first_name = name.first
|
114
108
|
end
|
115
|
-
|
109
|
+
|
116
110
|
# Canonicalise and set the last name(s).
|
117
111
|
def last_name=(last_name)
|
118
112
|
name = Name.new('First', last_name)
|
119
113
|
raise "invalid last name" unless name.last.length > 0 && name.first.length > 0
|
120
114
|
@last_name = name.last
|
121
115
|
end
|
122
|
-
|
116
|
+
|
123
117
|
# Return the full name, last name first.
|
124
118
|
def name
|
125
119
|
"#{last_name}, #{first_name}"
|
126
120
|
end
|
127
|
-
|
121
|
+
|
128
122
|
# Federation. Is either unknown (nil) or a string containing at least three letters.
|
129
123
|
def fed=(fed)
|
130
124
|
obj = Federation.find(fed)
|
131
125
|
@fed = obj ? obj.code : nil
|
132
126
|
raise "invalid federation (#{fed})" if @fed.nil? && fed.to_s.strip.length > 0
|
133
127
|
end
|
134
|
-
|
128
|
+
|
135
129
|
# Chess title. Is either unknown (nil) or one of: _GM_, _IM_, _FM_, _CM_, _NM_,
|
136
130
|
# or any of these preceeded by the letter _W_.
|
137
131
|
def title=(title)
|
@@ -142,7 +136,7 @@ All other attributes are unaffected.
|
|
142
136
|
@title = nil if @title == ''
|
143
137
|
raise "invalid chess title (#{title})" unless @title.nil? || @title.match(/^W?[GIFCN]M$/)
|
144
138
|
end
|
145
|
-
|
139
|
+
|
146
140
|
# Gender. Is either unknown (nil) or one of _M_ or _F_.
|
147
141
|
def gender=(gender)
|
148
142
|
@gender = gender.to_s.strip[0,1].upcase
|
@@ -150,7 +144,7 @@ All other attributes are unaffected.
|
|
150
144
|
@gender = 'F' if @gender == 'W'
|
151
145
|
raise "invalid gender (#{gender})" unless @gender.nil? || @gender.match(/^[MF]$/)
|
152
146
|
end
|
153
|
-
|
147
|
+
|
154
148
|
# Add a result. Don't use this method directly - use ICU::Tournament#add_result instead.
|
155
149
|
def add_result(result)
|
156
150
|
raise "invalid result" unless result.class == ICU::Result
|
@@ -165,17 +159,17 @@ All other attributes are unaffected.
|
|
165
159
|
@results.insert(i, result)
|
166
160
|
end
|
167
161
|
end
|
168
|
-
|
162
|
+
|
169
163
|
# Lookup a result by round number.
|
170
164
|
def find_result(round)
|
171
165
|
@results.find { |r| r.round == round }
|
172
166
|
end
|
173
|
-
|
167
|
+
|
174
168
|
# Return the player's total points.
|
175
169
|
def points
|
176
170
|
@results.inject(0.0) { |t, r| t += r.points }
|
177
171
|
end
|
178
|
-
|
172
|
+
|
179
173
|
# Renumber the player according to the supplied hash. Return self.
|
180
174
|
def renumber(map)
|
181
175
|
raise "player number #{@num} not found in renumbering hash" unless map[@num]
|
@@ -183,7 +177,7 @@ All other attributes are unaffected.
|
|
183
177
|
@results.each{ |r| r.renumber(map) }
|
184
178
|
self
|
185
179
|
end
|
186
|
-
|
180
|
+
|
187
181
|
# Loose equality test. Passes if the names match and the federations are not different.
|
188
182
|
def ==(other)
|
189
183
|
return true if equal?(other)
|
@@ -193,7 +187,7 @@ All other attributes are unaffected.
|
|
193
187
|
return false if @fed && other.fed && @fed != other.fed
|
194
188
|
true
|
195
189
|
end
|
196
|
-
|
190
|
+
|
197
191
|
# Strict equality test. Passes if the playes are loosly equal and also if their IDs, rating, gender and title are not different.
|
198
192
|
def eql?(other)
|
199
193
|
return true if equal?(other)
|
@@ -203,7 +197,7 @@ All other attributes are unaffected.
|
|
203
197
|
end
|
204
198
|
true
|
205
199
|
end
|
206
|
-
|
200
|
+
|
207
201
|
# Merge in some of the details of another player.
|
208
202
|
def merge(other)
|
209
203
|
raise "cannot merge two players that are not equal" unless self == other
|