Powered by AppSignal & Oban Pro

Day 16

2021/day-16.livemd

Day 16

Setup

Mix.install([{:kino, "~> 0.4.1"}])
input = Kino.Input.text("input:")
defmodule BITS do
  def decode(string), do: decode([], string)

  defp decode(packets, rest) when byte_size(rest) > 7 do
    {packet, rest} = decode_one(rest)

    decode([packet | packets], rest)
  end

  defp decode(packets, _rest), do: Enum.reverse(packets)

  defp decode_one(<<version::binary-size(3), "100", rest::binary>>) do
    decode_literal(String.to_integer(version, 2), 4, rest)
  end

  defp decode_one(
         <<version::binary-size(3), type::binary-size(3), length_type::binary-size(1),
           rest::binary>>
       ) do
    decode_operator(
      String.to_integer(version, 2),
      String.to_integer(type, 2),
      length_type,
      rest
    )
  end

  defp decode_literal(version, type, rest) do
    {chunks, rest} = decode_literal_chunks(rest, [])

    literal = for chunk <- chunks, into: "", do: chunk

    {{:literal, version, type, String.to_integer(literal, 2)}, rest}
  end

  defp decode_literal_chunks(<<"0", chunk::binary-size(4), rest::binary>>, chunks),
    do: {Enum.reverse([chunk | chunks]), rest}

  defp decode_literal_chunks(<<"1", chunk::binary-size(4), rest::binary>>, chunks),
    do: decode_literal_chunks(rest, [chunk | chunks])

  defp decode_operator(version, type, "0", <<length::binary-size(15), rest::binary>>) do
    length = String.to_integer(length, 2)
    {sliced, rest} = String.split_at(rest, length)

    {{:operator, version, type, decode(sliced)}, rest}
  end

  defp decode_operator(version, type, "1", <<count::binary-size(11), rest::binary>>) do
    count = String.to_integer(count, 2)

    {subs, rest} =
      1..count
      |> Enum.reduce({[], rest}, fn _, {subs, rest} ->
        {sub, rest} = decode_one(rest)
        {[sub | subs], rest}
      end)

    {{:operator, version, type, Enum.reverse(subs)}, rest}
  end

  def new(input) do
    input
    |> String.split("", trim: true)
    |> Enum.map(fn c ->
      c
      |> String.to_integer(16)
      |> Integer.to_string(2)
      |> String.pad_leading(4, "0")
    end)
    |> Enum.join()
  end

  def eval(packets), do: eval(packets, [])

  defp eval([], acc), do: acc
  defp eval([{:literal, _, _, value} | packets], acc), do: eval(packets, [value | acc])

  defp eval([{:operator, _, 0, subs} | packets], acc),
    do: eval(packets, [eval(subs) |> Enum.sum() | acc])

  defp eval([{:operator, _, 1, subs} | packets], acc),
    do: eval(packets, [eval(subs) |> Enum.product() | acc])

  defp eval([{:operator, _, 2, subs} | packets], acc),
    do: eval(packets, [eval(subs) |> Enum.min() | acc])

  defp eval([{:operator, _, 3, subs} | packets], acc),
    do: eval(packets, [eval(subs) |> Enum.max() | acc])

  defp eval([{:operator, _, 5, [s1, s2]} | packets], acc),
    do: eval(packets, [if(eval([s1]) > eval([s2]), do: 1, else: 0) | acc])

  defp eval([{:operator, _, 6, [s1, s2]} | packets], acc),
    do: eval(packets, [if(eval([s1]) < eval([s2]), do: 1, else: 0) | acc])

  defp eval([{:operator, _, 7, [s1, s2]} | packets], acc),
    do: eval(packets, [if(eval([s1]) == eval([s2]), do: 1, else: 0) | acc])

  def sum(packets) do
    sum(packets, 0)
  end

  defp sum([{:literal, version, _, _} | rest], sum), do: sum(rest, sum + version)
  defp sum([{:operator, version, _, subs} | rest], sum), do: sum(subs ++ rest, sum + version)
  defp sum([], sum), do: sum
end

bits =
  input
  |> Kino.Input.read()
  |> BITS.new()
  |> BITS.decode()

Part 1

bits
|> BITS.sum()

Part 2

bits
|> BITS.eval()
|> hd()