From: James Gray Date: 2008-02-07T00:42:56+09:00 Subject: Re: Parsing JSON (#155) On Feb 5, 2008, at 5:27 PM, Eric Mahurin wrote: > ch/s F E gzip author/gem > ------- - - ---- ---------- > 186042 5 0 685 James Edward Gray II (RE, recursive descent) Here's a revised version of this parser that passes all of the tests: #!/usr/bin/env ruby -wKU require "strscan" class JSONParser AST = Struct.new(:value) def parse(input) @input = StringScanner.new(input.strip) parse_value.value ensure @input.eos? or error("Unexpected data") end private def parse_value trim_space parse_object or parse_array or parse_string or parse_number or parse_keyword or error("Illegal JSON value") ensure trim_space end def parse_object if @input.scan(/\{\s*/) object = Hash.new more_pairs = false while key = parse_string @input.scan(/\s*:\s*/) or error("Expecting object separator") object[key.value] = parse_value.value more_pairs = @input.scan(/\s*,\s*/) or break end error("Missing object pair") if more_pairs @input.scan(/\s*\}\s*/) or error("Unclosed object") AST.new(object) else false end end def parse_array if @input.scan(/\[\s*/) array = Array.new more_values = false while contents = parse_value rescue nil array << contents.value more_values = @input.scan(/\s*,\s*/) or break end error("Missing value") if more_values @input.scan(/\s*\]\s*/) or error("Unclosed array") AST.new(array) else false end end def parse_string if @input.scan(/"/) string = String.new while contents = parse_string_content || parse_string_escape string << contents.value end @input.scan(/"\s*/) or error("Unclosed string") AST.new(string) else false end end def parse_string_content @input.scan(/[^\\"]+/) and AST.new(@input.matched) end def parse_string_escape if @input.scan(%r{\\["\\/]}) AST.new(@input.matched[-1]) elsif @input.scan(/\\[bfnrt]/) AST.new(eval(%Q{"#{@input.matched}"})) elsif @input.scan(/\\u[0-9a-fA-F]{4}/) AST.new([Integer("0x#{@input.matched[2..-1]}")].pack("U")) else false end end def parse_number @input.scan(/-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?\b/) and AST.new(eval(@input.matched)) end def parse_keyword @input.scan(/\b(?:true|false|null)\b/) and AST.new(eval(@input.matched.sub("null", "nil"))) end def trim_space @input.scan(/\s+/) end def error(message) if @input.eos? raise "Unexpected end of input." else raise "#{message}: #{@input.peek(@input.string.length)}" end end end __END__ James Edward Gray II