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

Advent of Code - Day 9

elixir-livebook/day9.livemd

Advent of Code - Day 9

Mix.install([
  {:kino_aoc, "~> 0.1"},
  # {:benchee, "~> 1.0", only: :dev}
  {:kino_benchee, github: "akoutmos/kino_benchee", branch: "initial_release_prep"}
])

Introduction

Puzzle

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

Parser

Code - Parser

defmodule Parser do
  def parse(input) do
    input
    |> String.split("\n", trim: true)
    |> Enum.map(fn line ->
      line
      |> String.split(" ", trim: true)
      |> Enum.map(&String.to_integer/1)
    end)
  end
end

Tests - Parser

ExUnit.start(autorun: false)

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

  @input """
  0 3 6 9 12 15
  1 3 6 10 15 21
  10 13 16 21 30 45
  """
  @expected [
    [0, 3, 6, 9, 12, 15],
    [1, 3, 6, 10, 15, 21],
    [10, 13, 16, 21, 30, 45]
  ]

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

ExUnit.run()
defmodule DeltaCalcs do
  def recursive_deltas(sub_sequence, acc, level) do
    delta =
      sub_sequence
      |> Enum.chunk_every(2, 1)
      |> Enum.map(fn
        [a, b] -> a - b
        [_] -> nil
      end)
      |> Enum.reject(&is_nil/1)

    if Enum.all?(delta, &(&1 == 0)) do
      acc
    else
      recursive_deltas(delta, Map.put(acc, level + 1, delta), level + 1)
    end
  end

  def forward_calculate_deltas_until_zero(sequence) do
    sequence_reversed =
      sequence |> Enum.reverse()

    recursive_deltas(sequence_reversed, %{0 => sequence_reversed}, 0)
  end

  def backwards_calculate_deltas_until_zero(sequence) do
    recursive_deltas(sequence, %{0 => sequence}, 0)
  end

  def extrapolate(deltas) do
    deltas
    |> Map.values()
    |> Enum.map(fn delta -> hd(delta) end)
    |> Enum.sum()
  end
end

Part One

Code - Part 1

defmodule PartOne do
  import DeltaCalcs

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

  def run(input) do
    input
    |> Parser.parse()
    |> Enum.map(fn sequence ->
      sequence
      |> forward_calculate_deltas_until_zero()
      |> extrapolate()
    end)
    |> Enum.sum()
  end
end

Tests - Part 1

ExUnit.start(autorun: false)

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

  @input """
  0 3 6 9 12 15
  1 3 6 10 15 21
  10 13 16 21 30 45
  """
  @expected 114

  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
  import DeltaCalcs

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

  def run(input) do
    input
    |> Parser.parse()
    |> Enum.map(fn sequence ->
      sequence
      |> backwards_calculate_deltas_until_zero()
      |> extrapolate()
    end)
    |> Enum.sum()
  end
end

Tests - Part 2

ExUnit.start(autorun: false)

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

  @input """
  0 3 6 9 12 15
  1 3 6 10 15 21
  10 13 16 21 30 45
  """
  @expected 2

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

ExUnit.run()

Solution - Part 2

PartTwo.solve(puzzle_input)

Benchmarking

defmodule Benchmarks do
  def part1(input) do
    PartOne.run(input)
  end

  def part2(input) do
    PartTwo.run(input)
  end
end

Benchee.run(
  %{
    "day09.part1" => &Benchmarks.part1/1,
    "day09.part2" => &Benchmarks.part2/1
  },
  inputs: %{
    "puzzle" => puzzle_input
  },
  time: 1,
  memory_time: 1,
  reduction_time: 1
)