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 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
- The gem is hosted on gemcutter only:
8
+ For ruby 1.9.2 (version 1.1.2 was the last compatible with ruby 1.8.7).
9
9
 
10
- sudo gem install icu_tournament
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:
@@ -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 name federation}
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
- =begin rdoc
4
-
5
- == Federations
6
-
7
- This class can be used to map a string into an object representing a chess federation.
8
- In FIDE, chess federations are generally either referred to by their full names such as
9
- _Ireland_ or _Russia_ or by three letter codes such as _IRL_ or _RUS_. The three letter
10
- codes are mostly the same as those found in the international standard known as
11
- {ISO 3166-1 alpha-3}[http://en.wikipedia.org/wiki/ISO_3166-1_alpha-3], but with
12
- some differences (e.g. for England, Scotland and Wales).
13
-
14
- You cannot directly create instances of this class using _new_. Instead, you supply
15
- a string to the class method _find_ and, if the string supplied uniguely identifies a
16
- federation, an instance is returned which responds to _name_ and _code_.
17
-
18
- fed = ICU::Federation.find('IRL')
19
- fed.name # => "Ireland"
20
- fed.code # => "IRL"
21
-
22
- If the string is not sufficient to identify a federation, the _find_ method returns _nil_.
23
-
24
- fed = ICU::Federation.find('ZYX') # => nil
25
-
26
- If the string is three letters long and matches (case insenstively) one of the unique
27
- federation codes, then the instance corresponding to that federation is returned.
28
-
29
- ICU::Federation.find('rUs').code # => "RUS"
30
-
31
- If the string is more than three letters long and if it is a substring (case insensitive)
32
- of exactly one federation name, then that federation is returned.
33
-
34
- ICU::Federation.find('ongoli').name # => "Mongolia"
35
-
36
- In all other cases, nil is returned. In the following example, the string matches more than one federation.
37
-
38
- ICU::Federation.find('land') # => nil
39
-
40
- The method is not fooled by irrelevant white space.
41
-
42
- ICU::Federation.find(' united states ').code # => 'USA'
43
-
44
- The class method _menu_ will return an array of two-element arrays each of which contain a name
45
- and a code.
46
-
47
- ICU::Federation.menu # => [['Afghanistan', 'AFG'], ['Albania', 'ALB], ...]
48
-
49
- Such an array could be used, for example, as the basis of a selection menu in a web application.
50
- Various options are available to alter the array returned. Use the _:order_ option to order by code
51
- instead of the default (by country name).
52
-
53
- ICU::Federation.menu(:order => 'code') # => [..., ['Ireland', 'IRL'], ['Iraq', 'IRQ], ...]
54
-
55
- To put one country at the top (followed by the rest, in order) supply the country's code with the _:top_ option:
56
-
57
- ICU::Federation.menu(:top => 'IRL') # => [['Ireland', 'IRL'], ['Afghanistan', 'AFG], ...]
58
-
59
- To supply an extra "None" item at the top, specify its label with the _:none_ option:
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
- =begin rdoc
4
-
5
- == Player
6
-
7
- A player in a tournament must have a first name, a last name and a number
8
- which is unique in the tournament but otherwise arbitary.
9
-
10
- bobby = ICU::Player.new('robert j', 'fischer', 17)
11
-
12
- Names are automatically cannonicalised (tidied up).
13
-
14
- bobby.first_name # => 'Robert J.'
15
- bobby.last_name # => 'Fischer'
16
-
17
- In addition, players have a number of optional attributes which can be specified
18
- via setters or in constructor hash options: _id_ (local or national ID), _fide_
19
- (FIDE ID), _fed_ (federation), _title_, _rating_, _rank_ and _dob_ (date of birth).
20
-
21
- peter = ICU::Player.new('Peter', 'Svidler', 21, :fed => 'rus', :title => 'g', :rating = 2700)
22
- peter.dob = '17th June, 1976'
23
- peter.rank = 1
24
-
25
- Some of these values will also be canonicalised to some extent. For example,
26
- date of birth will be turned into _yyyy-mm-dd_ format, the chess title will be two
27
- to three capital letters always ending in _M_ and the federation, if it's three
28
- letters long, will be upcased.
29
-
30
- peter.dob # => 1976-07-17
31
- peter.title # => 'GM'
32
- peter.fed # => 'RUS'
33
-
34
- It is preferable to add results (ICU::Result) to a player via the tournament (ICU::Tournament) object's
35
- _add_result_ method rather than the method of the same name belonging to player instances. Doing so
36
- allows mirrored results to be added to both players with one call (e.g. one player won, so the
37
- other lost). A player's results can later be retieved via the _results_ accessor.
38
-
39
- Total scores is available via the _points_ method.
40
-
41
- peter.points # => 5.5
42
-
43
- A player can have up to two ID numbers (both positive integers or nil): _id_ (local or national ID,
44
- such as ICU number) and _fide_ (FIDE ID).
45
-
46
- peter.id = 16790 # ICU
47
- peter.fide = 4102142 # FIDE
48
-
49
- Players can be compared to see if they're roughly or exactly the same, which may be useful in detecting duplicates.
50
- If the names match and the federations don't disagree then two players are equal according to the _==_ operator.
51
- The player number is irrelevant.
52
-
53
- john1 = ICU::Player.new('John', 'Smith', 12)
54
- john2 = ICU::Player.new('John', 'Smith', 22, :fed = 'IRL')
55
- john2 = ICU::Player.new('John', 'Smith', 32, :fed = 'ENG')
56
-
57
- john1 == john2 # => true (federations don't disagree because one is unset)
58
- john2 == john3 # => false (federations disagree)
59
-
60
- If, in addition, _rating_, _dob_, _gender_, _id_ and _fide_ do not disagree then two players are equal
61
- according to the stricter criteria of _eql?_.
62
-
63
- mark1 = ICU::Player.new('Mark', 'Orr', 31, :fed = 'IRL', :rating => 2100)
64
- mark2 = ICU::Player.new('Mark', 'Orr', 33, :fed = 'IRL', :rating => 2100, :title => 'IM')
65
- mark3 = ICU::Player.new('Mark', 'Orr', 37, :fed = 'IRL', :rating => 2200, :title => 'IM')
66
-
67
- mark1.eql?(mark2) # => true (ratings agree and titles don't disagree)
68
- mark2.eql?(mark3) # => false (the ratings are not the same)
69
-
70
- The presence of two players in the same tournament that are equal according to _==_ but unequal
71
- according to _eql?__ is likely to indicate a data entry error.
72
-
73
- If two instances represent the same player and are equal according to _==_ then the _id_, _fide_, _rating_,
74
- _title_ and _fed_ attributes of the two can be merged. For example:
75
-
76
- fox1 = ICU::Player.new('Tony', 'Fox', 12, :id => 456)
77
- fox2 = ICU::Player.new('Tony', 'Fox', 21, :rating => 2100, :fed => 'IRL', :gender => 'M')
78
- fox1.merge(fox2)
79
-
80
- Any attributes present in the second player but not in the first are copied to the first.
81
- All other attributes are unaffected.
82
-
83
- fox1.rating # => 2100
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