Class: ICU::Tournament::Krause

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

Overview

This is the format used to submit tournament results to FIDE for rating.

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

012 Fantasy Tournament
032 IRL
042 2009.09.09
0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890
132                                                                                        09.09.09  09.09.10  09.09.11
001    1 w    Mouse,Minerva                     1900 USA     1234567 1928.05.15  1.0    2     2 b 0     3 w 1
001    2 m  m Duck,Daffy                        2200 IRL     7654321 1937.04.17  2.0    1     1 w 1               3 b 1
001    3 m  g Mouse,Mickey                      2600 USA     1726354 1928.05.15  0.0    3               1 b 0     2 w 0

This file can be parsed as follows.

parser = ICU::Tournament::Krause.new
tournament = parser.parse_file('tournament.tab')

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                   # => "Fantasy Tournament"
tournament.start                  # => "2009-09-09"
tournament.fed                    # => "IRL"
tournament.players.size           # => 9

Some values, not explicitly set in the file, are deduced:

tournament.rounds                 # => 3
tournament.finish                 # => "2009-09-11"

A player can be retrieved from the tournament via the players array or by sending a valid player number to the player method.

minnie = tournament.player(1)
minnie.name                       # => "Mouse, Minerva"
minnie.points                     # => 1.0
minnie.results.size               # => 2

daffy = tournament.player(2)
daffy.title                       # => "IM"
daffy.rating                      # => 2200
daffy.fide_rating                 # => nil
daffy.fed                         # => "IRL"
daffy.id                          # => nil
daffy.fide_id                     # => 7654321
daffy.dob                         # => "1937-04-17"

By default, ratings are interpreted as ICU. If, instead, they should be interpreted as FIDE ratings, add the fide option:

tournament = parser.parse_file('tournament.tab', :fide => true)
daffy = tournament.player(2)
daffy.rating                      # => nil
daffy.fide_rating                 # => 2200

ID numbers, on the other hand, are automatically classified as either FIDE or ICU on the basis of size. IDs larger than 100000 are assumed to be FIDE IDs, while smaller numbers are treated as ICU IDs.

If the ranking numbers are missing from the file or inconsistent (e.g. player A is ranked above player B but has less points) they are recalculated as a side effect of the parse.

daffy.rank                        # => 1
minnie.rank                       # => 2
mickey.rank                       # => 3

Comments in the input file (lines that do not start with a valid data identification number) are available from the parser instance via its comments method (returning a string). Note that these comments are reset evry time the instance is used to parse another file.

parser.comments                   # => "0123456789..."

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.

Serialization

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

krause = parser.serialize(tournament)

Or alternatively, by the serialize method of the tournament object if the name of the serializer is supplied.

krause = tournament.serialize('Krause')

By default, local (ICU) IDs and ratings are used for the serialization, but both methods accept an option that causes FIDE IDs and ratings to be used instead:

krause = parser.serialize(tournament, :fide => true)
krause = tournament.serialize('Krause', :fide => true)

By default all available information is output for each player, however, this is customizable. The player number, name, total points and results are always output but any of the remaining data (gender, title, rating or fide_rating, fed, id or fide_id, dob and rank) can be omitted, if desired, by specifying an array of columns to include or exclude. To omitt all the optional data, supply an empty array:

krause = tournament.serialize('Krause', :only => [])

To omitt just federation and rating but include all others:

krause = tournament.serialize('Krause', :except => [:fed, :rating])

To include only date of birth and title:

krause = tournament.serialize('Krause', :only => [:dob, :title])

To output FIDE IDs and ratings use the fide option in conjunctions with the id and rating options:

krause = tournament.serialize('Krause', :only => [:gender, :id, :rating], :fide => true)

Parser Strictness

In practice, Krause formatted files encontered in the wild can be produced in a variety of different ways and not always according to FIDE’s standard, which itself is rather loose. This Ruby gem deals with that situation by not raising parsing errors when data is encountered where it is clear what is meant, even if it doesn’t conform to the standards, such as they are. However, on output (serialisation) a strict interpretation of FIDE’s standard is adhered to.

For example in input data if a player’s gender is given as “F” it’s clear this means female, even though the specification calls for a lower case “w” (for woman) in this case. Similarly, for titles where, for example, both “GM” and FIDE’s “g” are recognised as meaning Grand Master.

