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

Day 1

livebook/2023/day_1.livemd

Day 1

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

Input

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

Parser

defmodule Parser do
  @regex1 ~r/\d/
  # credit https://github.com/Lakret/aoc2023/blob/main/d01.livemd
  @regex2 ~r/(?(\d))|(?(one)|(two)|(three)|(four)|(five)|(six)|(seven)|(eight)|(nine))/

  @num_dict %{
    "one" => "1",
    "two" => "2",
    "three" => "3",
    "four" => "4",
    "five" => "5",
    "six" => "6",
    "seven" => "7",
    "eight" => "8",
    "nine" => "9"
  }

  def parse(input) do
    nums =
      Regex.scan(@regex1, input)
      |> List.flatten()

    (List.first(nums) <> List.last(nums))
    |> String.to_integer()
  end

  def parse_two(input) do
    matches =
      scan_with_overlaps(input, [])
      |> Enum.map(&amp;to_digit/1)

    (hd(matches) <> List.last(matches))
    |> String.to_integer()
  end

  defp scan_with_overlaps(input, []) when is_binary(input) do
    scan_with_overlaps(
      input,
      Regex.run(
        @regex2,
        input,
        return: :index,
        capture: :first
      ),
      []
    )
  end

  defp scan_with_overlaps(input, [{index, length}], matches) do
    scan_with_overlaps(
      input,
      Regex.run(
        @regex2,
        input,
        return: :index,
        capture: :first,
        offset: index + 1
      ),
      [String.slice(input, index, length) | matches]
    )
  end

  defp scan_with_overlaps(_input, nil, matches), do: Enum.reverse(matches)

  defp to_digit(digit) do
    Map.get(@num_dict, digit, digit)
  end
end

Test Parser

ExUnit.start(autorun: false)

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

  @input "1abc2"
  @expected 12

  test "parse test" do
    actual = parse(@input)
    assert actual == @expected
  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
    |> String.split("\n", trim: true)
    |> Enum.map(&amp;String.trim/1)
    |> Enum.map(&amp;Parser.parse/1)
    |> Enum.sum()
  end
end

Test Part 1

ExUnit.start(autorun: false)

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

  @input puzzle_input

  @expected 55477

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

ExUnit.run()
PartOne.solve(puzzle_input)

Part 2

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

  def run(input) do
    input
    |> String.split("\n", trim: true)
    |> Enum.map(&amp;String.trim/1)
    |> Enum.map(&amp;Parser.parse_two/1)
    |> Enum.sum()
  end
end

Test Part 2

ExUnit.start(autorun: false)

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

  @input puzzle_input
  @expected 54431

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

ExUnit.run()