Skip to content

Instantly share code, notes, and snippets.

@petertseng
Created March 17, 2017 04:56
Show Gist options
  • Select an option

  • Save petertseng/909e56876812100ccdf322dab700230b to your computer and use it in GitHub Desktop.

Select an option

Save petertseng/909e56876812100ccdf322dab700230b to your computer and use it in GitHub Desktop.
mini JSON
class ParseError < StandardError; end
def object(s, i)
o = {}
state = :key
key = nil
j = i + 1
while (c = s[j])
case state
when :key
case c
when ?}
return [o, j + 1] if o.empty?
# This came after a comma, so it's not allowed.
raise ParseError, "expected string key (after key #{key} for object started at #{i}), instead got #{c} at #{j}"
when ?"
closing_quote = s.index(?", j + 1)
raise ParseError, "missing closing quote (for key started at #{j + 1} for object started at #{i})" unless closing_quote
key = s[(j + 1)...closing_quote]
state = :colon
j = closing_quote + 1
else raise ParseError, "expected string key (for object started at #{i}), instead got #{c} at #{j}"
end
when :colon
if c == ?:
state = :value
j += 1
else raise ParseError, "expected colon (for key #{key} for object started at #{i}), instead got #{c} at #{j}"
end
when :value
case c
when ?{
subobj, j = object(s, j)
o[key] = subobj
state = :comma
when ?"
closing_quote = s.index(?", j + 1)
raise ParseError, "missing closing quote (for value started at #{j + 1} for key #{key} for object started at #{i})" unless closing_quote
o[key] = s[(j + 1)...closing_quote]
state = :comma
j = closing_quote + 1
else raise ParseError, "expected value (for key #{key} for object started at #{i}), instead got #{c} at #{j}"
end
when :comma
case c
when ?,
j += 1
state = :key
when ?}; return [o, j + 1]
else raise ParseError, "expected comma (after key #{key} for object started at #{i}), instead got #{c} at #{j}"
end
else
raise ParseError, "Unknown state #{state}"
end
end
# TODO explain what I wanted, based on my state
raise ParseError, "incomplete object started at #{i}"
end
def json(s)
i = s.each_char.find_index { |c| !c.strip.empty? }
raise ParseError, "whitespace only '#{s}' invalid" unless i
if s[i] == ?{
o, i = object(s, 0)
raise ParseError, "extra characters after #{o} at #{i}" if i != s.size
o
else
raise ParseError, "unexpected #{s[i]} at #{i}"
end
end
kv = '{"k":"v"}'.freeze
tests = [
['{}', {}],
['{"k":"v"}', {?k => ?v}],
# trailing comma is sadly not allowed
['{"k":"v",}', :raise],
['{"k1":"v1","k2":"v2"}', {'k1' => 'v1', 'k2' => 'v2'}],
['{}{}', :raise],
*(0...kv.size).map { |l| [kv[0...l], :raise] },
['{k:"v"}', :raise],
['{"k":v}', :raise],
# TIMING: Done with all string cases at 0:40
# (this included the time taken to actually write the tests)
['{"a":{"b":"c"}}', {?a => {?b => ?c}}],
# TIMING: Nested object took only four extra lines and < 1 minute.
]
passed = 0
failed = 0
tests.each { |i, exp|
puts i
begin
p = json(i)
rescue ParseError => e
if exp == :raise
passed += 1
else
puts " raised #{e}"
failed += 1
end
next
else
if exp == :raise
failed += 1
puts " didn't raise (=> #{p}), but should have"
next
end
end
if p == exp
passed += 1
else
failed += 1
puts " => #{p}, should be #{exp}"
end
}
puts "#{passed} passed, #{failed} failed"
raise "#{failed} failed" if failed > 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment