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

Advent of Code Template

2023-05.livemd

Advent of Code Template

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

Problem

{:ok, input} = KinoAOC.download_puzzle("2023", "5", System.fetch_env!("LB_AOC_SESSION"))

Solvers

defmodule ParserDay5 do
  import NimbleParsec

  @labels_in_order ~w(seed soil fertilizer water light temperature humidity location)

  ws = ascii_char([?\s]) |> times(min: 1) |> ignore()
  newlines = ascii_char([?\n]) |> times(min: 1) |> ignore()

  seeds =
    string("seeds: ")
    |> ignore()
    |> concat(
      ws
      |> optional()
      |> integer(min: 1)
      |> times(min: 1)
    )
    |> tag(:seeds)

  label =
    @labels_in_order
    |> Enum.map(&string/1)
    |> choice()
    |> map({String, :to_atom, []})

  range_map =
    integer(min: 1)
    |> concat(optional(ws))
    |> times(3)
    |> reduce(:parse_range)
    |> concat(optional(newlines))

  mapping =
    label
    |> concat(ignore(string("-to-")))
    |> concat(label)
    |> concat(ignore(string(" map:")))
    |> concat(newlines)
    |> concat(times(range_map, min: 1))
    |> tag(:mapping)
    |> concat(optional(newlines))

  defparsec(
    :input,
    seeds
    |> concat(newlines)
    |> concat(
      mapping
      |> times(min: 1)
      |> reduce(:create_mappings_map)
    )
  )

  def labels, do: @labels_in_order

  defp create_mappings_map(mappings) do
    {:mappings,
     Map.new(mappings, fn {:mapping, [from, to | ranges]} ->
       {{from, to}, Enum.sort(ranges)}
     end)}
  end

  defp parse_range([dest_start, source_start, range_size]) do
    {range_from_start_size(source_start, range_size), dest_start - source_start}
  end

  defp range_from_start_size(start, size), do: Range.new(start, start + size)
end
input
|> ParserDay5.input()
target = 17
offset = 34

Enum.find_value([1..8], target, fn range ->
  if target in range, do: target + offset
end)
defmodule PartOne do
  def parse(input) do
    {:ok, parsed, "", _context, _line, _column} = ParserDay5.input(input)
    parsed
  end

  def process(input) do
    seeds = Keyword.fetch!(input, :seeds)
    mappings = Keyword.fetch!(input, :mappings)

    conversion_order = Enum.map(ParserDay5.labels(), &String.to_atom/1)

    seeds
    |> Enum.map(&seed_to_location(&1, conversion_order, mappings))
    |> Enum.min()
  end

  def solve(input) do
    input
    |> parse()
    |> process()
  end

  def maybe_convert_number(n, current, next, mappings) do
    mappings
    |> Map.fetch!({current, next})
    |> Enum.find_value(n, fn {range, offset} ->
      if n in range, do: n + offset
    end)
  end

  def seed_to_location(seed, conversion_order, mappings) do
    conversion_order
    |> Enum.chunk_every(2, 1, :discard)
    |> Enum.reduce(seed, fn [current, next], n ->
      maybe_convert_number(n, current, next, mappings)
    end)
  end
end
defmodule PartTwo do
  import PartOne, only: [parse: 1]

  def process(_input) do
    ""
  end

  def solve(input) do
    input
    |> parse()
    |> process()
  end
end

Solutions

PartOne.solve(input)
PartTwo.solve(input)

Tests

ExUnit.start(auto_run: false, seed: 12345, timeout: 5000)

defmodule PartOneTest do
  use ExUnit.Case, async: true

  doctest PartOne

  describe "Part One" do
    @test_input """
    seeds: 79 14 55 13

    seed-to-soil map:
    50 98 2
    52 50 48

    soil-to-fertilizer map:
    0 15 37
    37 52 2
    39 0 15

    fertilizer-to-water map:
    49 53 8
    0 11 42
    42 0 7
    57 7 4

    water-to-light map:
    88 18 7
    18 25 70

    light-to-temperature map:
    45 77 23
    81 45 19
    68 64 13

    temperature-to-humidity map:
    0 69 1
    1 0 69

    humidity-to-location map:
    60 56 37
    56 93 4

    """
    test "finds closest location" do
      assert PartOne.solve(@test_input) == 35
    end
  end
end

defmodule PartTwoTest do
  use ExUnit.Case, async: true

  @moduletag :skip

  doctest PartOne

  describe "Part Two" do
    @test_input """
    """
    test "TODO" do
      assert PartTwo.solve(@test_input) == false
    end
  end
end

ExUnit.run()

Golfing

Benchmarks

Benchee.run(
  %{
    "PartOne" => &PartOne.solve/1,
    "PartTwo" => &PartTwo.solve/1
  },
  inputs: %{
    input: input,
    test_input: """
    """
  },
  warmup: 2,
  time: 3,
  memory_time: 3,
  reduction_time: 3
)

Failures

Sometimes my ideas don’t work out.