Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Day 12: JSAbacusFramework.io

2015/12.livemd

Day 12: JSAbacusFramework.io

Mix.install([
  {:kino, "~> 0.12.3"}
])

Modules

defmodule Parser do
  def parse(input) do
    parser = json()

    parser.(input)
    |> elem(1)
  end

  defp json() do
    lazy(fn -> choice([string(), integer(), array(), object()]) end)
  end

  defp lazy(combinator) do
    fn input ->
      parser = combinator.()
      parser.(input)
    end
  end

  defp object() do
    sequence([
      char(?{),
      optional(separated_list(key_value_pair(), char(?,))),
      char(?})
    ])
    |> map(fn [_, pairs, _] ->
      Enum.reduce(pairs, %{}, fn
        {key, value}, acc -> Map.put(acc, key, value)
      end)
    end)
  end

  defp key_value_pair() do
    sequence([
      string(),
      char(?:),
      json()
    ])
    |> map(fn [key, _, value] -> {key, value} end)
  end

  defp array() do
    sequence([
      char(?[),
      optional(separated_list(json(), char(?,))),
      char(?])
    ])
    |> map(fn
      [_, nil, _] -> []
      [_, values, _] -> values
    end)
  end

  defp integer() do
    sequence([optional(char(?-)), some(digit())])
    |> map(fn
      [nil, digits] -> digits
      [?-, digits] -> [?- | digits]
    end)
    |> map(&to_string/1)
    |> map(&String.to_integer/1)
  end

  defp string() do
    sequence([char(?"), many(ascii_letter()), char(?")])
    |> map(fn [_, chars, _] -> to_string(chars) end)
    |> map(&to_string/1)
  end

  defp map(parser, mapper) do
    fn input ->
      with {:ok, term, rest} <- parser.(input) do
        {:ok, mapper.(term), rest}
      end
    end
  end

  defp choice(parsers) do
    fn input ->
      case parsers do
        [] ->
          {:error, "no parser succeeded"}

        [first_parser | other_parsers] ->
          with {:error, _reason} <- first_parser.(input) do
            choice(other_parsers).(input)
          end
      end
    end
  end

  defp separated_list(element_parser, separator_parser) do
    sequence([
      element_parser,
      many(sequence([separator_parser, element_parser]))
    ])
    |> map(fn [first_element, rest] ->
      other_elements = Enum.map(rest, fn [_, element] -> element end)
      [first_element | other_elements]
    end)
  end

  defp sequence(parsers) do
    fn input ->
      case parsers do
        [] ->
          {:ok, [], input}

        [first_parser | other_parsers] ->
          with {:ok, first_term, rest} <- first_parser.(input),
               {:ok, other_terms, rest} <- sequence(other_parsers).(rest) do
            {:ok, [first_term | other_terms], rest}
          end
      end
    end
  end

  defp digit() do
    satisfy(char(), &amp;(&amp;1 in ?0..?9))
  end

  defp ascii_letter() do
    satisfy(char(), &amp;(&amp;1 in ?a..?z or &amp;1 in ?A..?Z))
  end

  defp optional(parser) do
    fn input ->
      case parser.(input) do
        {:error, _reason} -> {:ok, nil, input}
        {:ok, term, rest} -> {:ok, term, rest}
      end
    end
  end

  defp some(parser) do
    sequence([parser, many(parser)])
    |> map(fn [first_term, other_terms] -> [first_term | other_terms] end)
  end

  defp many(parser) do
    fn input ->
      case parser.(input) do
        {:error, _reason} ->
          {:ok, [], input}

        {:ok, first_term, rest} ->
          {:ok, other_terms, rest} = many(parser).(rest)
          {:ok, [first_term | other_terms], rest}
      end
    end
  end

  defp satisfy(parser, acceptor) do
    fn input ->
      with {:ok, term, rest} <- parser.(input) do
        if acceptor.(term) do
          {:ok, term, rest}
        else
          {:error, "term rejected"}
        end
      end
    end
  end

  defp char(expected) do
    satisfy(char(), &amp;(&amp;1 == expected))
  end

  defp char() do
    fn
      "" -> {:error, "unexpected eof"}
      <> -> {:ok, char, rest}
    end
  end
end
defmodule Json do
  def numbers(json, excluding_objects \\ :none) do
    json
    |> leaves(excluding_objects)
    |> Enum.filter(&amp;is_integer/1)
  end

  defp leaves(json, exclude_objects)
  defp leaves(json, _) when is_integer(json), do: [json]
  defp leaves(json, _) when is_bitstring(json), do: [json]

  defp leaves(json, exclude_objects) when is_list(json) do
    Enum.reduce(json, [], fn value, acc -> acc ++ leaves(value, exclude_objects) end)
  end

  defp leaves(json, exclude_objects) when is_map(json) do
    values = Map.values(json)
    excluding = to_string(exclude_objects)

    if excluding in values do
      []
    else
      Enum.reduce(Map.values(json), [], fn
        value, acc -> acc ++ leaves(value, exclude_objects)
      end)
    end
  end
end

Input

input = Kino.Input.textarea("Please paste your puzzle input:")

Part 1

input
|> Kino.Input.read()
|> Parser.parse()
|> Json.numbers()
|> Enum.sum()
input
|> Kino.Input.read()
|> Parser.parse()
|> Json.numbers(:red)
|> Enum.sum()