defmodule Mix.Tasks.Aoc do use Mix.Task import Aoc2023.Common defp module(1), do: Aoc2023.Day1 defp module(2), do: Aoc2023.Day2 defp module(3), do: Aoc2023.Day3 defp module(4), do: Aoc2023.Day4 defp module(5), do: Aoc2023.Day5 defp module(6), do: Aoc2023.Day6 defp module(7), do: Aoc2023.Day7 defp module(8), do: Aoc2023.Day8 defp module(9), do: Aoc2023.Day9 defp module(10), do: Aoc2023.Day10 defp module(11), do: Aoc2023.Day11 defp module(12), do: Aoc2023.Day12 defp module(13), do: Aoc2023.Day13 defp module(14), do: Aoc2023.Day14 defp module(15), do: Aoc2023.Day15 defp module(16), do: Aoc2023.Day16 defp module(17), do: Aoc2023.Day17 defp module(18), do: Aoc2023.Day18 # [MODULE INSERTION POINT] defp base_dir(), do: System.get_env("AOC_BASE") defp tests(day) do case File.ls(tests_dir(day)) do {:ok, paths} -> paths |> Enum.filter(&(not String.contains?(&1, "."))) |> Enum.sort() {:error, e} -> dbg(e) [] end end defp read(path) do f = File.open!(path, [:utf8]) contents = IO.read(f, :eof) :ok = File.close(f) String.trim(contents) end defp test(day, test) do f = "#{tests_dir(day)}#{test}" { test, read(f), if File.exists?(f <> ".1") do read(f <> ".1") else nil end, if File.exists?(f <> ".2") do read(f <> ".2") else nil end } end defp tests_dir(day), do: "#{base_dir()}/tests/day#{day}/" defp create_file(path, contents) do File.mkdir_p!(Path.dirname(path)) f = File.open!(path, [:exclusive, :utf8]) IO.write(f, contents) :ok = File.close(f) end def run(params) do {opts, params} = params |> OptionParser.parse!(strict: [day: :integer]) day = opts |> Keyword.get(:day, Date.utc_today().day) run(day, params) end defp run(day, ["new"]) do create_file("#{base_dir()}/lib/day#{day}.ex", """ defmodule Aoc2023.Day#{day} do use Aoc2023 def parse(input) do input end def part1(_input) do end def part2(_input) do end end """) this_file = "#{base_dir()}/lib/mix_tasks.ex" contents = read(this_file) |> String.replace( "\n # [MODULE INSERTION POINT]", "\n defp module(#{day}), do: Aoc2023.Day#{day}\n # [MODULE INSERTION POINT]" ) f = File.open!(this_file, [:write, :utf8]) IO.write(f, contents) :ok = File.close(f) end defp run(day, ["test", "new"]) do last = tests(day) |> Enum.map(&String.to_integer(&1)) |> Enum.max(&>=/2, fn -> 0 end) create_file("#{tests_dir(day)}#{last + 1}", IO.read(:stdio, :eof)) end defp run(day, ["test", "expected", test, part, value]) do create_file("#{tests_dir(day)}#{test}.#{part}", value) end defp run(day, ["test", "expected", test, part]) do create_file("#{tests_dir(day)}#{test}.#{part}", IO.read(:stdio, :eof)) end defp run(day, ["test", "watch"]) do {:ok, pid} = Mix.Tasks.Aoc.Watcher.start_link({[dirs: ["lib/", "tests/day#{day}/"]], self()}) Process.monitor(pid) loop = fn loop -> IO.puts(IO.ANSI.clear() <> IO.ANSI.cursor(0, 0)) IO.puts("At #{DateTime.utc_now(:second)}:") System.shell("mix compile") :code.purge(module(day)) :code.purge(Aoc2023.Common) :code.load_file(module(day)) :code.load_file(Aoc2023.Common) spawn(fn -> run(day, ["test"]) end) receive do :update -> loop.(loop) {:DOWN, _, :process, ^pid, _} -> nil end end loop.(loop) end defp run(day, ["test"]) do mod = module(day) tests = tests(day) tests |> Enum.map(&test(day, &1)) |> Enum.map(fn {test, input, p1e, p2e} -> parsed = mod.parse(input) result = fn part, got, expected -> cond do stringify(got) == expected -> IO.puts(IO.ANSI.format([:green, "Test #{test}.#{part} succeeded (#{expected})"])) expected -> IO.puts( IO.ANSI.format([ :red, "Test #{test}.#{part} failed: expected #{expected}, got #{stringify(got)}" ]) ) true -> IO.puts("Test #{test}.#{part}: #{stringify(got)}") end end result.(1, mod.part1(parsed), p1e) result.(2, mod.part2(parsed), p2e) end) end defp run(day, ["run"]) do run(day, ["fetch"]) run_file(day, "#{base_dir()}/inputs/day#{day}") end defp run(day, ["run", "submit", part]) do run(day, ["fetch"]) mod = module(day) parsed = mod.parse(read("#{base_dir()}/inputs/day#{day}")) val = case part do "1" -> mod.part1(parsed) "2" -> mod.part2(parsed) end HTTPoison.start() resp = HTTPoison.post!( "https://adventofcode.com/2023/day/#{day}/answer", "level=#{part}&answer=#{stringify(val)}", "user-agent": "aoc-ex by ben@soroos.net", cookie: "session=#{System.get_env("AOC_SESSION")}", "content-type": "application/x-www-form-urlencoded" ).body Regex.named_captures(~r[
(?
.*)
]is, resp)["main"] |> IO.puts end defp run(day, ["run", "-"]) do run_file(day) end defp run(day, ["fetch"]) do file = "#{base_dir()}/inputs/day#{day}" if not File.exists?(file) do HTTPoison.start() resp = HTTPoison.get!("https://adventofcode.com/2023/day/#{day}/input", "user-agent": "aoc-ex by ben@soroos.net", cookie: "session=#{System.get_env("AOC_SESSION")}" ) create_file(file, resp.body) end end defp run_file(day, file \\ nil) do module(day).main(if file, do: read(file), else: IO.read(:stdio, :eof)) end defmodule Watcher do use GenServer def start_link(args) do GenServer.start_link(__MODULE__, args) end def init({args, callback}) do {:ok, watcher_pid} = FileSystem.start_link(args) FileSystem.subscribe(watcher_pid) {:ok, %{watcher_pid: watcher_pid, callback: callback}} end def handle_info( {:file_event, watcher_pid, {_path, _events}}, %{watcher_pid: watcher_pid, callback: callback} = state ) do send(callback, :update) {:noreply, state} end def handle_info({:file_event, _watcher_pid, :stop}, state) do {:noreply, state} end end end