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.
- data/.gitignore +4 -0
- data/LICENCE +22 -0
- data/README.rdoc +75 -0
- data/Rakefile +57 -0
- data/VERSION.yml +5 -0
- data/lib/icu_tournament.rb +8 -0
- data/lib/icu_tournament/federation.rb +303 -0
- data/lib/icu_tournament/name.rb +274 -0
- data/lib/icu_tournament/player.rb +204 -0
- data/lib/icu_tournament/result.rb +191 -0
- data/lib/icu_tournament/team.rb +90 -0
- data/lib/icu_tournament/tournament.rb +508 -0
- data/lib/icu_tournament/tournament_fcsv.rb +310 -0
- data/lib/icu_tournament/tournament_krause.rb +329 -0
- data/lib/icu_tournament/util.rb +156 -0
- data/spec/federation_spec.rb +176 -0
- data/spec/name_spec.rb +208 -0
- data/spec/player_spec.rb +313 -0
- data/spec/result_spec.rb +203 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/team_spec.rb +60 -0
- data/spec/tournament_fcsv_spec.rb +548 -0
- data/spec/tournament_krause_spec.rb +379 -0
- data/spec/tournament_spec.rb +733 -0
- data/spec/util_spec.rb +357 -0
- metadata +97 -0
@@ -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
|