When it comes to dates, the specification recommends the YYYY/MM/DD format for birth dates and YY/MM/DD for round dates but quotes an example where the start and finish dates are in the opposite order (DD.MM.YYYY) with a different separator. In practice, the author has encountered Krause files with US style date formatting (MM-DD-YYYY) and other bizarre formats (YY.DD.MM) which suffer from ambiguity when the day is 12 or less. It’s not the separator (“/”, “=”, “.”) that causes a problem but the year, month and day order. The solution adopted here is for all serialized dates to be in YYYY-MM-DD format (or YY-MM-DD for round dates which must fit in 8 characters), which is a recognised international standard (ISO 8601). However, for parsing, a much wider variation is permitted and there is some ability to detect and correct ambiguous dates. For example the following dates would all be interpreted as 2011-03-30:

  • 30th March 2011

  • 30.03.2011

  • 03/30/2011

Where no additional information is available to resolve an ambiguity, the month is assumed to come in the middle, so 04/03/2011 is interpreted as 2011.03.04 and not 2011.04.03.

Some Krause files that the author has encountered in the wild have 3-letter player federation codes that are not federations at all but something completely different (for example the first 3 letters of the player’s club). This is a clear violation of the specification and raises a parsing exception. However in practice it’s often necessary to deal with such files so the parser has two options to help in these cases. If the fed option is set to “ignore” then all player federation codes will be ignored, even if valid. While when set to “skip” then invalid codes will be ignored but valid ones retained.

tournament = parser.parse_file('tournament.tab', :fed => "ignore")
tournament = parser.parse_file('tournament.tab', :fed => "skip")

Similar options are available for parsing SwissPerfect files (see ICU::Tournament::SwissPerfect) which can suffer from the same problem.

Automatic Total Correction

Another problem encountered with Krause files in practice is a mismatch between the declared total points for a player and the sum of their points from each round. Normally this just raises a parsing exception. However, there is one set of circumstances when such mismatches can be repaired:

  • the declared total score is higher than the sum of scores,

  • the player has at least one bye which isn’t a full point bye or at least one round where no result is recorded,

  • the number of byes or missing results is enough to account for the difference in total score.

If all these conditions are met then just enough bye scores are incremented, or new byes created, to make the sum match the total, and the data will parse without raising an exception.

012 Mismatched Totals
042 2011.03.04
001    1      Mouse,Minerva                                                      1.0    2     2 b 0  0000 - =
001    2      Mouse,Mickey                                                       1.5    1     1 w 1

In this example both totals are underestimates. However, player 1 has a half-point bye which can be upgraded to a full-point and player 2 has no result in round 2 which leaves room for the creation of a new half-point bye. So this data parses without error and serializes to:

012 Mismatched Totals
042 2011-03-04
001    1      Mouse,Minerva                                                      1.0    2     2 b 0  0000 - +
001    2      Mouse,Mickey                                                       1.5    1     1 w 1  0000 - =

Tournament Attributes

The following lists Krause data identification numbers, their description and, where available, their corresponding attributes in an ICU::Tournament instance.

001 Player record

Use players to get all players or player with a player number to get a single instance.

012 Name

Get or set with name. Free text. A tounament name is mandatory.

013 Teams

Create an ICU::Team, add player numbers to it, use add_team to add to tournament, get_team/teams to retrive it/them.

022 City

Get or set with city. Free text.

032 Federation

Get or set with fed. Getter returns either nil or a three letter code. Setter can take various formats (see ICU::Federation).

042 Start date

Get or set with start. Getter returns yyyy-mm-dd format, but setter can use any reasonable date format. Start date is mandadory.

052 End date

Get or set with finish. Returns either yyyy-mm-dd format or nil if not set. Like start, can be set with various date formats.

062 Number of players

Not used. Treated as comment in parsed files. Can be determined from the size of the players array.

072 Number of rated players

Not used. Treated as comment in parsed files. Can be determined by analysing the array returned by players.

082 Number of teams

Not used. Treated as comment in parsed files.

092 Type of tournament

Get or set with type. Free text.

102 Arbiter(s)

Get or set with -arbiter_. Free text.

112 Deputy(ies)

Get or set with deputy. Free text.

122 Time control

Get or set with time_control. Free text.

132 Round dates

Get an array of dates using round_dates or one specific round date by calling round_date with a round number.

Constant Summary collapse

OPTIONS =
[
  [:gender,  "Gender"],
  [:title,    "Title"],
  [:rating,  "Rating"],
  [:fed, "Federation"],
  [:id,          "ID"],
  [:dob,        "DOB"],
  [:rank,      "Rank"],
]

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#commentsObject (readonly)

Returns the value of attribute comments.



199
200
201
# File 'lib/icu_tournament/tournament_krause.rb', line 199

def comments
  @comments
end

#errorObject (readonly)

Returns the value of attribute error.



199
200
201
# File 'lib/icu_tournament/tournament_krause.rb', line 199

def error
  @error
end

Instance Method Details

#parse(krs, arg = {}) ⇒ Object

