Class: ICU::Tournament::ForeignCSV

Inherits:
Object
  • Object
show all
Defined in:
lib/icu_tournament/tournament_fcsv.rb

Overview

This is a format (specification) used by the ICU for players to submit their individual results in foreign tournaments for domestic rating.

Suppose, for example, that the following data is the file tournament.csv:

Event,"Isle of Man Masters, 2007"
Start,2007-09-22
End,2007-09-30
Rounds,9
Website,http://www.bcmchess.co.uk/monarch2007/

Player,456,Fox,Anthony
1,0,B,Taylor,Peter P.,2209,,ENG
2,=,W,Nadav,Egozi,2205,,ISR
3,=,B,Cafolla,Peter,2048,,IRL
4,1,W,Spanton,Tim R.,1982,,ENG
5,1,B,Grant,Alan,2223,,SCO
6,0,-
7,=,W,Walton,Alan J.,2223,,ENG
8,0,B,Bannink,Bernard,2271,FM,NED
9,=,W,Phillips,Roy,2271,,MRI
Total,4

This file can be parsed as follows.

parser = ICU::Tournament::ForeignCSV.new
tournament = parser.parse_file('tournament.csv')

If the file is correctly specified, the return value from the parse_file method is an instance of ICU::Tournament (rather than nil, which indicates an error). In this example the file is valid, so:

tournament.name                                     # => "Isle of Man Masters, 2007"
tournament.start                                    # => "2007-09-22"
tournament.finish                                   # => "2007-09-30"
tournament.rounds                                   # => 9
tournament.website                                  # => "http://www.bcmchess.co.uk/monarch2007/"

The main player (the ICU player whose results are being reported for rating) played 9 rounds but only 8 other players (he had a bye in round 6), so the total number of players is 9.

tournament.players.size                             # => 9

Each player has a unique number for the tournament, starting at 1 for the first ICU player.

player = tournament.player(1)
player.name                                         # => "Fox, Anthony"

In the example, this player has 4 points from 9 rounds but only 8 of his results are are rateable (because of the bye).

player.points                                       # => 4.0
player.results.size                                 # => 9
player.results.find_all{ |r| r.rateable }.size      # => 8

The other players all have numbers greater than 1.

opponents = tournamnet.players.reject { |o| o.num == 1 }

There are 8 opponents (of the main player) each with exactly one game.

opponents.size                                      # => 8
opponents.find_all{ |o| o.results.size == 1 }.size  # => 8

If the file contains errors, then the return value from parse_file is nil and an error message is returned by the error method of the parser object. The method parse_file! is similar except that it raises errors, and the methods parse and parse! are similar except their inputs are strings rather than file names.

A tournament can be serialized back to CSV format (the reverse of parsing) with the serialize method of the parser object.

csv = parser.serialize(tournament)

Or equivalently, the serialize instance method of the tournament, if the appropriate parser name is supplied.

csv = tournament.serialize('ForeignCSV')

Extra condtions, over and above the normal validation rules, apply before any tournament validates or can be serialized in this format.

  • the tournament must have a site attribute and a finish date

  • there must be at least one player with an id (ICU ID number)

  • all foreign players (those without an ICU ID) must have a fed attribute (federation)

  • all ICU players must have a result in every round (even if it is just bye or is unrateable)

  • all the opponents of each ICU player must have a federation (this could include other ICU players with federation IRL)

  • at least one of each ICU player’s opponents must have a rating

If any of these are not satisfied, then the following method calls will all raise an exception:

tournament.validate!(:type => 'ForeignCSV')
tournament.serialize('ForeignCSV')
ICU::Tournament::ForeignCSV.new.serialize(tournament)

You can also build the tournament object from scratch using your own data and then serialize it. For example, here are the commands to reproduce the example above. Note that in this format opponents’ ratings are FIDE.

t = ICU::Tournament.new("Isle of Man Masters, 2007", '2007-09-22', :finish => '2007-09-30', :rounds => 9)
t.site = 'http://www.bcmchess.co.uk/monarch2007/'
t.add_player(ICU::Player.new('Anthony',  'Fox',      1, :fide_rating => 2100, :fed => 'IRL', :id => 456))
t.add_player(ICU::Player.new('Peter P.', 'Taylor',   2, :fide_rating => 2209, :fed => 'ENG'))
t.add_player(ICU::Player.new('Egozi',    'Nadav',    3, :fide_rating => 2205, :fed => 'ISR'))
t.add_player(ICU::Player.new('Peter',    'Cafolla',  4, :fide_rating => 2048, :fed => 'IRL'))
t.add_player(ICU::Player.new('Tim R.',   'Spanton',  5, :fide_rating => 1982, :fed => 'ENG'))
t.add_player(ICU::Player.new('Alan',     'Grant',    6, :fide_rating => 2223, :fed => 'SCO'))
t.add_player(ICU::Player.new('Alan J.',  'Walton',   7, :fide_rating => 2223, :fed => 'ENG'))
t.add_player(ICU::Player.new('Bernard',  'Bannink',  8, :fide_rating => 2271, :fed => 'NED', :title => 'FM'))
t.add_player(ICU::Player.new('Roy',      'Phillips', 9, :fide_rating => 2271, :fed => 'MRI'))
t.add_result(ICU::Result.new(1, 1, 'L', :opponent => 2, :colour => 'B'))
t.add_result(ICU::Result.new(2, 1, 'D', :opponent => 3, :colour => 'W'))
t.add_result(ICU::Result.new(3, 1, 'D', :opponent => 4, :colour => 'B'))
t.add_result(ICU::Result.new(4, 1, 'W', :opponent => 5, :colour => 'W'))
t.add_result(ICU::Result.new(5, 1, 'W', :opponent => 6, :colour => 'B'))
t.add_result(ICU::Result.new(6, 1, 'L'))
t.add_result(ICU::Result.new(7, 1, 'D', :opponent => 7, :colour => 'W'))
t.add_result(ICU::Result.new(8, 1, 'L', :opponent => 8, :colour => 'B'))
t.add_result(ICU::Result.new(9, 1, 'D', :opponent => 9, :colour => 'W'))
puts t.serialize('ForeignCSV')

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#errorObject (readonly)

