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 SubmissionHi!
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.
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. 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
endclass 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
enddef 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 }
enddef parse_number(code)
code =~ NUM_FORMAT or raise RuntimeError, "Invalid number #{code}."
eval code
enddef parse_array(code)
code =~ /^\[(.*)\]$/ or raise RuntimeError, "No closing bracket for array #{code}."
$1.split_stateful.collect { |m| parse(m) }
enddef 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
enddef 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: