Please Forward: Ruby Quiz Submission

From: Alexander Stedile <as_news@gmx.at>
Date: February 3, 2008 6:30:39 PM CST
To: submission@rubyquiz.com
Subject: Please Forward: Ruby Quiz Submission

Hi!

Here is my try for my second ruby quiz. I proved that it is possible to write a JSON parser in less than 100 lines. My (handmade) solution has 94. This would leave space for five more comment lines. :wink:
Looking forward to seeing your parsers and of course the nice quiz summary which I really appreciate. Thanks for that.

By the way, I added a missing test. I had to do an extra check for that situation to my program. The reason is that the type is decided from the first character. And if there is a closing quotation mark at the end, it could be mistakenly interpreted as valid string 'a" "b'.
   assert_raise(RuntimeError) { @parser.parse(%Q{"a" "b"}) }
I also do not check for control characters in strings, but anyway.

As you see, I'm beginning to like eval. :slight_smile: And my solution heavily relies on regexp matching. The most important/tricky part is the String#split_stateful method at the top of the code.

Have fun,
Alex
______
class String
# Splits into sub-strings separated by ',' characters. Does not split
# contents within {}, , or "". \" does not end a string, \\" does.
# Checks if closing characters match previous opening ones.
def split_stateful
   memb = # list of members identified
   delims = # stack of delimiters
   split('').each { |c|
     memb << "" if memb.empty?
     case delims.last
     when '"' # quote mode
       c == '\\' and delims.push c
       c == '"' and delims.pop
     when '\\' # escape mode
       delims.pop
     else
       case c
       when '{', '[', '"' then delims.push c
       when ',' then ( memb << ""; c="" ) if delims.empty? # next element
       when '}' then delims.pop == '{' or raise RuntimeError, "Non-matching }."
       when ']' then delims.pop == '[' or raise RuntimeError, "Non-matching ]."
       end
     end
     memb[-1] += c
   }
   delims.empty? or raise RuntimeError, "No closing delimiter for #{delims.join(', ')}."
   memb
end
end

class JSONParser

NUM_FORMAT = /^(-)?(0|[1-9][0-9]*)(\.[0-9]+)?(E[+-]?([0-9]+))?$/i

# parse_value
def parse(code)
   code.strip!
   case code[0,1]
   when '"' then parse_string(code)
   when /[-0-9]/ then parse_number(code)
   when '{' then parse_object(code)
   when '[' then parse_array(code)
   else parse_keyword(code)
   end
end

def parse_string(code)
   code =~ /^"(.*)"$/ or raise RuntimeError, "String has no closing quotation mark."
   $_ = $1
   $_ =~ /([^\\]|(\\\\)+)"/ and raise RuntimeError, "Non-escaped \" not allowed in string #{$_}."
   gsub(/\\(.)/) { |m|
     case $1
     when 'b', 'f', 'n', 'r', 't'
       eval('"\\%s"' % $1)
     when 'u'
       m # no change, handled later
     when '"', '/', '\\'
       $1 # strip \ character
     else
       raise RuntimeError, "No such escape sequence \\#{$1}."
     end
   }
   gsub(/\\u([A-F0-9]{4})/i) { "%c" % $1.hex }
end

def parse_number(code)
   code =~ NUM_FORMAT or raise RuntimeError, "Invalid number #{code}."
   eval code
end

def parse_array(code)
   code =~ /^\[(.*)\]$/ or raise RuntimeError, "No closing bracket for array #{code}."
   $1.split_stateful.collect { |m| parse(m) }
end

def parse_object(code)
   code =~ /^\{(.*)\}$/ or raise RuntimeError, "No closing bracket for object #{code}."
   object = {}
   $1.split_stateful.each do |m|
     key, value = m.split(":", 2)
     object[parse_string(key.strip)] = parse(value)
   end
   object
end

def parse_keyword(code)
   case code
   when 'true', 'false' then eval(code)
   when 'null' then nil
   else
     raise RuntimeError, "Syntax error: #{code}."
   end
end
end

···

Begin forwarded message: