sanichi-chess_icu 0.1.0
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/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
|