sanichi-chess_icu 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/CHANGELOG +2 -0
- data/LICENCE +22 -0
- data/README.rdoc +11 -0
- data/Rakefile +41 -0
- data/VERSION.yml +4 -0
- data/lib/chess_icu.rb +3 -0
- data/lib/name.rb +217 -0
- data/lib/player.rb +143 -0
- data/lib/result.rb +103 -0
- data/lib/tournament.rb +81 -0
- data/lib/tournament_fcsv.rb +155 -0
- data/lib/util.rb +15 -0
- data/spec/name_spec.rb +172 -0
- data/spec/player_spec.rb +276 -0
- data/spec/result_spec.rb +165 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/tournament_fcsv_spec.rb +306 -0
- data/spec/tournament_spec.rb +166 -0
- data/spec/util_spec.rb +33 -0
- metadata +78 -0
data/.gitignore
ADDED
data/CHANGELOG
ADDED
data/LICENCE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2009 Mark Orr
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
require 'spec/rake/spectask'
|
5
|
+
|
6
|
+
begin
|
7
|
+
require 'jeweler'
|
8
|
+
Jeweler::Tasks.new do |gem|
|
9
|
+
gem.name = "chess_icu"
|
10
|
+
gem.summary = "For parsing files of chess tournament data into ruby classes."
|
11
|
+
gem.homepage = "http://github.com/sanichi/chess_icu"
|
12
|
+
gem.authors = ["Mark Orr"]
|
13
|
+
gem.email = "mark.j.l.orr@googlemail.com"
|
14
|
+
gem.files = FileList['[A-Z]*', '{lib,spec}/**/*', '.gitignore']
|
15
|
+
gem.has_rdoc = true
|
16
|
+
end
|
17
|
+
rescue LoadError
|
18
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
19
|
+
end
|
20
|
+
|
21
|
+
task :default => :spec
|
22
|
+
|
23
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
24
|
+
spec.libs << 'lib' << 'spec'
|
25
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
26
|
+
spec.spec_opts = ['--colour --format nested --loadby mtime --reverse']
|
27
|
+
end
|
28
|
+
|
29
|
+
Rake::RDocTask.new do |doc|
|
30
|
+
if File.exist?('VERSION.yml')
|
31
|
+
config = YAML.load(File.read('VERSION.yml'))
|
32
|
+
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
33
|
+
else
|
34
|
+
version = ""
|
35
|
+
end
|
36
|
+
|
37
|
+
rdoc.rdoc_dir = 'doc'
|
38
|
+
rdoc.title = "ChessIcu #{version}"
|
39
|
+
rdoc.rdoc_files.include('README*')
|
40
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
41
|
+
end
|
data/VERSION.yml
ADDED
data/lib/chess_icu.rb
ADDED
data/lib/name.rb
ADDED
@@ -0,0 +1,217 @@
|
|
1
|
+
module ICU
|
2
|
+
class Name
|
3
|
+
attr_reader :first, :last
|
4
|
+
|
5
|
+
def initialize(name1='', name2='')
|
6
|
+
@name1 = name1.to_s
|
7
|
+
@name2 = name2.to_s
|
8
|
+
canonicalize
|
9
|
+
end
|
10
|
+
|
11
|
+
def name
|
12
|
+
name = @first
|
13
|
+
name << ' ' if @first.length > 0 && @last.length > 0
|
14
|
+
name << @last
|
15
|
+
name
|
16
|
+
end
|
17
|
+
|
18
|
+
def rname
|
19
|
+
name = @last
|
20
|
+
name << ', ' if @first.length > 0 && @last.length > 0
|
21
|
+
name << @first
|
22
|
+
name
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_s
|
26
|
+
rname
|
27
|
+
end
|
28
|
+
|
29
|
+
def match(name1='', name2='')
|
30
|
+
other = Name.new(name1, name2)
|
31
|
+
match_first(first, other.first) && match_last(last, other.last)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def canonicalize
|
37
|
+
first, last = partition
|
38
|
+
@first = finish_first(first)
|
39
|
+
@last = finish_last(last)
|
40
|
+
end
|
41
|
+
|
42
|
+
def partition
|
43
|
+
if @name2.length == 0
|
44
|
+
# Only one imput so we must split first and last.
|
45
|
+
parts = @name1.split(/,/)
|
46
|
+
if parts.size > 1
|
47
|
+
last = clean(parts.shift || '')
|
48
|
+
first = clean(parts.join(' '))
|
49
|
+
else
|
50
|
+
parts = clean(@name1).split(/ /)
|
51
|
+
last = parts.pop || ''
|
52
|
+
first = parts.join(' ')
|
53
|
+
end
|
54
|
+
else
|
55
|
+
# Two inputs, so we are given first and last.
|
56
|
+
first = clean(@name1)
|
57
|
+
last = clean(@name2)
|
58
|
+
end
|
59
|
+
[first, last]
|
60
|
+
end
|
61
|
+
|
62
|
+
def clean(name)
|
63
|
+
name.gsub!(/`/, "'")
|
64
|
+
name.gsub!(/[^-a-zA-Z.'\s]/, '')
|
65
|
+
name.gsub!(/\./, ' ')
|
66
|
+
name.gsub!(/\s*-\s*/, '-')
|
67
|
+
name.gsub!(/'+/, "'")
|
68
|
+
name.strip.downcase.split(/\s+/).map do |n|
|
69
|
+
n.sub!(/^-+/, '')
|
70
|
+
n.sub!(/-+$/, '')
|
71
|
+
n.split(/-/).map do |p|
|
72
|
+
p.capitalize!
|
73
|
+
end.join('-')
|
74
|
+
end.join(' ')
|
75
|
+
end
|
76
|
+
|
77
|
+
def finish_first(names)
|
78
|
+
names.gsub(/([A-Z])\b/, '\1.')
|
79
|
+
end
|
80
|
+
|
81
|
+
def finish_last(names)
|
82
|
+
names.gsub!(/\b([A-Z])'([a-z])/) { |m| $1 << "'" << $2.upcase}
|
83
|
+
names.gsub!(/\bMc([a-z])/) { |m| 'Mc' << $1.upcase}
|
84
|
+
names.gsub!(/\bMac([a-z])/) do |m|
|
85
|
+
letter = $1
|
86
|
+
'Mac'.concat(@name2.match("[mM][aA][cC]#{letter}") ? letter : letter.upcase)
|
87
|
+
end
|
88
|
+
names.gsub!(/\bO ([A-Z])/) { |m| "O'" << $1 }
|
89
|
+
names
|
90
|
+
end
|
91
|
+
|
92
|
+
# Match a complete first name.
|
93
|
+
def match_first(first1, first2)
|
94
|
+
# Is this one a walk in the park?
|
95
|
+
return true if first1 == first2
|
96
|
+
|
97
|
+
# No easy ride. Begin by splitting into individual first names.
|
98
|
+
first1 = split_first(first1)
|
99
|
+
first2 = split_first(first2)
|
100
|
+
|
101
|
+
# Get the long list and the short list.
|
102
|
+
long, short = first1.size >= first2.size ? [first1, first2] : [first2, first1]
|
103
|
+
|
104
|
+
# The short one must be a "subset" of the long one.
|
105
|
+
# An extra condition must also be satisfied.
|
106
|
+
extra = false
|
107
|
+
(0..long.size-1).each do |i|
|
108
|
+
lword = long.shift
|
109
|
+
score = match_first_name(lword, short.first)
|
110
|
+
if score >= 0
|
111
|
+
short.shift
|
112
|
+
extra = true if i == 0 || score == 0
|
113
|
+
end
|
114
|
+
break if short.empty? || long.empty?
|
115
|
+
end
|
116
|
+
|
117
|
+
# There's a match if the following is true.
|
118
|
+
short.empty? && extra
|
119
|
+
end
|
120
|
+
|
121
|
+
# Match a complete last name.
|
122
|
+
def match_last(last1, last2)
|
123
|
+
return true if last1 == last2
|
124
|
+
[last1, last2].each do |last|
|
125
|
+
last.downcase! # MacDonaugh and Macdonaugh
|
126
|
+
last.gsub!(/\bmac/, 'mc') # MacDonaugh and McDonaugh
|
127
|
+
last.tr!('-', ' ') # Lowry-O'Reilly and Lowry O'Reilly
|
128
|
+
end
|
129
|
+
last1 == last2
|
130
|
+
end
|
131
|
+
|
132
|
+
# Split a complete first name for matching.
|
133
|
+
def split_first(first)
|
134
|
+
first.tr!('-', ' ') # J. K. and J.-K.
|
135
|
+
first = first.split(/ /) # split on spaces
|
136
|
+
first = [''] if first.size == 0 # in case input was empty string
|
137
|
+
first
|
138
|
+
end
|
139
|
+
|
140
|
+
# Match individual first names or initials.
|
141
|
+
# -1 = no match
|
142
|
+
# 0 = full match
|
143
|
+
# 1 = match involving 1 initial
|
144
|
+
# 2 = match involving 2 initials
|
145
|
+
def match_first_name(first1, first2)
|
146
|
+
initials = 0
|
147
|
+
initials+= 1 if first1.match(/^[A-Z]\.?$/)
|
148
|
+
initials+= 1 if first2.match(/^[A-Z]\.?$/)
|
149
|
+
return initials if first1 == first2
|
150
|
+
return 0 if initials == 0 && match_nick_name(first1, first2)
|
151
|
+
return -1 unless initials > 0
|
152
|
+
return initials if first1[0] == first2[0]
|
153
|
+
-1
|
154
|
+
end
|
155
|
+
|
156
|
+
# Match two first names that might be equivalent nicknames.
|
157
|
+
def match_nick_name(nick1, nick2)
|
158
|
+
compile_nick_names unless @@nc
|
159
|
+
code1 = @@nc[nick1]
|
160
|
+
return false unless code1
|
161
|
+
code1 == @@nc[nick2]
|
162
|
+
end
|
163
|
+
|
164
|
+
# Compile the nick names code hash when matching nick names is first attempted.
|
165
|
+
def compile_nick_names
|
166
|
+
@@nc = Hash.new
|
167
|
+
code = 1
|
168
|
+
@@nl.each do |nicks|
|
169
|
+
nicks.each do |n|
|
170
|
+
throw "duplicate name #{n}" if @@nc[n]
|
171
|
+
@@nc[n] = code
|
172
|
+
end
|
173
|
+
code+= 1
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# A array of data for matching nicknames and also a few common misspellings.
|
178
|
+
@@nc = nil
|
179
|
+
@@nl = <<EOF.split(/\n/).reject{|x| x.length == 0 }.map{|x| x.split(' ')}
|
180
|
+
Abdul Abul
|
181
|
+
Alexander Alex
|
182
|
+
Anandagopal Ananda
|
183
|
+
Anne Ann
|
184
|
+
Anthony Tony
|
185
|
+
Benjamin Ben
|
186
|
+
Catherine Cathy Cath
|
187
|
+
Daniel Danial Danny Dan
|
188
|
+
David Dave
|
189
|
+
Deborah Debbie
|
190
|
+
Des Desmond
|
191
|
+
Eamonn Eamon
|
192
|
+
Edward Eddie Ed
|
193
|
+
Eric Erick Erik
|
194
|
+
Frederick Frederic Fred
|
195
|
+
Gerald Gerry
|
196
|
+
Gerhard Gerard Ger
|
197
|
+
James Jim
|
198
|
+
Joanna Joan Joanne
|
199
|
+
John Johnny
|
200
|
+
Jonathan Jon
|
201
|
+
Kenneth Ken Kenny
|
202
|
+
Michael Mike Mick Micky
|
203
|
+
Nicholas Nick Nicolas
|
204
|
+
Nicola Nickie Nicky
|
205
|
+
Patrick Pat Paddy
|
206
|
+
Peter Pete
|
207
|
+
Philippe Philip Phillippe Phillip
|
208
|
+
Rick Ricky
|
209
|
+
Samual Sam Samuel
|
210
|
+
Stefanie Stef
|
211
|
+
Stephen Steven Steve
|
212
|
+
Terence Terry
|
213
|
+
Thomas Tom Tommy
|
214
|
+
William Will Willy Willie Bill
|
215
|
+
EOF
|
216
|
+
end
|
217
|
+
end
|
data/lib/player.rb
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
module ICU
|
2
|
+
class Player
|
3
|
+
attr_accessor :first_name, :last_name, :num, :id, :fed, :title, :rating, :rank, :dob
|
4
|
+
attr_reader :results
|
5
|
+
|
6
|
+
def initialize(first_name, last_name, num, opt={})
|
7
|
+
self.first_name = first_name
|
8
|
+
self.last_name = last_name
|
9
|
+
self.num = num
|
10
|
+
[:id, :fed, :title, :rating, :rank, :dob].each do |atr|
|
11
|
+
self.send("#{atr}=", opt[atr]) unless opt[atr].nil?
|
12
|
+
end
|
13
|
+
@results = []
|
14
|
+
end
|
15
|
+
|
16
|
+
def first_name=(first_name)
|
17
|
+
name = Name.new(first_name, 'Last')
|
18
|
+
raise "invalid first name" unless name.first.length > 0
|
19
|
+
@first_name = name.first
|
20
|
+
end
|
21
|
+
|
22
|
+
def last_name=(last_name)
|
23
|
+
name = Name.new('First', last_name)
|
24
|
+
raise "invalid last name" unless name.last.length > 0 && name.first.length > 0
|
25
|
+
@last_name = name.last
|
26
|
+
end
|
27
|
+
|
28
|
+
def name
|
29
|
+
"#{last_name}, #{first_name}"
|
30
|
+
end
|
31
|
+
|
32
|
+
# Player number. Any integer.
|
33
|
+
def num=(num)
|
34
|
+
@num = case num
|
35
|
+
when Fixnum then num
|
36
|
+
else num.to_i
|
37
|
+
end
|
38
|
+
raise "invalid player number (#{num})" if @num == 0 && !num.to_s.match(/\d/)
|
39
|
+
end
|
40
|
+
|
41
|
+
# National or FIDE ID. Is either unknown (nil) or a positive integer.
|
42
|
+
def id=(id)
|
43
|
+
@id = case id
|
44
|
+
when nil then nil
|
45
|
+
when Fixnum then id
|
46
|
+
when /^\s*$/ then nil
|
47
|
+
else id.to_i
|
48
|
+
end
|
49
|
+
raise "invalid ID (#{id})" unless @id.nil? || @id > 0
|
50
|
+
end
|
51
|
+
|
52
|
+
# Federation. Is either unknown (nil) or contains at least three letters.
|
53
|
+
def fed=(fed)
|
54
|
+
@fed = fed.to_s.strip
|
55
|
+
@fed.upcase! if @fed.length == 3
|
56
|
+
@fed = nil if @fed == ''
|
57
|
+
raise "invalid federation (#{fed})" unless @fed.nil? || @fed.match(/[a-z]{3}/i)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Chess title. Is either unknown (nil) or one of a set of possibilities (after a little cleaning up).
|
61
|
+
def title=(title)
|
62
|
+
@title = title.to_s.strip.upcase
|
63
|
+
@title << 'M' if @title.match(/[A-LN-Z]$/)
|
64
|
+
@title = nil if @title == ''
|
65
|
+
raise "invalid chess title (#{title})" unless @title.nil? || @title.match(/^W?[GIFCN]M$/)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Elo rating. Is either unknown (nil) or a positive integer.
|
69
|
+
def rating=(rating)
|
70
|
+
@rating = case rating
|
71
|
+
when nil then nil
|
72
|
+
when Fixnum then rating
|
73
|
+
when /^\s*$/ then nil
|
74
|
+
else rating.to_i
|
75
|
+
end
|
76
|
+
raise "invalid rating (#{rating})" unless @rating.nil? || @rating > 0
|
77
|
+
end
|
78
|
+
|
79
|
+
# Rank in the tournament. Is either unknown (nil) or a positive integer.
|
80
|
+
def rank=(rank)
|
81
|
+
@rank = case rank
|
82
|
+
when nil then nil
|
83
|
+
when Fixnum then rank
|
84
|
+
when /^\s*$/ then nil
|
85
|
+
else rank.to_i
|
86
|
+
end
|
87
|
+
raise "invalid rank (#{rank})" unless @rank.nil? || @rank > 0
|
88
|
+
end
|
89
|
+
|
90
|
+
# Date of birth. Is either unknown (nil) or a yyyy-mm-dd format date.
|
91
|
+
def dob=(dob)
|
92
|
+
dob = dob.to_s.strip
|
93
|
+
@dob = dob == '' ? nil : Util.parsedate(dob)
|
94
|
+
raise "invalid DOB (#{dob})" if @dob.nil? && dob.length > 0
|
95
|
+
end
|
96
|
+
|
97
|
+
# Add a result.
|
98
|
+
def add_result(result)
|
99
|
+
raise "invalid result" unless result.class == ICU::Result
|
100
|
+
raise "player number (#{@num}) is not matched to result player number (#{result.player})" unless @num == result.player
|
101
|
+
raise "round number (#{result.round}) of new result should be unique" unless @results.map { |r| r.round }.grep(result.round).size == 0
|
102
|
+
@results << result
|
103
|
+
end
|
104
|
+
|
105
|
+
# Lookup a result by round number.
|
106
|
+
def find_result(round)
|
107
|
+
@results.find { |r| r.round == round }
|
108
|
+
end
|
109
|
+
|
110
|
+
# Return the player's total points.
|
111
|
+
def points
|
112
|
+
@results.inject(0.0) { |t, r| t += r.points }
|
113
|
+
end
|
114
|
+
|
115
|
+
# Loose equality test.
|
116
|
+
def ==(other)
|
117
|
+
return true if equal?(other)
|
118
|
+
return false unless other.is_a? Player
|
119
|
+
return false unless @first_name == other.first_name
|
120
|
+
return false unless @last_name == other.last_name
|
121
|
+
return false if @fed && other.fed && @fed != other.fed
|
122
|
+
true
|
123
|
+
end
|
124
|
+
|
125
|
+
# Strict equality test.
|
126
|
+
def eql?(other)
|
127
|
+
return true if equal?(other)
|
128
|
+
return false unless self == other
|
129
|
+
[:id, :rating, :title].each do |m|
|
130
|
+
return false if self.send(m) && other.send(m) && self.send(m) != other.send(m)
|
131
|
+
end
|
132
|
+
true
|
133
|
+
end
|
134
|
+
|
135
|
+
# Merge in some of the details of another player.
|
136
|
+
def subsume(other)
|
137
|
+
raise "cannot merge two players that are not strictly equal" unless eql?(other)
|
138
|
+
[:id, :rating, :title, :fed].each do |m|
|
139
|
+
self.send("#{m}=", other.send(m)) if other.send(m)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|