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

Advent of Code 2024 - Day 04

elixir/2024/day_04.livemd

Advent of Code 2024 - Day 04

Mix.install([
  {:kino_aoc, "~> 0.1.7"}
])

Introduction

2024 - Day 04

Puzzle

{:ok, puzzle_input} =
  KinoAOC.download_puzzle("2024", "4", System.fetch_env!("LB_AOC_SESSION"))

Parser

Code - Parser

defmodule Parser do
  def parse(input) do
    input
    |> String.split("\n", trim: true)
    |> Enum.with_index()
    |> Enum.reduce(%{}, fn {line, y}, map ->
      line
      |> String.codepoints()
      |> Enum.with_index()
      |> Enum.reduce(map, fn {char, x}, map ->
        Map.put(map, {x, y}, char)
      end)
    end)
  end
end

Tests - Parser

ExUnit.start(autorun: false)

defmodule ParserTest do
  use ExUnit.Case, async: true
  import Parser

  @input """
  XA
  MB
  """
  @expected %{{0, 0} => "X", {0, 1} => "M", {1, 0} => "A", {1, 1} => "B"}

  test "parse test" do
    actual = parse(@input)
    assert actual == @expected
  end
end

ExUnit.run()

Part One

Code - Part 1

defmodule PartOne do
  @dir [{1, 0}, {1, -1}, {0, -1}, {-1, -1}, {-1, 0}, {-1, 1}, {0, 1}, {1, 1}]

  def solve(input) do
    IO.puts("--- Part One ---")
    IO.puts("Result: #{run(input)}")
  end

  def run(input) do
    map = Parser.parse(input)

    map
    |> Enum.filter(fn {_, v} -> v == "X" end)
    |> Enum.map(fn {{x, y}, _} -> count_xmas(map, x, y) end)
    |> Enum.sum()
  end

  defp count_xmas(map, x, y) do
    @dir
    |> Enum.count(fn {vx, vy} ->
      1..3
      |> Enum.map(fn v -> Map.get(map, {x + vx * v, y + vy * v}, "") end)
      |> Enum.join("") ==
        "MAS"
    end)
  end
end

Tests - Part 1

ExUnit.start(autorun: false)

defmodule PartOneTest do
  use ExUnit.Case, async: true
  import PartOne

  @input """
  MMMSXXMASM
  MSAMXMSMSA
  AMXSXMAAMM
  MSAMASMSMX
  XMASAMXAMM
  XXAMMXXAMA
  SMSMSASXSS
  SAXAMASAAA
  MAMMMXMMMM
  MXMXAXMASX
  """
  @expected 18

  test "part one" do
    actual = run(@input)
    assert actual == @expected
  end
end

ExUnit.run()

Solution - Part 1

PartOne.solve(puzzle_input)

Part Two

Code - Part 2

defmodule PartTwo do
  @up [{1, -1}, {-1, -1}]
  @down [{-1, 1}, {1, 1}]
  @left [{-1, -1}, {-1, 1}]
  @right [{1, -1}, {1, 1}]

  def solve(input) do
    IO.puts("--- Part Two ---")
    IO.puts("Result: #{run(input)}")
  end

  def run(input) do
    map = Parser.parse(input)

    map
    |> Enum.filter(fn {_, v} -> v == "A" end)
    |> Enum.count(fn {{x, y}, _} -> is_xmas(map, x, y) end)
  end

  defp is_xmas(map, x, y) do
    [@up, @down, @left, @right]
    |> Enum.map(fn dir ->
      dir
      |> Enum.map(fn {vx, vy} ->
        Map.get(map, {x + vx, y + vy}, "")
      end)
      |> Enum.join("")
    end)
    |> then(fn
      ["SS", "MM", _, _] -> true
      ["MM", "SS", _, _] -> true
      [_, _, "SS", "MM"] -> true
      [_, _, "MM", "SS"] -> true
      _ -> false
    end)
  end
end

Tests - Part 2

ExUnit.start(autorun: false)

defmodule PartTwoTest do
  use ExUnit.Case, async: true
  import PartTwo

  @input """
  MMMSXXMASM
  MSAMXMSMSA
  AMXSXMAAMM
  MSAMASMSMX
  XMASAMXAMM
  XXAMMXXAMA
  SMSMSASXSS
  SAXAMASAAA
  MAMMMXMMMM
  MXMXAXMASX
  """
  @expected 9

  test "part two" do
    actual = run(@input)
    assert actual == @expected
  end
end

ExUnit.run()

Solution - Part 2

PartTwo.solve(puzzle_input)