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 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