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

AoC 2023 Day 19

2023/day19.livemd

AoC 2023 Day 19

Mix.install([
  {:kino_aoc, "~> 0.1.5"},
  {:nimble_parsec, "~> 1.4"}
])

Input

{:ok, puzzle_input} =
  KinoAOC.download_puzzle("2023", "19", System.fetch_env!("LB_AOC_COOKIE_SECRET"))
# puzzle_input = """
# px{a<2006:qkq,m>2090:A,rfg}
# pv{a>1716:R,A}
# lnx{m>1548:A,A}
# rfg{s<537:gd,x>2440:R,A}
# qs{s>3448:A,lnx}
# qkq{x<1416:A,crn}
# crn{x>2662:A,R}
# in{s<1351:px,qqz}
# qqz{s>2770:qs,m<1801:hdj,R}
# gd{a>3333:R,R}
# hdj{m>838:A,pv}

# {x=787,m=2655,a=1222,s=2876}
# {x=1679,m=44,a=2067,s=496}
# {x=2036,m=264,a=79,s=2244}
# {x=2461,m=1339,a=466,s=291}
# {x=2127,m=1623,a=2188,s=1013}
# """

Parsing

defmodule Parser do
  import NimbleParsec

  id =
    choice([
      ascii_string([?a..?z], min: 1),
      string("A"),
      string("R")
    ])
    |> unwrap_and_tag(:id)

  property =
    choice([
      string("x"),
      string("m"),
      string("a"),
      string("s")
    ])
    |> unwrap_and_tag(:property)

  comparison =
    choice([
      string(">"),
      string("<")
    ])
    |> unwrap_and_tag(:comparison)

  defparsec(
    :rule,
    property
    |> concat(comparison)
    |> unwrap_and_tag(integer(min: 1), :number)
    |> ignore(string(":"))
    |> concat(id)
    |> ignore(optional(string(",")))
    |> tag(:rule)
  )

  workflow =
    id
    |> ignore(string("{"))
    |> times(parsec(:rule), min: 1)
    |> ignore(optional(string(",")))
    |> concat(id |> unwrap_and_tag(:default))
    |> ignore(string("}"))

  defparsec(:workflow, workflow)

  part_value =
    property
    |> ignore(string("="))
    |> unwrap_and_tag(integer(min: 1), :value)
    |> ignore(optional(string(",")))
    |> wrap()

  part =
    ignore(string("{"))
    |> times(part_value, min: 1)
    |> ignore(string("}"))

  defparsec(:part, part)
end
defmodule Part do
  def parse(string) do
    {:ok, parsed, _, _, _, _} = Parser.part(string)
    Enum.into(parsed, %{}, fn [property: p, value: v] -> {String.to_atom(p), v} end)
  end
end
defmodule Rule do
  defstruct [:compare, :destination]

  def from_parsed(parsed) do
    prop = parsed[:property] |> String.to_atom()

    compare = fn part ->
      case parsed[:comparison] do
        ">" -> part[prop] > parsed[:number]
        "<" -> part[prop] < parsed[:number]
      end
    end

    %Rule{
      compare: compare,
      destination: String.to_atom(parsed[:id])
    }
  end
end
defmodule Workflow do
  defstruct [:id, :rules, :fallback]

  use GenServer

  def parse(string) do
    {:ok, parsed, _, _, _, _} = Parser.workflow(string)
    id = String.to_atom(parsed[:id])
    {:id, default} = parsed[:default]

    rules =
      parsed
      |> Keyword.get_values(:rule)
      |> Enum.map(&amp;Rule.from_parsed/1)

    %Workflow{
      id: id,
      rules: rules,
      fallback: String.to_atom(default)
    }
  end

  def apply_rules(part, %Workflow{rules: [], fallback: fallback}), do: send(fallback, part)

  def apply_rules(part, %Workflow{rules: [rule | rules]} = wf) do
    case rule.compare.(part) do
      true -> send(rule.destination, part)
      false -> apply_rules(part, %Workflow{wf | rules: rules})
    end
  end

  # Process logic

  def start_link(%Workflow{} = workflow) do
    GenServer.start_link(__MODULE__, workflow, name: workflow.id)
  end

  @impl true
  def init(%Workflow{} = state) do
    {:ok, state}
  end

  @impl true
  def handle_info(part, state) do
    apply_rules(part, state)

    {:noreply, state}
  end
end
[workflows, parts] = String.split(puzzle_input, "\n\n", trim: true)

workflows =
  workflows
  |> String.split("\n")
  |> Enum.map(&amp;Workflow.parse/1)
parts =
  parts
  |> String.split("\n", trim: true)
  |> Enum.map(&amp;Part.parse/1)

Part 1

defmodule Rejector do
  use GenServer

  def start_link(args), do: GenServer.start_link(__MODULE__, args, name: :R)

  def init(_), do: {:ok, []}

  def handle_info(msg, state), do: {:noreply, [msg | state]}
end

defmodule Acceptor do
  use GenServer

  def start_link(args), do: GenServer.start_link(__MODULE__, args, name: :A)

  def init(_), do: {:ok, 0}

  def handle_info(part, state) do
    part_rating =
      Map.values(part)
      |> Enum.sum()

    {:noreply, state + part_rating}
  end
end
Rejector.start_link([])
Acceptor.start_link([])
workflows |> Enum.map(&amp;Workflow.start_link/1)
parts |> Enum.map(fn p -> send(:in, p) end)
:sys.get_state(:A)

Part 2