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

Day 2

livebook/2024/day_2.livemd

Day 2

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

Input

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

test input

test_input = """
  7 6 4 2 1
  1 2 7 8 9
  9 7 6 2 1
  1 3 2 4 5
  8 6 4 4 1
  1 3 6 7 9
"""

parser

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

RowParser.parse(test_input)

test parser

ExUnit.start(autorun: false)

defmodule RowParserTest do
  use ExUnit.Case, async: true

  @test_input """
  7 6 4 2 1
  1 2 7 8 9
  9 7 6 2 1
  1 3 2 4 5
  8 6 4 4 1
  1 3 6 7 9
  """

  test "row parse test" do
    assert RowParser.parse(@test_input) |> Enum.at(0) == [7, 6, 4, 2, 1]
    assert RowParser.parse(@test_input) |> Enum.at(5) == [1, 3, 6, 7, 9]
  end
end

ExUnit.run()

part 1

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

  def run(input) do
   input
   |> RowParser.parse()
   |> Enum.map(&evaluate_report/1)
   |> Enum.count(&(&1 == true))
  end

  def evaluate_report(report) do
    levels = report
    |> Enum.chunk_every(2, 1, :discard)

    in_range = levels
    |> Enum.map(&diff/1)
    |> Enum.all?(&in_range/1)

    all_positive = levels
    |> Enum.map(&diff/1)
    |> Enum.all?(&positive/1)

    all_negative = levels
    |> Enum.map(&diff/1)
    |> Enum.all?(&negative/1)

    in_range && all_positive || in_range && all_negative
  end

  def diff([a, b]), do: a - b

  def in_range(n), do: abs(n) in 1..3

  def positive(n), do: n > 0
  
  def negative(n), do: n < 0
end

PartOne.run(test_input)

Section

part 1 - test

ExUnit.start(autorun: false)

defmodule PartOneTest do
  use ExUnit.Case

  @input puzzle_input
  
  test "part one" do
    assert PartOne.run(@input) == 591
  end
end

ExUnit.run()

part 2


defmodule PartTwo do
  def run(input) do
    input
    |> RowParser.parse()
    |> Enum.map(&amp;evaluate_report/1)
    |> Enum.count(&amp;(&amp;1 == true))
  end

  def evaluate_report(report) do
    if valid_sequence?(report) do
      true
    else
      # Try removing each number to see if it makes sequence valid
      0..(length(report) - 1)
      |> Enum.any?(fn index ->
        report
        |> List.delete_at(index)
        |> valid_sequence?()
      end)
    end
  end

  def valid_sequence?(report) do
    diff = 
    report 
    |> Enum.chunk_every(2, 1, :discard)
    |> Enum.map(&amp;diff/1)

    all_valid = Enum.all?(diff, &amp;valid_diff?/1)

    cond do
      Enum.empty?(diff) -> false
        not all_valid -> false 
        Enum.all?(diff, &amp;(&amp;1 > 0)) -> true # all increasing
          Enum.all?(diff, &amp;(&amp;1 < 0)) -> true # all decreasing
          true -> false   
    end    
  end
  
  def diff([a, b]), do: b - a
  
  def valid_diff?(diff), do: abs(diff) in 1..3
end

# test_input
puzzle_input
|> PartTwo.run()

part 2 - test

ExUnit.start(autorun: false)

defmodule PartTwoTest do
  use ExUnit.Case

   @input puzzle_input

  test "part two" do
    assert PartTwo.run(@input) == 621
  end
end

ExUnit.run()