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

Advent of Code 2023 - Day 1

2023/aoc23.01.livemd

Advent of Code 2023 - Day 1

Introduction

Child elf amends a trebuchet calibration document and our goal is to extract the values.

Advent of Code 2023 Day 1: https://adventofcode.com/2023/day/1

Image prompt: https://imagine.meta.com/?prompt=child+elf+coloring+over+an+excel+spreadsheet

Input

input =
  File.read!(File.cwd!() <> "/code/advent_of_code/aoc2023/day01.input")
  |> String.split()

Part 1

We need to extract a two-digit number from every line. The tens digit is the first digit in the line; the ones digit is the last digit.

Since we’re extracting digits as characters, we need to subtract ASCII 0 from every number.

input
|> Enum.map(&amp;Enum.filter(&amp;1 |> to_charlist(), fn c -> c >= ?0 and c <= ?9 end))
|> Enum.map(&amp;{Enum.at(&amp;1, 0), Enum.at(&amp;1, -1)})
|> Enum.map(fn {t, o} -> (t - ?0) * 10 + o - ?0 end)
|> Enum.sum()

First pass was successful as written.

Part 2

Turns out digits as words are also valid digits so we need to amend our solution.

words_to_numbers = fn s ->
  Regex.split(~r/one|two|three|four|five|six|seven|eight|nine/, s, include_captures: true)
  |> Enum.map(fn
    "one" -> "1"
    "two" -> "2"
    "three" -> "3"
    "four" -> "4"
    "five" -> "5"
    "six" -> "6"
    "seven" -> "7"
    "eight" -> "8"
    "nine" -> "9"
    x -> x
  end)
  |> Enum.join()
end

# input = [
#   "two1nine",
#   "eightwothree",
#   "abcone2threexyz",
#   "xtwone3four",
#   "4nineeightseven2",
#   "zoneight234",
#   "7pqrstsixteen"
# ]

input
|> Enum.map(words_to_numbers)
|> Enum.map(&amp;Enum.filter(&amp;1 |> to_charlist(), fn c -> c > ?0 and c <= ?9 end))
|> Enum.map(&amp;{Enum.at(&amp;1, 0), Enum.at(&amp;1, -1)})
|> Enum.map(fn {t, o} -> (t - ?0) * 10 + o - ?0 end)
|> Enum.sum()

# 54623 is too high

Wrong answer. I suspect the “eightwothree” case may be a clue to the failure. If the string ended with “eightwo”, we would think the last digit is 8 and not 2.

If we were to continue down this path, we would need to evaluate the string one character at a time when mapping words to numbers. But let’s try something else…

Using modules to clean it up

Using modules to make the pattern matching cleaner. Re-implemented part 1 to set an API

defmodule Digits.Part1 do
  def first_digit(<>) when c >= ?0 and c <= ?9, do: c - ?0
  def first_digit(<<_, rest::binary>>), do: first_digit(rest)

  def last_digit(s), do: String.reverse(s) |> first_digit()
end
input
|> Enum.map(fn s -> 10 * Digits.Part1.first_digit(s) + Digits.Part1.last_digit(s) end)
|> Enum.sum()

Pattern matching FTW here. We look either a single digit, the text of a digit, or something else. We look forwards for the first number or we reverse the string and look for either “backwards.”

defmodule Digits.Part2 do
  # Stop when we encounter a new "digit"
  def first_digit(<>) when c >= ?0 and c <= ?9, do: c - ?0
  def first_digit(<<"one", _rest::binary>>), do: 1
  def first_digit(<<"two", _rest::binary>>), do: 2
  def first_digit(<<"three", _rest::binary>>), do: 3
  def first_digit(<<"four", _rest::binary>>), do: 4
  def first_digit(<<"five", _rest::binary>>), do: 5
  def first_digit(<<"six", _rest::binary>>), do: 6
  def first_digit(<<"seven", _rest::binary>>), do: 7
  def first_digit(<<"eight", _rest::binary>>), do: 8
  def first_digit(<<"nine", _rest::binary>>), do: 9
  def first_digit(<<_, rest::binary>>), do: first_digit(rest)

  # Reverse the string, then search for digits but backwards
  def last_digit(s), do: _last_digit(String.reverse(s))
  defp _last_digit(<>) when c >= ?0 and c <= ?9, do: c - ?0
  defp _last_digit(<<"eno", _rest::binary>>), do: 1
  defp _last_digit(<<"owt", _rest::binary>>), do: 2
  defp _last_digit(<<"eerht", _rest::binary>>), do: 3
  defp _last_digit(<<"ruof", _rest::binary>>), do: 4
  defp _last_digit(<<"evif", _rest::binary>>), do: 5
  defp _last_digit(<<"xis", _rest::binary>>), do: 6
  defp _last_digit(<<"neves", _rest::binary>>), do: 7
  defp _last_digit(<<"thgie", _rest::binary>>), do: 8
  defp _last_digit(<<"enin", _rest::binary>>), do: 9
  defp _last_digit(<<_, rest::binary>>), do: _last_digit(rest)
end
input
|> Enum.map(fn s -> 10 * Digits.Part2.first_digit(s) + Digits.Part2.last_digit(s) end)
|> Enum.sum()
defmodule Digits do
  @moduledoc """
  Silly module to DRY-ify the code for parts 1 & 2.
  """
  def sum(input, module) do
    input
    |> Enum.map(fn s -> 10 * module.first_digit(s) + module.last_digit(s) end)
    |> Enum.sum()
  end
end
Digits.sum(input, Digits.Part1) |> IO.inspect(label: :part1)
Digits.sum(input, Digits.Part2) |> IO.inspect(label: :part2)