sanichi-chess_icu 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/result.rb ADDED
@@ -0,0 +1,103 @@
1
+ module ICU
2
+ class Result
3
+ attr_reader :round, :player, :score, :colour, :opponent, :rateable
4
+
5
+ def initialize(round, player, score, opt={})
6
+ self.round = round
7
+ self.player = player
8
+ self.score = score
9
+ [:colour, :opponent].each { |a| self.send("#{a}=", opt[a]) unless opt[a].nil? }
10
+ self.rateable = opt[:rateable] # always attempt to set this, and do it last, to get the right default
11
+ end
12
+
13
+ # Round number. Must be a positive integer.
14
+ def round=(round)
15
+ @round = round.to_i
16
+ raise "invalid round number (#{round})" unless @round > 0
17
+ end
18
+
19
+ # Player number. Can be any integer.
20
+ def player=(player)
21
+ @player = case player
22
+ when Fixnum then player
23
+ else player.to_i
24
+ end
25
+ raise "invalid player number (#{player})" if @player == 0 && !player.to_s.match(/\d/)
26
+ end
27
+
28
+ # Score for the game, even if a default. One of 'W', 'L' or 'D' (after some cleaning up).
29
+ def score=(score)
30
+ @score = case score.to_s.strip
31
+ when /^(1\.0|1|\+|W|w)$/ then 'W'
32
+ when /^(0\.5|½|\=|D|d)$/ then 'D'
33
+ when /^(0\.0|0|\-|L|l)$/ then 'L'
34
+ else raise "invalid score (#{score})"
35
+ end
36
+ end
37
+
38
+ # The score as a number.
39
+ def points
40
+ case @score
41
+ when 'W' then 1.0
42
+ when 'L' then 0.0
43
+ else 0.5
44
+ end
45
+ end
46
+
47
+ # Colour. Either 'W' or 'B' after some cleaning up.
48
+ def colour=(colour)
49
+ @colour = case colour.to_s
50
+ when '' then nil
51
+ when /W/i then 'W'
52
+ when /B/i then 'B'
53
+ else raise "invalid colour (#{colour})"
54
+ end
55
+ end
56
+
57
+ # Opponent player number. Either absent (nil) or any integer except the player number.
58
+ def opponent=(opponent)
59
+ @opponent = case opponent
60
+ when nil then nil
61
+ when Fixnum then opponent
62
+ when /^\s*$/ then nil
63
+ else opponent.to_i
64
+ end
65
+ raise "invalid opponent number (#{opponent})" if @opponent == 0 && !opponent.to_s.match(/\d/)
66
+ raise "opponent number and player number (#{@opponent}) must be different" if @opponent == player
67
+ self.rateable = true if @opponent
68
+ end
69
+
70
+ # Rateable flag. If false, game is not rateable. Can only be true if there is an opponent.
71
+ def rateable=(rateable)
72
+ if opponent.nil?
73
+ @rateable = false
74
+ return
75
+ end
76
+ @rateable = case rateable
77
+ when nil then true # default (when absent) is true
78
+ when false then false # this is the only way to turn it off
79
+ else true
80
+ end
81
+ end
82
+
83
+ # Reverse a result so it is seen from the opponent's perspective.
84
+ def reverse(rateable=nil)
85
+ return unless @opponent
86
+ r = Result.new(@round, @opponent, @score == 'W' ? 'L' : (@score == 'L' ? 'W' : 'D'))
87
+ r.opponent = @player
88
+ r.colour = 'W' if @colour == 'B'
89
+ r.colour = 'B' if @colour == 'W'
90
+ r.rateable = rateable || @rateable
91
+ r
92
+ end
93
+
94
+ # Loose equality.
95
+ def ==(other)
96
+ return unless other.is_a? Result
97
+ [:round, :player, :opponent, :colour, :score].each do |m|
98
+ return false unless self.send(m) == other.send(m)
99
+ end
100
+ true
101
+ end
102
+ end
103
+ end
data/lib/tournament.rb ADDED
@@ -0,0 +1,81 @@
1
+ module ICU
2
+ class Tournament
3
+ attr_reader :name, :start, :rounds, :site
4
+
5
+ # Constructor.
6
+ def initialize(name, start, opt={})
7
+ self.name = name
8
+ self.start = start
9
+ [:rounds, :site].each { |a| self.send("#{a}=", opt[a]) unless opt[a].nil? }
10
+ @player = {}
11
+ end
12
+
13
+ # Tournament name.
14
+ def name=(name)
15
+ raise "invalid tournament name (#{name})" unless name.to_s.match(/[a-z]/i)
16
+ @name = name.to_s.strip
17
+ end
18
+
19
+ # Start data in yyyy-mm-dd format.
20
+ def start=(start)
21
+ start = start.to_s.strip
22
+ @start = Util.parsedate(start)
23
+ raise "invalid start date (#{start})" unless @start
24
+ end
25
+
26
+ # Number of rounds. Is either unknown (nil) or a positive integer.
27
+ def rounds=(rounds)
28
+ @rounds = case rounds
29
+ when nil then nil
30
+ when Fixnum then rounds
31
+ when /^\s*$/ then nil
32
+ else rounds.to_i
33
+ end
34
+ raise "invalid number of rounds (#{rounds})" unless @rounds.nil? || @rounds > 0
35
+ end
36
+
37
+ # Web site. Either unknown or a reasonably valid looking URL.
38
+ def site=(site)
39
+ @site = site.to_s.strip
40
+ @site = nil if @site == ''
41
+ @site = "http://#{@site}" if @site && !@site.match(/^https?:\/\//)
42
+ raise "invalid site (#{site})" unless @site.nil? || @site.match(/^https?:\/\/[-\w]+(\.[-\w]+)+(\/[^\s]*)?$/i)
43
+ end
44
+
45
+ # Add players.
46
+ def add_player(player)
47
+ raise "invalid player" unless player.class == ICU::Player
48
+ raise "player number (#{player.num}) should be unique" if @player[player.num]
49
+ @player[player.num] = player
50
+ end
51
+
52
+ # Get a player by their number.
53
+ def player(num)
54
+ @player[num]
55
+ end
56
+
57
+ # Return an array of all the players.
58
+ def players
59
+ @player.values
60
+ end
61
+
62
+ # Lookup a player in the tournament.
63
+ def find_player(player)
64
+ players.find { |p| p == player }
65
+ end
66
+
67
+ # Add results.
68
+ def add_result(result, reverse_rateable=true)
69
+ raise "invalid result" unless result.class == ICU::Result
70
+ raise "result round number (#{result.round}) inconsistent with number of tournament rounds" if @rounds && result.round > @rounds
71
+ raise "player number (#{result.player}) does not exist" unless @player[result.player]
72
+ @player[result.player].add_result(result)
73
+ if result.opponent
74
+ raise "opponent number (#{result.opponent}) does not exist" unless @player[result.opponent]
75
+ reverse = result.reverse
76
+ reverse.rateable = false unless reverse_rateable
77
+ @player[result.opponent].add_result(reverse)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,155 @@
1
+ require 'fastercsv'
2
+
3
+ module ICU
4
+ class Tournament
5
+ class ForeignCSV
6
+ attr_reader :tournament
7
+
8
+ # Constructor.
9
+ def initialize(csv)
10
+ @state, @line, @round, @sum = 0, 0, nil, nil
11
+ parse(csv)
12
+ end
13
+
14
+ private
15
+
16
+ def parse(csv)
17
+ @tournament = Tournament.new('Dummy', '2000-01-01')
18
+ FasterCSV.parse(csv, :row_sep => :auto) do |r|
19
+ @line += 1 # increment line number
20
+ next if r.size == 0 # skip empty lines
21
+ r = r.map{|c| c.nil? ? '' : c.strip} # trim all spaces, turn nils to blanks
22
+ next if r[0] == '' # skip blanks in column 1
23
+ @r = r # remember this record for later
24
+
25
+ begin
26
+ case @state
27
+ when 0 then event
28
+ when 1 then start
29
+ when 2 then rounds
30
+ when 3 then website
31
+ when 4 then player
32
+ when 5 then result
33
+ when 6 then total
34
+ else raise "internal error - state #{@state} does not exist"
35
+ end
36
+ rescue => err
37
+ raise err.class, "line #{@line}: #{err.message}", err.backtrace unless err.message.match(/^line [1-9]/)
38
+ raise
39
+ end
40
+ end
41
+
42
+ unless @state == 4
43
+ exp = case @state
44
+ when 0 then "the event name"
45
+ when 1 then "the start date"
46
+ when 2 then "the number of rounds"
47
+ when 3 then "the website address"
48
+ when 5 then "a result for round #{@round+1}"
49
+ when 6 then "a total score"
50
+ end
51
+ raise "line #{@line}: premature termination - expected #{exp}"
52
+ end
53
+ raise "line #{@line}: no players found in file" if @tournament.players.size == 0
54
+ end
55
+
56
+ def event
57
+ abort "the 'Event' keyword", 0 unless @r[0].match(/^(Event|Tournament)$/i)
58
+ abort "the event name", 1 unless @r.size > 1 && @r[1] != ''
59
+ @tournament.name = @r[1]
60
+ @state = 1
61
+ end
62
+
63
+ def start
64
+ abort "the 'Start' keyword", 0 unless @r[0].match(/^(Start(\s+Date)?|Date)$/i)
65
+ abort "the start date", 1 unless @r.size > 1 && @r[1] != ''
66
+ @tournament.start = @r[1]
67
+ @state = 2
68
+ end
69
+
70
+ def rounds
71
+ abort "the 'Rounds' keyword", 0 unless @r[0].match(/(Number of )?Rounds$/)
72
+ abort "the number of rounds", 1 unless @r.size > 1 && @r[1].match(/^[1-9]\d*/)
73
+ @tournament.rounds = @r[1]
74
+ @state = 3
75
+ end
76
+
77
+ def website
78
+ abort "the 'Website' keyword", 0 unless @r[0].match(/^(Web(\s?site)?|Site)$/i)
79
+ abort "the event website", 1 unless @r.size > 1 && @r[1] != ''
80
+ @tournament.site = @r[1]
81
+ @state = 4
82
+ end
83
+
84
+ def player
85
+ abort "the 'Player' keyword", 0 unless @r[0].match(/^Player$/i)
86
+ abort "a player's ICU number", 1 unless @r.size > 1 && @r[1].match(/^[1-9]/i)
87
+ abort "a player's last name", 2 unless @r.size > 2 && @r[2].match(/[a-z]/i)
88
+ abort "a player's first name", 3 unless @r.size > 3 && @r[3].match(/[a-z]/i)
89
+ @player = Player.new(@r[3], @r[2], @tournament.players.size + 1, :id => @r[1])
90
+ old_player = @tournament.find_player(@player)
91
+ if old_player
92
+ raise "two players with the same name (#{@player.name}) have conflicting details" unless old_player.eql?(@player)
93
+ raise "same player (#{@player.name}) has more than one set of results" if old_player.id
94
+ old_player.subsume(@player)
95
+ @player = old_player
96
+ else
97
+ @tournament.add_player(@player)
98
+ end
99
+ @round = 0
100
+ @state = 5
101
+ end
102
+
103
+ def result
104
+ @round+= 1
105
+ abort "round number #{round}", 0 unless @r[0].to_i == @round
106
+ abort "a colour (W/B) or dash (for a bye)", 2 unless @r.size > 2 && @r[2].match(/^(W|B|-)/i)
107
+ result = Result.new(@round, @player.num, @r[1])
108
+ if @r[2] == '-'
109
+ @tournament.add_result(result)
110
+ else
111
+ result.colour = @r[2]
112
+ opponent = Player.new(@r[4], @r[3], @tournament.players.size + 1, :rating => @r[5], :title => @r[6], :fed => @r[7])
113
+ raise "opponent must have a rating and federation" unless opponent.rating && opponent.fed
114
+ old_player = @tournament.find_player(opponent)
115
+ if old_player
116
+ raise "two players with the same name (#{opponent.name}) have conflicting details" unless old_player.eql?(opponent)
117
+ result.opponent = old_player.num
118
+ if old_player.id
119
+ old_player.subsume(opponent)
120
+ old_result = @player.find_result(@round)
121
+ raise "missing result for player (#{@player.name}) in round #{@round}" unless old_result
122
+ raise "mismatched results for player (#{old_player.name}) in round #{@round}" unless result == old_result
123
+ old_result.rateable = true
124
+ else
125
+ old_result = old_player.find_result(@round)
126
+ raise "a player (#{old_player.name}) has more than one game in the same round (#{@round})" if old_result
127
+ @tournament.add_result(result, false)
128
+ end
129
+ else
130
+ @tournament.add_player(opponent)
131
+ result.opponent = opponent.num
132
+ @tournament.add_result(result, false)
133
+ end
134
+ end
135
+ @state = 6 if @round == @tournament.rounds
136
+ end
137
+
138
+ def total
139
+ points = @player.points
140
+ abort "the 'Total' keyword", 0 unless @r[0].match(/^Total$/i)
141
+ abort "the player's (#{@player.object_id}, #{@player.results.size}) total points to be #{points}", 1 unless @r[1].to_f == points
142
+ @state = 4
143
+ end
144
+
145
+ def abort(expected, cell)
146
+ got = @r[cell]
147
+ error = "line #{@line}"
148
+ error << ", cell #{cell+1}"
149
+ error << ": expected #{expected}"
150
+ error << " but got #{got == '' ? 'a blank cell' : "'#{got}'"}"
151
+ raise error
152
+ end
153
+ end
154
+ end
155
+ end
data/lib/util.rb ADDED
@@ -0,0 +1,15 @@
1
+ module ICU
2
+ class Util
3
+ # Parse dates into yyyy-mm-dd format, preferring European format. Return nil on error.
4
+ def self.parsedate(date)
5
+ date = date.to_s
6
+ return nil unless date.match(/[1-9]/)
7
+ date.sub!(/^([1-9]|0[1-9]|[12][0-9]|3[01])([^\d])([1-9]|0[1-9]|1[0-2])([^\d])/, '\3\2\1\4')
8
+ begin
9
+ Date.parse(date, true).to_s
10
+ rescue
11
+ return nil
12
+ end
13
+ end
14
+ end
15
+ end
data/spec/name_spec.rb ADDED
@@ -0,0 +1,172 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ module ICU
4
+ describe Name do
5
+ context "public methods" do
6
+ before(:each) do
7
+ @simple = Name.new('mark j l', 'orr')
8
+ end
9
+
10
+ it "#first returns the first name(s)" do
11
+ @simple.first.should == 'Mark J. L.'
12
+ end
13
+
14
+ it "#last returns the last name(s)" do
15
+ @simple.last.should == 'Orr'
16
+ end
17
+
18
+ it "#name returns the full name with first name(s) first" do
19
+ @simple.name.should == 'Mark J. L. Orr'
20
+ end
21
+
22
+ it "#rname returns the full name with last name(s) first" do
23
+ @simple.rname.should == 'Orr, Mark J. L.'
24
+ end
25
+
26
+ it "#to_s is the same as rname" do
27
+ @simple.to_s.should == 'Orr, Mark J. L.'
28
+ end
29
+
30
+ it "#match returns true if and only if two names match" do
31
+ @simple.match('mark j l orr').should be_true
32
+ @simple.match('malcolm g l orr').should be_false
33
+ end
34
+ end
35
+
36
+ context "names that are already canonical" do
37
+ it "should not be altered" do
38
+ Name.new('Mark J. L.', 'Orr').name.should == 'Mark J. L. Orr'
39
+ Name.new('Anna-Marie J.-K.', 'Liviu-Dieter').name.should == 'Anna-Marie J.-K. Liviu-Dieter'
40
+ end
41
+ end
42
+
43
+ context "last names beginning with a single letter followed by a quote" do
44
+ it "should be handled correctly" do
45
+ Name.new('una', "O'boyle").name.should == "Una O'Boyle"
46
+ Name.new('jonathan', 'd`arcy').name.should == "Jonathan D'Arcy"
47
+ Name.new('erwin e', "L'AMI").name.should == "Erwin E. L'Ami"
48
+ Name.new('cormac', "o brien").name.should == "Cormac O'Brien"
49
+ end
50
+ end
51
+
52
+ context "last beginning with Mc" do
53
+ it "should be handled correctly" do
54
+ Name.new('shane', "mccabe").name.should == "Shane McCabe"
55
+ Name.new('shawn', "macDonagh").name.should == "Shawn MacDonagh"
56
+ Name.new('shawn', "macdonagh").name.should == "Shawn Macdonagh"
57
+ Name.new('bartlomiej', "macieja").name.should == "Bartlomiej Macieja"
58
+ end
59
+ end
60
+
61
+ context "doubled barrelled names or initials" do
62
+ it "should be handled correctly" do
63
+ Name.new('anna-marie', 'den-otter').name.should == 'Anna-Marie Den-Otter'
64
+ Name.new('j-k', 'rowling').name.should == 'J.-K. Rowling'
65
+ Name.new("mark j. - l", 'ORR').name.should == 'Mark J.-L. Orr'
66
+ Name.new('JOHANNA', "lowry-o'REILLY").name.should == "Johanna Lowry-O'Reilly"
67
+ Name.new('hannah', "lowry - o reilly").name.should == "Hannah Lowry-O'Reilly"
68
+ end
69
+ end
70
+
71
+ context "extraneous white space" do
72
+ it "should be handled correctly" do
73
+ Name.new(' mark j l ', " \t\r\n orr \n").name.should == 'Mark J. L. Orr'
74
+ end
75
+ end
76
+
77
+ context "extraneous full stops" do
78
+ it "should be handled correctly" do
79
+ Name.new('. mark j..l', 'orr.').name.should == 'Mark J. L. Orr'
80
+ end
81
+ end
82
+
83
+ context "construction from a single string" do
84
+ it "should be possible in simple cases" do
85
+ Name.new('ORR, mark j l').name.should == 'Mark J. L. Orr'
86
+ Name.new('MARK J L ORR').name.should == 'Mark J. L. Orr'
87
+ Name.new("O'Reilly, j-k").name.should == "J.-K. O'Reilly"
88
+ end
89
+ end
90
+
91
+ context "construction from an instance" do
92
+ it "should be possible" do
93
+ Name.new(Name.new('ORR, mark j l')).name.should == 'Mark J. L. Orr'
94
+ end
95
+ end
96
+
97
+ context "constuction corner cases" do
98
+ it "should be handled correctly" do
99
+ Name.new('Orr').name.should == 'Orr'
100
+ Name.new('Orr').rname.should == 'Orr'
101
+ Name.new('').name.should == ''
102
+ Name.new('').rname.should == ''
103
+ Name.new.name.should == ''
104
+ Name.new.rname.should == ''
105
+ end
106
+ end
107
+
108
+ context "inputs to matching" do
109
+ before(:all) do
110
+ @mark = Name.new('Mark', 'Orr')
111
+ @kram = Name.new('Mark', 'Orr')
112
+ end
113
+
114
+ it "should be flexible" do
115
+ @mark.match('Mark', 'Orr').should be_true
116
+ @mark.match('Mark Orr').should be_true
117
+ @mark.match('Orr, Mark').should be_true
118
+ @mark.match(@kram).should be_true
119
+ end
120
+ end
121
+
122
+ context "first name matches" do
123
+ it "should match when first names are the same" do
124
+ Name.new('Mark', 'Orr').match('Mark', 'Orr').should be_true
125
+ end
126
+
127
+ it "should be flexible with regards to hyphens in double barrelled names" do
128
+ Name.new('J.-K.', 'Rowling').match('J. K.', 'Rowling').should be_true
129
+ Name.new('Joanne-K.', 'Rowling').match('Joanne K.', 'Rowling').should be_true
130
+ end
131
+
132
+ it "should match initials" do
133
+ Name.new('M. J. L.', 'Orr').match('Mark John Legard', 'Orr').should be_true
134
+ Name.new('M.', 'Orr').match('Mark', 'Orr').should be_true
135
+ Name.new('M. J. L.', 'Orr').match('Mark', 'Orr').should be_true
136
+ Name.new('M.', 'Orr').match('M. J.', 'Orr').should be_true
137
+ Name.new('M. J. L.', 'Orr').match('M. G.', 'Orr').should be_false
138
+ end
139
+
140
+ it "should not match on full names not in first position or without an exact match" do
141
+ Name.new('J. M.', 'Orr').match('John', 'Orr').should be_true
142
+ Name.new('M. J.', 'Orr').match('John', 'Orr').should be_false
143
+ Name.new('M. John', 'Orr').match('John', 'Orr').should be_true
144
+ end
145
+
146
+ it "should handle common nicknames" do
147
+ Name.new('William', 'Orr').match('Bill', 'Orr').should be_true
148
+ Name.new('David', 'Orr').match('Dave', 'Orr').should be_true
149
+ Name.new('Mick', 'Orr').match('Mike', 'Orr').should be_true
150
+ end
151
+
152
+ it "should not mix up nick names" do
153
+ Name.new('David', 'Orr').match('Bill', 'Orr').should be_false
154
+ end
155
+ end
156
+
157
+ context "last name matches" do
158
+ it "should be flexible with regards to hyphens in double barrelled names" do
159
+ Name.new('Johanna', "Lowry-O'Reilly").match('Johanna', "Lowry O'Reilly").should be_true
160
+ end
161
+
162
+ it "should be case insensitive in matches involving Macsomething and MacSomething" do
163
+ Name.new('Alan', 'MacDonagh').match('Alan', 'Macdonagh').should be_true
164
+ end
165
+
166
+ it "should cater for the common mispelling of names beginning with Mc or Mac" do
167
+ Name.new('Alan', 'McDonagh').match('Alan', 'MacDonagh').should be_true
168
+ Name.new('Darko', 'Polimac').match('Darko', 'Polimc').should be_false
169
+ end
170
+ end
171
+ end
172
+ end