Skip to content

Instantly share code, notes, and snippets.

@ndac-todoroki
Last active April 17, 2017 11:11
Show Gist options
  • Save ndac-todoroki/22cb4bf30d60a8ce481643fd83ae490a to your computer and use it in GitHub Desktop.
Save ndac-todoroki/22cb4bf30d60a8ce481643fd83ae490a to your computer and use it in GitHub Desktop.
JSON parser on Elixir (for studying Elixir)
defmodule JSON do
@object_start "{"
@object_end "}"
@list_start "["
@list_end "]"
@splitter ","
@keyval_seperator ":"
@string_sybil "\""
@newline_sybil "\n"
@space " "
@numbers ~w(0 1 2 3 4 5 6 7 8 9)
defp to_number(string) when is_binary(string) do
try do
String.to_float(string)
rescue
ArgumentError -> String.to_integer(string)
end
end
# Parse JSON String to Elixir object.
#
# @param json [String] input JSON string
# @return { :ok, result }
def parse(json) when is_binary(json) do
{:ok, { result, _length}} = parse(json, 0)
{:ok, result}
end
defp parse(string, position) when is_binary(string) and is_integer(position) do
head = String.at(string, position)
case head do
@object_start -> parse_object(string, position)
@list_start -> parse_list(string, position)
@string_sybil -> parse_string(string, position)
"t" -> parse_true(string, position)
"f" -> parse_false(string, position)
"n" -> parse_null(string, position)
n when n in [@space, @newline_sybil] ->
parse(string, position + 1)
n when n in @numbers ->
parse_number(string, position)
_ -> raise "Unparsable error at #{head}"
end
end
# @return {:ok, {result_map, end_poz}}
defp parse_object(string, position, prev_map\\%{}) when is_integer(position) do
head = String.at(string, position)
case head doElixir
@object_start ->
parse_object(string, position + 1, prev_map)
@string_sybil ->
{:ok, { map = %{}, end_poz } } = parse_keyval(string, position)
parse_object(string, end_poz + 1, Map.merge(prev_map, map))
n when n in [@splitter, @space, @newline_sybil] ->
parse_object(string, position + 1, prev_map)
@object_end ->
{:ok, { prev_map, position }} # positionが } なので end_poz になる
_ ->
raise "JSON Object's key must be a String, but got #{head}"
end
end
# @return {:ok, {result_string, end_poz}}
defp parse_string(string, position) do
cond do
String.at(string, position) == "\"" ->
{:ok, {start..final}} = parse_string(string, position + 1, 0)
{:ok, {String.slice(string, start..final), position + (final - start) + 2}}
true ->
raise "not a valid JSON string"
end
end
# Stringの\"から\"までの間だけのrangeを返す
defp parse_string(string, position, counter) when is_integer(counter) do
head = String.at(string, position + counter)
case head do
"\\" ->
case String.at(string, position + counter + 2) do
l when l in ~w(\" \/ \\ \b \f \n \r \t) ->
parse_string(string, position, counter + 2) # escapes next letter
"\\u" ->
parse_string(string, position, counter + 5)
_ ->
raise "backslash not followed by an escapable letter!"
end
nil -> raise "out of bounds. maybe string ends with a unescaped backslash?"
@string_sybil ->
{:ok, {position..position + counter - 1}} # ends
_ ->
parse_string(string, position, counter + 1)
end
end
# @return {:ok, { %{key => val}, end_poz }}
defp parse_keyval(string, position) when is_binary(string) do
{:ok, {key, key_end}} = parse_string(string, position)
{:ok, value_poz} = get_value_poz(string, key_end + 1)
{:ok, {val, end_poz}} = parse(string, value_poz)
{:ok, {%{key => val}, end_poz}}
end
defp get_value_poz(string, poz) when is_integer(poz) do
head = String.at(string, poz)
case head do
@space -> get_value_poz(string, poz + 1)
@keyval_seperator -> get_value_poz(string, poz + 1)
_ -> {:ok, poz}
end
end
# {:ok, {float, end_poz}}
defp parse_number(string, position, temp\\"") when is_binary(string) do
head = String.at(string, position)
case head do
n when n in ["." | @numbers] ->
parse_number(string, position + 1, temp <> head)
_ ->
{:ok, { to_number(temp), position - 1 }}
end
end
defp parse_list(string, position, prev_list\\[]) when is_binary(string) and is_integer(position) do
head = String.at(string, position)
case head do
@list_start ->
parse_list(string, position + 1)
n when n in [@splitter, @space] ->
parse_list(string, position + 1, prev_list)
@list_end ->
{:ok, { Enum.reverse(prev_list), position }}
_ ->
{:ok, { result, end_poz }} = parse(string, position)
parse_list(string, end_poz + 1, [result | prev_list])
end
end
defp parse_true(string, position) do
"true" = String.slice(string, position..position+3)
{:ok, {true, position + 4}}
end
defp parse_false(string, position) do
"false" = String.slice(string, position..position+4)
{:ok, {true, position + 5}}
end
defp parse_null(string, position) do
"null" = String.slice(string, position..position+3)
{:ok, {nil, position + 4}}
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment