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 ADDED
@@ -0,0 +1,4 @@
1
+ coverage
2
+ doc
3
+ pkg
4
+ tmp
data/CHANGELOG ADDED
@@ -0,0 +1,2 @@
1
+ * 0.1.0 2009-04-11
2
+ * Initial version that parses foreign CSV files and has some basic name and date utilities.
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
@@ -0,0 +1,11 @@
1
+ == ChessIcu
2
+
3
+ For parsing files of chess tournament data into ruby classes.
4
+
5
+ == Install
6
+
7
+ sudo gem install sanichi-chess_icu --source http://gems.github.com
8
+
9
+ == Usage
10
+
11
+ TODO
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
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 1
4
+ :patch: 0
data/lib/chess_icu.rb ADDED
@@ -0,0 +1,3 @@
1
+ dir = File.dirname(__FILE__)
2
+ %w{util name player result tournament}.each { |file| require "#{dir}/#{file}" }
3
+ %w{fcsv}.each { |file| require "#{dir}/tournament_#{file}" }
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