#!/usr/bin/env ruby
# Yes, the lexer class is probably overkill, but I had it sitting around from
# another project and code reuse is a good thing =)
# I originally was going to tinker with this until the output ensured that
# each braket is immediately wrapped in a box by iteself, but I think this is
# proably good enough--this works for both of the example erroneous inputs in
# the quiz description--and it's already getting pretty long.
require 'strscan'
class Lexer
class << self
def add_rule(regex, token_type = nil, &block)
@rules ||= []
block ||= lambda { |match, token| [token, match] }
@rules << [ regex, token_type, block ]
end
def each_rule
@rules.each do |rule|
yield *rule
end
end
end
def initialize(input)
@scanner = StringScanner.new(input)
@tokens = Array.new()
@error_context_length = 20
end
attr_accessor :error_context_length
def tokens()
until @scanner.eos?
@tokens << find_match()
end
@tokens.compact
end
def find_match()
self.class.each_rule do |regex, token, block|
if (@scanner.scan(regex)) then
return block.call(@scanner.matched, token)
end
end
raise Exception,
"Parse error:\n" +
'Can not tokenize input near "' +
@scanner.peek(@error_context_length) + '"'
end
end
class BoxLexer < Lexer
add_rule(/[\[({]/, :begin_box)
add_rule(/[\])}]/, :end_box)
add_rule(/b/i, :bracket)
end
class Parser
@@matching_symbol = {
'(' => ')', ')' => '(',
'[' => ']', ']' => '[',
'{' => '}', '}' => '{'
}
def initialize input
@tokens = BoxLexer.new(input).tokens
@stack = []
@corrected_stream = []
end
def parse
while @tokens.size > 0
@current_token = @tokens.shift
dispatch_event
end
if (@current_token[0] != :end_box)
notify "!Wrapping all parts in wood '{'"
@corrected_stream.unshift [:begin_box, '{']
@corrected_stream << [:end_box, '}']
end
end
attr_reader :corrected_stream
private
def notify mesg
print ' ' * @stack.length
puts mesg
end
def dispatch_event
case @current_token[0]
when :begin_box
begin_event
when :bracket
bracket_event
when :end_box
end_event
end
end
def begin_event
value = @current_token[1]
notify "Found begin box '#{value}'"
@stack.push value
@corrected_stream << @current_token
end
def bracket_event
notify "Found a bracket"
if (@tokens.length < 1) then
premature_end
else
if (@tokens[0][0] != :end_box) then
notify "! Bracket must be followed by an ending box"
fix = guess_best_fix
notify "! Attempting to fix by adding end box: '#{fix}'"
@tokens.unshift [:end_box, fix]
end
@corrected_stream << @current_token
end
end
def end_event
value = @current_token[1]
symbol = @@matching_symbol[@stack.pop]
notify "Found end box '#{value}'"
if (value != symbol) then
if symbol then
notify "! Bad match: Expecting closed '#{symbol}' got '#{value}'"
notify "! Attempting to fix by adding '#{symbol}'"
@tokens.unshift @current_token
@corrected_stream << [:end_box, symbol]
else
premature_end
end
else
@corrected_stream << @current_token
end
end
def guess_best_fix
fix = @@matching_symbol[@stack[-1]]
if (!fix) then
notify "! Wow, we really screwed this one up."
notify "! Use soft packaging ')' because we are cheap ;-)"
fix = ')'
end
fix
end
def premature_end
value = @current_token[1]
new_sym = @@matching_symbol[value]
notify "! Premature end of box: found extra '#{value}'"
notify "! Attempting to fix by wrapping entire payload in '#{new_sym}'"
@corrected_stream.unshift [:begin_box, new_sym]
@corrected_stream << [:end_box, value]
end
end
if (ARGV.length < 1) then
$stderr.puts "Usage #{__FILE__} box_string"
exit
end
parser = Parser.new(ARGV[0])
parser.parse
puts parser.corrected_stream.collect{|t| t.last}.join('')