Created
April 22, 2020 20:43
-
-
Save JoshCheek/47eca3e9b8f48af2020c42233951dec2 to your computer and use it in GitHub Desktop.
Not sure why, but I suddenly felt compelled to make a JSON parser π€·ββοΈ
This file contains 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
def json_parse(json) | |
success, index, _ws = json_parse_optional_whitespace(0, json) | |
success, index, value = json_parse_value(index, json) | |
raise "Could not parse" unless success | |
value | |
end | |
def json_parse_value(index, json) | |
%I[ | |
json_parse_null | |
json_parse_true | |
json_parse_false | |
json_parse_float | |
json_parse_int | |
json_parse_string | |
json_parse_object | |
json_parse_array | |
].each do |name| | |
success, index, value = send(name, index, json) | |
return true, index, value if success | |
end | |
return false, index, nil | |
end | |
def json_parse_optional_whitespace(index, json) | |
whitespace = "" | |
loop do | |
char = json[index] | |
break unless char && char.match?(/\s/) | |
whitespace << char | |
index += 1 | |
end | |
return whitespace != "", index, whitespace | |
end | |
def json_parse_null(index, json) | |
json_parse_literal index, json, "null", nil | |
end | |
def json_parse_true(index, json) | |
json_parse_literal index, json, "true", true | |
end | |
def json_parse_false(index, json) | |
json_parse_literal index, json, "false", false | |
end | |
def json_parse_literal(index, json, literal, success_value, failure_value=success_value) | |
success, index = json_match_literal(index, json, literal) | |
return success, index, success ? success_value : failure_value | |
end | |
def json_match_literal(index, json, literal) | |
success = literal == json[index, literal.size] # inefficient, but easy implementation | |
return success, index + (success ? literal.size : 0) | |
end | |
def json_parse_float(index, json) | |
success, maybe_index, int = json_parse_int(index, json) | |
return false, index, nil unless success | |
success, maybe_index = json_match_literal maybe_index, json, "." | |
return false, index, nil unless success | |
success, maybe_index, decimal = json_parse_unsigned_int(maybe_index, json) | |
return false, index, nil unless success | |
return true, maybe_index, "#{int}.#{decimal}".to_f # stupid impl, but w/e | |
end | |
def json_parse_int(index, json) | |
success, index, sign = json_parse_literal(index, json, "-", -1, 1) | |
success, index, value = json_parse_unsigned_int(index, json) | |
return success, index, sign*value | |
end | |
def json_parse_unsigned_int(index, json) | |
int = 0 | |
i = index | |
loop do | |
char = json[i] | |
break if !char || char < '0' || '9' < char | |
int *= 10 | |
int += char.to_i | |
i += 1 | |
end | |
return i != index, i, int | |
end | |
def json_parse_string(index, json) | |
i = index | |
success, i = json_match_literal i, json, '"' | |
return false, index, "" unless success | |
str, escaped = "", false | |
loop do | |
char = json[i] | |
return false, index, "" unless char | |
if escaped | |
i += 1 | |
escaped = false | |
case char | |
when "n" then str << "\n" # newline | |
when "t" then str << "\t" # tab | |
when "\\" then str << "\\" # backslash | |
else str << char # there are more, but I don't feel like looking them up rn | |
end | |
next | |
end | |
break if char == '"' | |
i += 1 | |
if char == "\\" | |
escaped = true | |
else | |
str << char | |
end | |
end | |
success, i = json_match_literal i, json, '"' | |
return false, index, "" unless success | |
return true, i, str | |
end | |
def json_parse_object(index, json) | |
i = index | |
success, i = json_match_literal i, json, '{' | |
return false, index, {} unless success | |
# not actually sure if you can have `{"a":1,}`, this impl would allow it | |
object = {} | |
loop do | |
success, i, _ws = json_parse_optional_whitespace(i, json) | |
success, i, key = json_parse_string(i, json) | |
break unless success | |
success, i, _ws = json_parse_optional_whitespace(i, json) | |
success, i = json_match_literal(i, json, ':') | |
return false, index, {} unless success | |
success, i, _ws = json_parse_optional_whitespace(i, json) | |
success, i, val = json_parse_value(i, json) | |
return false, index, {} unless success | |
object[key] = val | |
success, i, _ws = json_parse_optional_whitespace(i, json) | |
success, i = json_match_literal(i, json, ',') | |
break unless success | |
end | |
success, i = json_match_literal(i, json, '}') | |
return false, index, {} unless success | |
return true, i, object | |
end | |
def json_parse_array(index, json) | |
i = index | |
success, i = json_match_literal i, json, '[' | |
return false, index, [] unless success | |
# not actually sure if you can have `{"a":1,}`, this impl would allow it | |
array = [] | |
loop do | |
success, i, _ws = json_parse_optional_whitespace(i, json) | |
success, i, val = json_parse_value(i, json) | |
break unless success | |
array << val | |
success, i, _ws = json_parse_optional_whitespace(i, json) | |
success, i = json_match_literal(i, json, ',') | |
break unless success | |
end | |
success, i = json_match_literal(i, json, ']') | |
return false, index, [] unless success | |
return true, i, array | |
end | |
require 'json' | |
json = JSON.dump({ | |
a: [1, 10, true, false, nil, [], [2,3], {}, {x: "y"}], | |
b: "str", | |
c: {obj: "val"}, | |
d: {}, | |
e: [], | |
f: 123.456, | |
g: true, | |
h: false, | |
i: nil, | |
j: -123, | |
k: -123.456, | |
l: 'abc"def', | |
m: 'abc\\"def', | |
}) | |
JSON.parse(json) == json_parse(json) # => true |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment