language: elixir prompt: https://adventofcode.com/2020/day/19
#! /usr/bin/env elixir
# This is the messiest solution yet. I struggled with this one,
# partially due to a 1 month time gap (personal things) between
# part 1 and 2, and partially just exhausted and want to finish
# the project more than I want to have each solution be excellent.
#
# tl;dr: Elixir is a better language than my code here suggests.
defmodule Solve do
def count_for_zero(rulebook, targets) do
rule_tree = treeify({:start, "0"}, rulebook)
Enum.count(targets, &tree_contains?(rule_tree, &1))
end
def tree_contains?(rules, target) do
# tree_search could return a suffix, so explicitly require true
result = tree_search(rules, target)
true == result
end
def tree_search(_, false), do: false
def tree_search(_, true), do: false
def tree_search(_, ""), do: false
def tree_search(target, target), do: true
def tree_search({_, [rule]}, target), do: tree_search(rule, target)
def tree_search(prefix, target) when is_bitstring(prefix) do
String.starts_with?(target, prefix) && String.replace_prefix(target, prefix, "")
end
def tree_search({:or, [rule | rules]}, target) do
tree_search(rule, target) || tree_search({:or, rules}, target)
end
def tree_search({:and, [rule | rules]}, target) do
tree_search({:and, rules}, tree_search(rule, target))
end
def tree_search({:slurp, [left, :self, right]}, target) do
1..String.length(target)
|> Enum.scan({0, target}, fn _, {n, acc} ->
{n + 1, tree_search(left, acc)}
end)
|> Enum.filter(fn {_, acc} -> !is_boolean(acc) end)
|> Enum.map(fn {n_left, target_rem} ->
1..n_left
|> Enum.scan({0, target_rem}, fn _, {n, acc} ->
{n + 1, tree_search(right, acc)}
end)
|> Enum.any?(fn {_, acc} -> true == acc end)
end)
|> Enum.any?(fn b -> b end)
end
def treeify({:start, value}, rules), do: treeify(Map.get(rules, value), rules)
def treeify(leaf, _) when is_bitstring(leaf), do: leaf
def treeify({:slurp, [a, :self, b]}, rules) do
{:slurp, [treeify(Map.get(rules, a), rules), :self, treeify(Map.get(rules, b), rules)]}
end
def treeify({:or, ors}, rules) do
{
:or,
Enum.map(ors, fn {:and, ands} ->
{
:and,
Enum.map(ands, fn value ->
treeify(Map.get(rules, value), rules)
end)
}
end)
}
end
def parse_rules(rules), do: parse_rules(rules, %{})
def parse_rules([], acc), do: acc
def parse_rules([rule | rules], acc) do
[key, raw_value] = String.split(rule, ": ")
values =
case Regex.run(~r/"(.)"/, raw_value) do
[_, s] -> s
_ -> {:or, parse_rule(raw_value)}
end
parse_rules(rules, Map.put(acc, key, values))
end
def parse_rule(raw_value) do
raw_value
|> String.split(" | ")
|> Enum.map(&String.split(&1, " "))
|> Enum.map(fn values -> {:and, values} end)
end
end
{:ok, input} = File.read("./input")
[rule_text, targets | _] =
input
|> String.split("\n\n")
|> Enum.map(&String.split(&1, "\n"))
rulebook = Solve.parse_rules(rule_text)
IO.write(Solve.count_for_zero(rulebook, targets))
IO.puts(" messages are valid.")
forbidden_arts = %{
"0" => {:slurp, ["42", :self, "31"]}
}
IO.write(Solve.count_for_zero(Map.merge(rulebook, forbidden_arts), targets))
IO.puts(" messages are valid (with danger).")