Returns the value of attribute error.



122
123
124
# File 'lib/icu_tournament/tournament_fcsv.rb', line 122

def error
  @error
end

Instance Method Details

#parse(csv) ⇒ Object

Parse CSV data returning a Tournament on success or a nil on failure. In the case of failure, an error message can be retrived via the error method.



176
177
178
179
180
181
182
183
# File 'lib/icu_tournament/tournament_fcsv.rb', line 176

def parse(csv)
  begin
    parse!(csv)
  rescue => ex
    @error = ex.message
    nil
  end
end

#parse!(csv, arg = {}) ⇒ Object

Parse CSV data returning a Tournament on success or raising an exception on error.



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/icu_tournament/tournament_fcsv.rb', line 125

def parse!(csv, arg={})
  @state, @line, @round, @sum, @error = 0, 0, nil, nil, nil
  @tournament = Tournament.new('Unspecified', '2000-01-01')
  csv = ICU::Util::String.to_utf8(csv) unless arg[:is_utf8]

  CSV.parse(csv, :row_sep => :auto) do |r|
    @line += 1                            # increment line number
    next if r.size == 0                   # skip empty lines
    r = r.map{|c| c.nil? ? '' : c.strip}  # trim all spaces, turn nils to blanks
    next if r[0] == ''                    # skip blanks in column 1
    @r = r                                # remember this record for later

    begin
      case @state
        when 0 then event
        when 1 then start
        when 2 then finish
        when 3 then rounds
        when 4 then website
        when 5 then player
        when 6 then result
        when 7 then total
        else raise "internal error - state #{@state} does not exist"
      end
    rescue => err
      raise err.class, "line #{@line}: #{err.message}", err.backtrace unless err.message.match(/^line [1-9]/)
      raise
    end
  end

  unless @state == 5
    exp = case @state
          when 0 then "the event name"
          when 1 then "the start date"
          when 2 then "the end date"
          when 3 then "the number of rounds"
          when 4 then "the website address"
          when 6 then "a result for round #{@round+1}"
          when 7 then "a total score"
          end
    raise "line #{@line}: premature termination - expected #{exp}"
  end
  raise "line #{@line}: no players found in file" if @tournament.players.size == 0

  @tournament.validate!

  @tournament
end

#parse_file(file) ⇒ Object

Same as parse except the input is a file name rather than file contents.



192
193
194
195
196
197
198
199
# File 'lib/icu_tournament/tournament_fcsv.rb', line 192

def parse_file(file)
  begin
    parse_file!(file)
  rescue => ex
    @error = ex.message
    nil
  end
end

#parse_file!(file) ⇒ Object

Same as parse! except the input is a file name rather than file contents.



186
187
188
189
# File 'lib/icu_tournament/tournament_fcsv.rb', line 186

def parse_file!(file)
  csv = ICU::Util::File.read_utf8(file)
  parse!(csv, :is_utf8 => true)
end

#serialize(t, arg = {}) ⇒ Object

Serialise a tournament back into CSV format.



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/icu_tournament/tournament_fcsv.rb', line 202

def serialize(t, arg={})
  t.validate!(:type => self)
  CSV.generate do |csv|
    csv << ["Event", t.name]
    csv << ["Start", t.start]
    csv << ["End", t.finish]
    csv << ["Rounds", t.rounds]
    csv << ["Website", t.site]
    t.players.each do |p|
      next unless p.id
      next unless p.results.size == t.rounds
      csv << []
      csv << ["Player", p.id, p.last_name, p.first_name]
      (1..t.rounds).each do |n|
        data = []
        data << n
        r = p.find_result(n)
        data << case r.score; when 'W' then '1'; when 'L' then '0'; else '='; end
        if r.opponent
          data << r.colour
          o = t.player(r.opponent)
          data << o.last_name
          data << o.first_name
          data << o.fide_rating
          data << o.title
          data << o.fed
        else
          data << '-'
        end
        csv << data
      end
      csv << ["Total", p.points]
    end
  end
end

#validate!(t) ⇒ Object

Additional tournament validation rules for this specific type.



239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
# File 'lib/icu_tournament/tournament_fcsv.rb', line 239

def validate!(t)
  raise "missing end date" unless t.finish
  raise "missing site" unless t.site.to_s.length > 0
  icu = t.players.find_all { |p| p.id }
  raise "there must be at least one ICU player (with an ID number)" if icu.size == 0
  foreign = t.players.find_all { |p| !p.id }
  raise "all foreign players must have a federation" if foreign.detect { |f| !f.fed }
  enough = false
  icu.each do |p|
    rated = 0
    results = 0
    (1..t.rounds).each do |r|
      result = p.find_result(r)
      if result
        results += 1
        raise "all opponents of ICU players must have a federation" if result.opponent && !t.player(result.opponent).fed
        rated += 1 if result.opponent && t.player(result.opponent).fide_rating
      end
    end
    raise "player #{p.num} (#{p.name}) has no rated opponents" if rated == 0
    enough = true if results == t.rounds
  end
  raise "at least one ICU player (with an ID number) must have a result in every round" unless enough
end