class Egd::PgnParser

Attributes

headers[R]

This service takes in a PGN string and parses it Returns the game tags (headers of the PGN file) and the actual moves made in SAN

pgn_content[R]

This service takes in a PGN string and parses it Returns the game tags (headers of the PGN file) and the actual moves made in SAN

Public Class Methods

new(pgn_content) click to toggle source
# File lib/egd/pgn_parser.rb, line 10
def initialize(pgn_content)
  @pgn_content = pgn_content
  @headers = []
  @movelist = []
  @game_attributes = {}
end

Public Instance Methods

call() click to toggle source
# File lib/egd/pgn_parser.rb, line 17
def call
  current_index = 0
  state = :initial
  buffer = ''

  while (current_index < @pgn_content.size)
    current_char = @pgn_content[current_index]
    current_index += 1

    if state == :initial
      if current_char == '['
        state = :start_parse_header
        next
      elsif (current_char == ' ' || current_char == "\n" || current_char == "\r")
        next
      else
        break
      end
    end

    if state == :start_parse_header
      if current_char == ']'
        state = :initial
        hd = parse_header(buffer)
        @headers << hd
        @game_attributes[hd[:type]] = hd[:value]
        buffer = ''
        next
      else
        buffer << current_char
        next
      end
    end
  end

  @movelist = simple_parse_moves

  hash = {moves: @movelist}
  hash.merge!(game_tags: @game_attributes) if @game_attributes.any?
  hash
end

Private Instance Methods

parse_header(header) click to toggle source
# File lib/egd/pgn_parser.rb, line 61
def parse_header(header)
  event_type = ""
  event_value = ""
  state = :parse_type
  current_index = 0
  buffer = ''

  while (current_index < header.size)
    current_char = header[current_index]
    current_index += 1

    if state == :parse_type
      if current_char == ' '
        event_type = buffer.dup
        buffer = ''
        state = :start_parse_value
        next
      else
        buffer << current_char
        next
      end
    elsif state == :start_parse_value
      if current_char == '"'
        state = :parse_value
        next
      else
        next
      end
    elsif state == :parse_value
      if current_char=='"'
        event_value = buffer.dup
        buffer = ''
      else
        buffer << current_char
      end
    end
  end

  {type: event_type, value: event_value}
end
simple_parse_moves() click to toggle source
# File lib/egd/pgn_parser.rb, line 102
def simple_parse_moves
  move_line =
    pgn_content.split("\n").map do |line|
      line unless line.strip[0] == "["
    end.compact.join(" ").
    gsub(%r'((1\-0)|(0\-1)|(1/2\-1/2)|(\*))\s*\z', "") # cut away game termination

  # strip out comments and alternatives
  while move_line.gsub!(%r'\{[^{}]*\}', ""); end
  while move_line.gsub!(%r'\([^()]*\)', ""); end

  # strip out "$n"-like annotations
  move_line.gsub!(%r'\$\d+ ', " ")

  # strip out ?! -like annotations
  move_line.gsub!(%r'[?!]+ ', " ")

  # strip out +/- like annotations
  move_line.gsub!(%r'(./.)|(= )|(\+\−)|(\-\+)|(\∞)', "")

  # squish whitespace
  move_line = move_line.strip.gsub(%r'\s{2,}', " ")

  # check if move line consists of legit chars only
  if !move_line.match?(%r'\A(?:[[:alnum:]]|[=\-+.#\* ])+\z')
    raise(
      "The PGN move portion has weird characters even after cleaning it.\n"\
      "Is the PGN valid?\n"\
      "The moves after cleaning came out as:\n#{move_line}"
    )
  end

  moves = []

  while move_line.match?(%r'\d+\.')
    parsed_moves = move_line.match(%r'\A
      (?<move_number>\d+)\.(?<move_chunk>.*?)(?:(?<remainder>\d+\..*\z)|\z)
    'x)

    move_number = parsed_moves[:move_number]
    move_chunk = parsed_moves[:move_chunk] #=> " e4 c5 "
    move_line = parsed_moves[:remainder].to_s.strip

    # a good place to DEBUG

    next if !move_chunk

    move_chunk = move_chunk.to_s.gsub(%r'\.{2}\s?', ".. ") # formats Black move ".."
    move_line = move_line.to_s.strip

    number_var = "@_#{move_number}"

    w = move_chunk.strip.split(" ")[0]
    b = move_chunk.strip.split(" ")[1]

    options = {}
    options.merge!(:w=>w) unless w.match?(%r'\.{2}')
    options.merge!(:b=>b) if b

    instance_variable_set(
      "@_#{move_number}",
      instance_variable_get("@_#{move_number}") ?
      instance_variable_get("@_#{move_number}").merge(options) :
      {:num=>move_number.to_i}.merge(options)
    )

    moves << instance_variable_get("@_#{move_number}") if instance_variable_get("@_#{move_number}")[:b]
    @last = instance_variable_get("@_#{move_number}")
  end

  # offload last to moves since there may not have been a black move
  moves << @last if !@last[:b]

  moves
end