Parse Krause 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.



284
285
286
287
288
289
290
291
# File 'lib/icu_tournament/tournament_krause.rb', line 284

def parse(krs, arg={})
  begin
    parse!(krs, arg)
  rescue => ex
    @error = ex.message
    nil
  end
end

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

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



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'lib/icu_tournament/tournament_krause.rb', line 213

def parse!(krs, arg={})
  @lineno = 0
  @tournament = Tournament.new('Unspecified', '2000-01-01')
  @name_set, @start_set = false, false
  @comments = ''
  @results = Array.new
  krs = ICU::Util::String.to_utf8(krs) unless arg[:is_utf8]
  lines = get_lines(krs)

  # Process all lines.
  lines.each do |line|
    @lineno += 1                 # increment line number
    next if line.match(/^\s*$/)  # skip blank lines
    @line = line                 # remember this line for later

    # Does it have a DIN or is it just a comment?
    if @line.match(/^(\d{3}) (.*)$/)
      din = $1             # data identification number (DIN)
      @data = $2           # the data after the DIN
    else
      add_comment
      next
    end

    # Process the line given the DIN.
    begin
      case din
        when '001' then add_player(arg)                   # player and results record
        when '012' then set_name                          # name (mandatory)
        when '013' then add_team                          # team name and members
        when '022' then @tournament.city = @data          # city
        when '032' then @tournament.fed = @data           # federation
        when '042' then set_start                         # start date (mandatory)
        when '052' then @tournament.finish = @data        # end date
        when '062' then add_comment                       # number of players (calculated from 001 records)
        when '072' then add_comment                       # number of rated players (calculated from 001 records)
        when '082' then add_comment                       # number of teams (calculated from 013 records)
        when '092' then @tournament.type = @data          # type of tournament
        when '102' then @tournament.arbiter = @data       # arbiter(s)
        when '112' then @tournament.deputy = @data        # deputy(ies)
        when '122' then @tournament.time_control = @data  # time control
        when '132' then add_round_dates(arg)              # round dates
        else raise "invalid DIN #{din}"
      end
    rescue => err
      raise err.class, "line #{@lineno}: #{err.message}", err.backtrace
    end
  end

  # Now that all players are present, add the results to the tournament.
  @results.each do |r|
    lineno, player, data, result = r
    begin
      @tournament.add_result(result)
    rescue => err
      raise "line #{lineno}, player #{player}, result '#{data}': #{err.message}"
    end
  end

  # Certain attributes are mandatory and should have been specifically set.
  raise "tournament name missing"       unless @name_set
  raise "tournament start date missing" unless @start_set

  # Finally, exercise the tournament object's internal validation, reranking if neccessary.
  @tournament.validate!(:rerank => true)

  @tournament
end

#parse_file(file, arg = {}) ⇒ Object

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



301
302
303
304
305
306
307
308
# File 'lib/icu_tournament/tournament_krause.rb', line 301

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

#parse_file!(file, arg = {}) ⇒ Object

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



294
295
296
297
298
# File 'lib/icu_tournament/tournament_krause.rb', line 294

def parse_file!(file, arg={})
  krause = ICU::Util::File.read_utf8(file)
  arg[:is_utf8] = true
  parse!(krause, arg)
end

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

Serialize a tournament back into Krause format.



311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# File 'lib/icu_tournament/tournament_krause.rb', line 311

def serialize(t, arg={})
  t.validate!(:type => self)
  krause = ''
  krause << "012 #{t.name}\n"
  krause << "022 #{t.city}\n"         if t.city
  krause << "032 #{t.fed}\n"          if t.fed
  krause << "042 #{t.start}\n"
  krause << "052 #{t.finish}\n"       if t.finish
  krause << "092 #{t.type}\n"         if t.type
  krause << "102 #{t.arbiter}\n"      if t.arbiter
  krause << "112 #{t.deputy}\n"       if t.deputy
  krause << "122 #{t.time_control}\n" if t.time_control
  t.teams.each do |team|
    krause << sprintf('013 %-31s', team.name)
    team.members.each{ |m| krause << sprintf(' %4d', m) }
    krause << "\n"
  end
  rounds = t.last_round
  if t.round_dates.size == rounds && rounds > 0
    krause << "132 #{' ' * 85}"
    t.round_dates.each{ |d| krause << d.sub(/^../, '  ') }
    krause << "\n"
  end
  t.players.each{ |p| krause << p.to_krause(rounds, arg) }
  krause
end

#validate!(t) ⇒ Object

Additional tournament validation rules for this specific type.



339
340
341
# File 'lib/icu_tournament/tournament_krause.rb', line 339

def validate!(t)
  # None.
end