module ICU
class Tournament
#
# This is the {format}[http://www.fide.com/component/content/article/5-whats-news/2245-736-general-data-exchange-format-for-tournament-results]
# used to submit tournament results to FIDE[http://www.fide.com] 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.fed # => "IRL"
# daffy.id # => 7654321
# daffy.fide # => nil
# daffy.dob # => "1937-04-17"
#
# By default, ID numbers and ratings in the input are interpreted as local IDs and ratings. If, instead, they should be interpreted as
# FIDE IDs and ratings, add the following option:
#
# tournament = parser.parse_file('tournament.tab', :fide_id => true)
# daffy = tournament.player(2)
# daffy.id # => nil
# daffy.fide # => 7654321
# daffy.rating # => nil
# daffy.fide_rating # => 2200
#
# If the ranking numbers are missing from the file or inconsistent (e.g. player A is ranked above player B
# but has less points than player B) 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.
#
# 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_id => true)
# krause = tournament.serialize('Krause', :fide_id => true)
#
# 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.
#
class Krause
attr_reader :error, :comments
# Parse Krause data returning a Tournament on success or raising an exception on error.
def parse!(krs, arg={})
@lineno = 0
@tournament = Tournament.new('Dummy', '2000-01-01')
@name_set, @start_set = false, false
@comments = ''
@results = Array.new
krs = ICU::Util.to_utf8(krs) unless arg[:is_utf8]
# Process all lines.
krs.each_line do |line|
@lineno += 1 # increment line number
line.strip! # remove leading and trailing white space
next if line == '' # 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 # 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 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.
def parse(krs, arg={})
begin
parse!(krs, arg)
rescue => ex
@error = ex.message
nil
end
end
# Same as parse! except the input is a file name rather than file contents.
def parse_file!(file, arg={})
krause = ICU::Util.read_utf8(file)
arg[:is_utf8] = true
parse!(krause, arg)
end
# Same as parse except the input is a file name rather than file contents.
def parse_file(file, arg={})
begin
parse_file!(file, arg)
rescue => ex
@error = ex.message
nil
end
end
# Serialise a tournament back into Krause format.
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
# Additional tournament validation rules for this specific type.
def validate!(t)
# None.
end
# :enddoc:
private
def set_name
@tournament.name = @data
@name_set = true
end
def set_start
@tournament.start = @data
@start_set = true
end
def add_player(arg={})
raise "player record less than minimum length" if @line.length < 99
# Player details.
num = @data[0, 4]
nam = Name.new(@data[10, 32])
opt =
{
:gender => @data[5, 1],
:title => @data[6, 3],
:fed => @data[49, 3],
:dob => @data[65, 10],
:rank => @data[81, 4],
}
opt[arg[:fide] ? :fide_id : :id] = @data[53, 11]
opt[arg[:fide] ? :fide_rating : :rating] = @data[44, 4]
player = Player.new(nam.first, nam.last, num, opt)
@tournament.add_player(player)
# Results.
points = @data[77, 4].strip
points = points == '' ? nil : points.to_f
index = 87
round = 1
total = 0.0
while @data.length >= index + 8
total+= add_result(round, player.num, @data[index, 8])
index+= 10
round+= 1
end
raise "declared points total (#{points}) does not agree with total from summed results (#{total})" if points && points != total
end
def add_result(round, player, data)
return 0.0 if data.strip! == '' # no result for this round
raise "invalid result '#{data}'" unless data.match(/^(0{1,4}|[1-9]\d{0,3}) (w|b|-) (1|0|=|\+|-)$/)
opponent = $1.to_i
colour = $2
score = $3
options = Hash.new
options[:opponent] = opponent unless opponent == 0
options[:colour] = colour unless colour == '-'
options[:rateable] = false unless score.match(/^(1|0|=)$/)
result = Result.new(round, player, score, options)
@results << [@lineno, player, data, result]
result.points
end
def add_team
raise error "team record less than minimum length" if @line.length < 40
team = Team.new(@data[0, 31])
index = 32
while @data.length >= index + 4
team.add_member(@data[index, 4])
index+= 5
end
@tournament.add_team(team)
end
def add_round_dates
raise "round dates record less than minimum length" if @line.length < 99
index = 87
while @data.length >= index + 8
date = @data[index, 8].strip
@tournament.add_round_date("20#{date}") unless date == ''
index+= 10
end
end
def add_comment
@comments << @line
@comments << "\n"
end
end
end
class Player
# Format a player's 001 record as it would appear in a Krause formatted file (including the final newline).
def to_krause(rounds, arg)
krause = '001'
krause << sprintf(' %4d', @num)
krause << sprintf(' %1s', case @gender; when 'M' then 'm'; when 'F' then 'w'; else ''; end)
krause << sprintf(' %2s', case @title; when nil then ''; when 'IM' then 'm'; when 'WIM' then 'wm'; else @title[0, @title.length-1].downcase; end)
krause << sprintf(' %-33s', "#{@last_name},#{@first_name}")
krause << sprintf(' %4s', arg[:fide] ? @fide_rating : @rating)
krause << sprintf(' %3s', @fed)
krause << sprintf(' %11s', arg[:fide] ? @fide_id : @id)
krause << sprintf(' %10s', @dob)
krause << sprintf(' %4.1f', points)
krause << sprintf(' %4s', @rank)
(1..rounds).each do |r|
result = find_result(r)
krause << sprintf(' %8s', result ? result.to_krause : '')
end
krause << "\n"
end
end
class Result
# Format a player's result as it would appear in a Krause formatted file (exactly 8 characters long, including leading whitespace).
def to_krause
return ' ' * 8 if !@opponent && !@colour && @score == 'L'
krause = sprintf('%4s ', @opponent || '0000')
krause << sprintf('%1s ', @colour ? @colour.downcase : '-')
krause << case @score; when 'W' then '1'; when 'L' then '0'; else '='; end if @rateable
krause << case @score; when 'W' then '+'; when 'L' then '-'; else '='; end if !@rateable
krause
end
end
end