icu_tournament 0.8.9

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.
@@ -0,0 +1,274 @@
1
+ module ICU
2
+
3
+ =begin rdoc
4
+
5
+ == Names
6
+
7
+ This class exists for two main reasons:
8
+
9
+ * to normalise to a common format the different ways names are typed in practice
10
+ * to be able to match two names even if they are not exactly the same
11
+
12
+ To create a name object, supply both the first and second names separately to the constructor.
13
+
14
+ robert = ICU::Name.new(' robert j ', ' FISHER ')
15
+
16
+ Capitalisation, white space and punctuation will all be automatically corrected:
17
+
18
+ robert.name # => 'Robert J. Fischer'
19
+ robert.rname # => 'Fischer, Robert J.' (reversed name)
20
+
21
+ To avoid ambiguity when either the first or second names consist of multiple words, it is better to
22
+ supply the two separately, if known. However, the full name can be supplied alone to the constructor
23
+ and a guess will be made as to the first and last names.
24
+
25
+ bobby = ICU::Name.new(' bobby fischer ')
26
+
27
+ bobby.first # => 'Bobby'
28
+ bobby.last # => 'Fischer'
29
+
30
+ Names will match even if one is missing middle initials or if a nickname is used for one of the first names.
31
+
32
+ bobby.match(robert) # => true
33
+
34
+ Note that the class is aware of only common nicknames (e.g. _Bobby_ and _Robert_, _Bill_ and _William_, etc), not all possibilities.
35
+
36
+ Supplying the _match_ method with strings is equivalent to instantiating a Name instance with the same
37
+ strings and then matching it. So, for example the following are equivalent:
38
+
39
+ robert.match('R. J.', 'Fischer') # => true
40
+ robert.match(ICU::Name('R. J.', 'Fischer')) # => true
41
+
42
+ In those examples, the inital _R_ matches the first letter of _Robert_. However, nickname matches will not
43
+ always work with initials. In the next example, the initial _R_ does not match the first letter _B_ of the
44
+ nickname _Bobby_.
45
+
46
+ bobby.match('R. J.', 'Fischer') # => false
47
+
48
+ Some of the ways last names are canonicalised are illustrated below:
49
+
50
+ ICU::Name.new('John', 'O Reilly').last # => "O'Reilly"
51
+ ICU::Name.new('dave', 'mcmanus').last # => "McManus"
52
+ ICU::Name.new('pete', 'MACMANUS').last # => "MacManus"
53
+
54
+ =end
55
+
56
+ class Name
57
+ attr_reader :first, :last
58
+
59
+ def initialize(name1='', name2='')
60
+ @name1 = name1.to_s
61
+ @name2 = name2.to_s
62
+ canonicalize
63
+ end
64
+
65
+ def name
66
+ name = ''
67
+ name << @first
68
+ name << ' ' if @first.length > 0 && @last.length > 0
69
+ name << @last
70
+ name
71
+ end
72
+
73
+ def rname
74
+ name = ''
75
+ name << @last
76
+ name << ', ' if @first.length > 0 && @last.length > 0
77
+ name << @first
78
+ name
79
+ end
80
+
81
+ def to_s
82
+ rname
83
+ end
84
+
85
+ def match(name1='', name2='')
86
+ other = Name.new(name1, name2)
87
+ match_first(first, other.first) && match_last(last, other.last)
88
+ end
89
+
90
+ private
91
+
92
+ def canonicalize
93
+ first, last = partition
94
+ @first = finish_first(first)
95
+ @last = finish_last(last)
96
+ end
97
+
98
+ def partition
99
+ if @name2.length == 0
100
+ # Only one imput so we must split first and last.
101
+ parts = @name1.split(/,/)
102
+ if parts.size > 1
103
+ last = clean(parts.shift || '')
104
+ first = clean(parts.join(' '))
105
+ else
106
+ parts = clean(@name1).split(/ /)
107
+ last = parts.pop || ''
108
+ first = parts.join(' ')
109
+ end
110
+ else
111
+ # Two inputs, so we are given first and last.
112
+ first = clean(@name1)
113
+ last = clean(@name2)
114
+ end
115
+ [first, last]
116
+ end
117
+
118
+ def clean(name)
119
+ name.gsub!(/`/, "'")
120
+ name.gsub!(/[^-a-zA-Z.'\s]/, '')
121
+ name.gsub!(/\./, ' ')
122
+ name.gsub!(/\s*-\s*/, '-')
123
+ name.gsub!(/'+/, "'")
124
+ name.strip.downcase.split(/\s+/).map do |n|
125
+ n.sub!(/^-+/, '')
126
+ n.sub!(/-+$/, '')
127
+ n.split(/-/).map do |p|
128
+ p.capitalize!
129
+ end.join('-')
130
+ end.join(' ')
131
+ end
132
+
133
+ def finish_first(names)
134
+ names.gsub(/([A-Z])\b/, '\1.')
135
+ end
136
+
137
+ def finish_last(names)
138
+ names.gsub!(/\b([A-Z])'([a-z])/) { |m| $1 << "'" << $2.upcase}
139
+ names.gsub!(/\bMc([a-z])/) { |m| 'Mc' << $1.upcase}
140
+ names.gsub!(/\bMac([a-z])/) do |m|
141
+ letter = $1
142
+ 'Mac'.concat(@name2.match("[mM][aA][cC]#{letter}") ? letter : letter.upcase)
143
+ end
144
+ names.gsub!(/\bO ([A-Z])/) { |m| "O'" << $1 }
145
+ names
146
+ end
147
+
148
+ # Match a complete first name.
149
+ def match_first(first1, first2)
150
+ # Is this one a walk in the park?
151
+ return true if first1 == first2
152
+
153
+ # No easy ride. Begin by splitting into individual first names.
154
+ first1 = split_first(first1)
155
+ first2 = split_first(first2)
156
+
157
+ # Get the long list and the short list.
158
+ long, short = first1.size >= first2.size ? [first1, first2] : [first2, first1]
159
+
160
+ # The short one must be a "subset" of the long one.
161
+ # An extra condition must also be satisfied.
162
+ extra = false
163
+ (0..long.size-1).each do |i|
164
+ lword = long.shift
165
+ score = match_first_name(lword, short.first)
166
+ if score >= 0
167
+ short.shift
168
+ extra = true if i == 0 || score == 0
169
+ end
170
+ break if short.empty? || long.empty?
171
+ end
172
+
173
+ # There's a match if the following is true.
174
+ short.empty? && extra
175
+ end
176
+
177
+ # Match a complete last name.
178
+ def match_last(last1, last2)
179
+ return true if last1 == last2
180
+ [last1, last2].each do |last|
181
+ last.downcase! # MacDonaugh and Macdonaugh
182
+ last.gsub!(/\bmac/, 'mc') # MacDonaugh and McDonaugh
183
+ last.tr!('-', ' ') # Lowry-O'Reilly and Lowry O'Reilly
184
+ end
185
+ last1 == last2
186
+ end
187
+
188
+ # Split a complete first name for matching.
189
+ def split_first(first)
190
+ first.tr!('-', ' ') # J. K. and J.-K.
191
+ first = first.split(/ /) # split on spaces
192
+ first = [''] if first.size == 0 # in case input was empty string
193
+ first
194
+ end
195
+
196
+ # Match individual first names or initials.
197
+ # -1 = no match
198
+ # 0 = full match
199
+ # 1 = match involving 1 initial
200
+ # 2 = match involving 2 initials
201
+ def match_first_name(first1, first2)
202
+ initials = 0
203
+ initials+= 1 if first1.match(/^[A-Z]\.?$/)
204
+ initials+= 1 if first2.match(/^[A-Z]\.?$/)
205
+ return initials if first1 == first2
206
+ return 0 if initials == 0 && match_nick_name(first1, first2)
207
+ return -1 unless initials > 0
208
+ return initials if first1[0] == first2[0]
209
+ -1
210
+ end
211
+
212
+ # Match two first names that might be equivalent nicknames.
213
+ def match_nick_name(nick1, nick2)
214
+ compile_nick_names unless @@nc
215
+ code1 = @@nc[nick1]
216
+ return false unless code1
217
+ code1 == @@nc[nick2]
218
+ end
219
+
220
+ # Compile the nick names code hash when matching nick names is first attempted.
221
+ def compile_nick_names
222
+ @@nc = Hash.new
223
+ code = 1
224
+ @@nl.each do |nicks|
225
+ nicks.each do |n|
226
+ throw "duplicate name #{n}" if @@nc[n]
227
+ @@nc[n] = code
228
+ end
229
+ code+= 1
230
+ end
231
+ end
232
+
233
+ # A array of data for matching nicknames and also a few common misspellings.
234
+ @@nc = nil
235
+ @@nl = <<EOF.split(/\n/).reject{|x| x.length == 0 }.map{|x| x.split(' ')}
236
+ Abdul Abul
237
+ Alexander Alex
238
+ Anandagopal Ananda
239
+ Anne Ann
240
+ Anthony Tony
241
+ Benjamin Ben
242
+ Catherine Cathy Cath
243
+ Daniel Danial Danny Dan
244
+ David Dave
245
+ Deborah Debbie
246
+ Des Desmond
247
+ Eamonn Eamon
248
+ Edward Eddie Ed
249
+ Eric Erick Erik
250
+ Frederick Frederic Fred
251
+ Gerald Gerry
252
+ Gerhard Gerard Ger
253
+ James Jim
254
+ Joanna Joan Joanne
255
+ John Johnny
256
+ Jonathan Jon
257
+ Kenneth Ken Kenny
258
+ Michael Mike Mick Micky
259
+ Nicholas Nick Nicolas
260
+ Nicola Nickie Nicky
261
+ Patrick Pat Paddy
262
+ Peter Pete
263
+ Philippe Philip Phillippe Phillip
264
+ Rick Ricky
265
+ Robert Bob Bobby
266
+ Samual Sam Samuel
267
+ Stefanie Stef
268
+ Stephen Steven Steve
269
+ Terence Terry
270
+ Thomas Tom Tommy
271
+ William Will Willy Willie Bill
272
+ EOF
273
+ end
274
+ end
@@ -0,0 +1,204 @@
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_ (either FIDE or national),
19
+ _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
+ the date of birth conforms to a _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 method call (e.g. one player won, 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
+ Players can be compared to see if they're roughly or exactly the same, which may be useful in detecting duplicates.
44
+ If the names match and the federations don't disagree then two players are equal according to the _==_ operator.
45
+ The player number is irrelevant.
46
+
47
+ john1 = ICU::Player.new('John', 'Smith', 12)
48
+ john2 = ICU::Player.new('John', 'Smith', 22, :fed = 'IRL')
49
+ john2 = ICU::Player.new('John', 'Smith', 32, :fed = 'ENG')
50
+
51
+ john1 == john2 # => true (federations don't disagree because one is unset)
52
+ john2 == john3 # => false (federations disagree)
53
+
54
+ If, in addition, _rating_, _dob_, _gender_ and _id_ do not disagree then two players are equal
55
+ according to the stricter criteria of _eql?_.
56
+
57
+ mark1 = ICU::Player.new('Mark', 'Orr', 31, :fed = 'IRL', :rating => 2100)
58
+ mark2 = ICU::Player.new('Mark', 'Orr', 33, :fed = 'IRL', :rating => 2100, :title => 'IM')
59
+ mark3 = ICU::Player.new('Mark', 'Orr', 37, :fed = 'IRL', :rating => 2200, :title => 'IM')
60
+
61
+ mark1.eql?(mark2) # => true (ratings agree and titles don't disagree)
62
+ mark2.eql?(mark3) # => false (the ratings are not the same)
63
+
64
+ The presence of two players in the same tournament that are equal according to _==_ but unequal
65
+ according to _eql?__ is likely to indicate a data entry error.
66
+
67
+ If two instances represent the same player and are equal according to _==_ then the _id_, _rating_,
68
+ _title_ and _fed_ attributes of the two can be merged. For example:
69
+
70
+ fox1 = ICU::Player.new('Tony', 'Fox', 12, :id => 456)
71
+ fox2 = ICU::Player.new('Tony', 'Fox', 21, :rating => 2100, :fed => 'IRL', :gender => 'M')
72
+ fox1.merge(fox2)
73
+
74
+ Any attributes present in the second player but not in the first are copied to the first.
75
+ All other attributes are unaffected.
76
+
77
+ fox1.rating # => 2100
78
+ fox1.fed # => 'IRL'
79
+ fox1.gender # => 'M'
80
+
81
+ =end
82
+
83
+ class Player
84
+
85
+ extend ICU::Accessor
86
+ attr_integer :num
87
+ attr_positive_or_nil :id, :rating, :rank
88
+ attr_date_or_nil :dob
89
+
90
+ attr_reader :results, :first_name, :last_name, :fed, :title, :gender
91
+
92
+ # Constructor. Must supply both names and a unique number for the tournament.
93
+ def initialize(first_name, last_name, num, opt={})
94
+ self.first_name = first_name
95
+ self.last_name = last_name
96
+ self.num = num
97
+ [:id, :fed, :title, :rating, :rank, :dob, :gender].each do |atr|
98
+ self.send("#{atr}=", opt[atr]) unless opt[atr].nil?
99
+ end
100
+ @results = []
101
+ end
102
+
103
+ # Canonicalise and set the first name(s).
104
+ def first_name=(first_name)
105
+ name = Name.new(first_name, 'Last')
106
+ raise "invalid first name" unless name.first.length > 0
107
+ @first_name = name.first
108
+ end
109
+
110
+ # Canonicalise and set the last name(s).
111
+ def last_name=(last_name)
112
+ name = Name.new('First', last_name)
113
+ raise "invalid last name" unless name.last.length > 0 && name.first.length > 0
114
+ @last_name = name.last
115
+ end
116
+
117
+ # Return the full name, last name first.
118
+ def name
119
+ "#{last_name}, #{first_name}"
120
+ end
121
+
122
+ # Federation. Is either unknown (nil) or a string containing at least three letters.
123
+ def fed=(fed)
124
+ obj = Federation.find(fed)
125
+ @fed = obj ? obj.code : nil
126
+ raise "invalid federation (#{fed})" if @fed.nil? && fed.to_s.strip.length > 0
127
+ end
128
+
129
+ # Chess title. Is either unknown (nil) or one of: _GM_, _IM_, _FM_, _CM_, _NM_,
130
+ # or any of these preceeded by the letter _W_.
131
+ def title=(title)
132
+ @title = title.to_s.strip.upcase
133
+ @title << 'M' if @title.match(/[A-LN-Z]$/)
134
+ @title = 'IM' if @title == 'M'
135
+ @title = 'WIM' if @title == 'WM'
136
+ @title = nil if @title == ''
137
+ raise "invalid chess title (#{title})" unless @title.nil? || @title.match(/^W?[GIFCN]M$/)
138
+ end
139
+
140
+ # Gender. Is either unknown (nil) or one of _M_ or _F_.
141
+ def gender=(gender)
142
+ @gender = gender.to_s.strip[0,1].upcase
143
+ @gender = nil if @gender == ''
144
+ @gender = 'F' if @gender == 'W'
145
+ raise "invalid gender (#{gender})" unless @gender.nil? || @gender.match(/^[MF]$/)
146
+ end
147
+
148
+ # Add a result. Don't use this method directly - use ICU::Tournament#add_result instead.
149
+ def add_result(result)
150
+ raise "invalid result" unless result.class == ICU::Result
151
+ raise "player number (#{@num}) is not matched to result player number (#{result.player})" unless @num == result.player
152
+ already = @results.find_all { |r| r.round == result.round }
153
+ return if already.size == 1 && already[0].eql?(result)
154
+ raise "round number (#{result.round}) of new result is not unique and new result is not the same as existing one" unless already.size == 0
155
+ @results << result
156
+ end
157
+
158
+ # Lookup a result by round number.
159
+ def find_result(round)
160
+ @results.find { |r| r.round == round }
161
+ end
162
+
163
+ # Return the player's total points.
164
+ def points
165
+ @results.inject(0.0) { |t, r| t += r.points }
166
+ end
167
+
168
+ # Renumber the player according to the supplied hash. Return self.
169
+ def renumber(map)
170
+ raise "player number #{@num} not found in renumbering hash" unless map[@num]
171
+ self.num = map[@num]
172
+ @results.each{ |r| r.renumber(map) }
173
+ self
174
+ end
175
+
176
+ # Loose equality test. Passes if the names match and the federations are not different.
177
+ def ==(other)
178
+ return true if equal?(other)
179
+ return false unless other.is_a? Player
180
+ return false unless @first_name == other.first_name
181
+ return false unless @last_name == other.last_name
182
+ return false if @fed && other.fed && @fed != other.fed
183
+ true
184
+ end
185
+
186
+ # Strict equality test. Passes if the playes are loosly equal and also if their ID, rating, gender and title are not different.
187
+ def eql?(other)
188
+ return true if equal?(other)
189
+ return false unless self == other
190
+ [:id, :rating, :title, :gender].each do |m|
191
+ return false if self.send(m) && other.send(m) && self.send(m) != other.send(m)
192
+ end
193
+ true
194
+ end
195
+
196
+ # Merge in some of the details of another player.
197
+ def merge(other)
198
+ raise "cannot merge two players that are not equal" unless self == other
199
+ [:id, :rating, :title, :fed, :gender].each do |m|
200
+ self.send("#{m}=", other.send(m)) unless self.send(m)
201
+ end
202
+ end
203
+ end
204
+ end