Created
March 17, 2017 04:56
-
-
Save petertseng/909e56876812100ccdf322dab700230b to your computer and use it in GitHub Desktop.
mini JSON
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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