# Advent of Code 2023 day 1: Trebuchet

## Part 1:

The task involves analyzing a calibration document containing lines of text. Each line represents a calibration value that needs to be recovered by extracting the first and last digits and combining them into a two-digit number. The goal is to find the sum of all these calibration values.

For example:

1abc2
pqr3stu8vwx
a1b2c3d4e5f
treb7uchet


In this example, the calibration values of these four lines are 12, 38, 15, and 77. Adding these together produces 142.

#!/usr/bin/env python3

import sys
import re

def main():
if len(sys.argv) != 2:
print("Error - No file provided.", file=sys.stderr)

total  = 0

with open(sys.argv[1], "r") as f:
for line in f:
start, end = (
re.search(r"\d", line).group(),
re.search(r"\d", line[::-1]).group(),
)
total += int(start) * 10 + int(end)

print(f"Final value: {total}")

if __name__ == "__main__":
main()



## Part 2:

In problem two, the calibration document may include digits spelled out with letters (e.g., "one," "two," etc.). The task is to identify the actual first and last digits in each line, taking into account both numerical digits and spelled-out numbers. The overall objective remains the same: find the sum of the calibration values obtained from combining the first and last digits of each line.

For example:

two1nine
eightwothree
abcone2threexyz
xtwone3four
4nineeightseven2
zoneight234
7pqrstsixteen


In this example, the calibration values are 29, 83, 13, 24, 42, 14, and 76. Adding these together produces 281.

#!/usr/bin/env python3

import sys

def main():
if len(sys.argv) != 2:
print("Error - No file provided.", file=sys.stderr)

digits = {
"one": 1,
"two": 2,
"three": 3,
"four": 4,
"five": 5,
"six": 6,
"seven": 7,
"eight": 8,
"nine": 9,
}

total = 0

with open(sys.argv[1], "r") as f:
for line in f:
start, end = 0, 0

for idx, ch in enumerate(line):
if ch.isdigit():
if not start:
start = int(ch)
end = int(ch)
else:
for digit in digits:
if line[idx:].startswith(digit):
if not start:
start = digits[digit]
end = digits[digit]
break

total += start * 10 + end

print(f"Final value: {total}")

if __name__ == "__main__":
main()


What is a simpler way to solve the problem? How do I reduce the nesting in part 2?

• Your code will consider "0" as a valid first/last digit, but will not consider "zero" as a spelt-out version of "0". Does the problem statement (ahem) spell out whether or not "0" or "zero" will appear in the input? Commented Dec 7, 2023 at 15:34
• @AJNeufeld No, the problem statement does not spell out that "0" or "zero" would appear in the input, that's why I didn't give any thought to it. But I forgot to mention that in the question, or add a link to the page: adventofcode.com/2023/day/1 . Nevertheless, I changed the code accordingly. Thank you. Commented Dec 7, 2023 at 18:31
• Thank-you for adding the link to the problem description in your post. Note however: the "Part 2" description is not available for those who have not registered with the site and submitted a solution for "Part 1". This is a high bar of effort for reviews of your code. We can't tell if the "0"/"zero" issue is a bug or just something which the problem states will never be seen. Ensure your problem description includes all of the problems requirements, including assumptions and limits. Commented Dec 7, 2023 at 21:41

# Bug

"1zero" is probably supposed to evaluate to 10. You don't handle the digit "zero".

# Reversing the string

Your first approach relies on line[::-1] in order to search for the "last" digit, by pretending it is the first in the reversed string.

Instead, you could easily search for the last digit with a slight modification of the regular expression:

re.search(r".*(\d)", line).group(1)


The .* is greedy, so it will match as many characters as possible, and then backtrack to find a digit. This may or may not be as clear for single digits. Without a doubt, it will be clearer than searching for (say) "evif" in a reversed string, so is probably a better approach.

# Matching digits/word-digits

You can expand your regex pattern to match "word-digits" as well as digits:

pattern = r"(\d|zero|one|two|three|four|five|six|seven|eight|nine)"
...
re.search(pattern, line)[1]


Searching for the last can be done by tacking .* to the front of the pattern as well.

# To ints

You use int(digit) or digits[digit] to convert digit/strings into numbers. If you mapped the digit words to the digit characters in the dictionary, then you could look up the character (or word) in the dictionary, (defaulting to the value being looked up, if not present), and then convert than to an integer:

    value = int(digits.get(token, token))


# Code Organization

You should break your code into reusable functions. If in "Part 1", you had a calibration_total(lines) function, which summed the calibration_value(line) for each line, and calibration_value(line) called first_digit(line) and last_digit(line) functions to get the first and last digit values ...

def first_digit(line: str) -> int:
...

def last_digit(line: str) -> int:
...

def calibration_value(line: str) -> int:
return 10 * first_digit(line) + last_digit(line)

def calibration_total(lines: Iterable[str]):
return sum(map(calibration_value, lines))

def main():
if len(sys.argv) != 2:
print("Error - No file provided.", file=sys.stderr)

with open(sys.argv[1], "r") as f:
total = calibration_total(f)

print(f"Final value: {total}")

if __name__ == "__main__":
main()


then for "Part 2", you'd just be adjusting the first_digit and last_digit functions. The "driving portion" of the program would not have needed to be changed.

Eg:

import re
from typing import Iterable

DIGIT_NAMES = "zero one two three four five six seven eight nine".split()
DIGIT = {key: i for i, key in enumerate(DIGIT_NAMES)}
DIGIT_RE = rf'(\d|{"|".join(DIGIT_NAMES)})'
FIRST_DIGIT_PATTERN = re.compile(DIGIT_RE)
LAST_DIGIT_PATTERN = re.compile(".*" + DIGIT_RE)

def digit_to_int(digit: str) -> int:
return int(DIGIT.get(digit, digit))

def first_digit(line: str) -> int:
return digit_to_int(FIRST_DIGIT_PATTERN.search(line)[1])

def last_digit(line: str) -> int:
return digit_to_int(LAST_DIGIT_PATTERN.search(line)[1])

• ""1zero" is probably supposed to evaluate to 10": No, zero doesn't count as a valid number. Commented Dec 7, 2023 at 9:02
• @InSync In that case, both versions of the OP’s code will have bugs. The first will find "0"’s with \d, and the second with ch.isdigit() Commented Dec 7, 2023 at 13:48
• There is no 0/zero in the input, so OP's code is fine. Commented Dec 7, 2023 at 14:39