Advent of code 2023: 01
Tags: python, adventofcode, programming
Published: 2024-03-28 00:09:27

While it's 2024 and the challenge is long past, I've decided to have a look at the 2023 advent of code. The aim of putting this in a blog series, is to document my approach and gain some feedback. I'm not sure how many of these challenges i'll end up tackling, but i suppose now is as good a moment as any to get started with [number one](https://adventofcode.com/2023/day/1)

## Part 1

As a brief summary, the goal is to parse a bunch of text lines and, for each line, construct a number composed from the first and the last digit encountered. Then all line numbers need to be summed to a single value. The example taking from the challenge should make everything clear:

> 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. 

Let's start by making a helper that, for a line of text, extracts the number. As a side note, since the challenge doesn't mention input where no number is present in a line, i suppose we don't really need to cover it. I decided to support this though, by having lines without any digits give zero and have no effect on the final sum. 

```python
def get_line_num(line: str) -> int:
    def first_number(line: Iterable[str]) -> int:
        return next((int(ch) for ch in line if ch.isdigit()), 0)

    first_digit = first_number(line)
    last_digit = first_number(reversed(line))

    return int(f"{first_digit}{last_digit}")
```

I've used an inner function to keep things DRY, since the task of finding the first and the last number is pretty much the same. The implementation is a neat little one-liner pattern i like to use once in a while. Using next with a generator expression gives a very compact way to retrieve the first element matching some condition. A key thing to know here is that next throws a `StopIteration` exception by default, but can be given an optional default value to return instead (0). 

The only odd thing here is typing wise. Since there is no char type in python, and reversed gives an iterable of strings, the first_number function takes an `Iterable[str]` so that both cases are covered. While this works, it is not ideal. You could pass a list of strings to the function, which is not intended and not caught statically by the type checker. 

All that is left is to take some input lines, pass them to the `get_line_number` function and sum them. 

```python
def get_number(lines: Iterable[str]):
    return sum(get_line_num(line) for line in lines)
```

## Part 2

For the next part, we need to extend our parsing by also supporting spelled out digits. 

> 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.

To solve this I extend the previous method by adding some bookkeeping to track how much of each pattern was encountered so far in `pattern_seen_chars`. Each number represents the amount of characters currently seen in sequence of a specific pattern. If the full pattern was encountered, the corresponding number is returned. 

```python
patterns = [
    "one",
    "two",
    "three",
    "four",
    "five",
    "six",
    "seven",
    "eight",
    "nine",
]

def get_line_num(line: str) -> int:
    def first_number(line: Iterable[str], patterns: Sequence[str]) -> int:
        pattern_seen_chars = [0] * len(patterns)
        for ch in line:
            if ch.isdigit():
                return int(ch)
            else:
                for i, p in enumerate(patterns):
                    if p[pattern_seen_chars[i]] == ch:
                        if len(p) - 1 == pattern_seen_chars[i]:
                            return i + 1
                        pattern_seen_chars[i] += 1
                    else:
                        pattern_seen_chars[i] = 1 if p[0] == ch else 0

        return 0

    first_digit = first_number(line, patterns)
    last_digit = first_number(reversed(line), [p[::-1] for p in patterns])

    return int(f"{first_digit}{last_digit}")
```

Note that i still use the same method of calling the inner helper to get both the first and the last number. Only this time, the patterns also need to be supplied in reverse (`[p[::-1] for p in patterns]`).

The rest of the solution, namely summing all line numbers, stays the same.

That's it for this challenge. Don't hesitate to give some feedback in case you have done it as well!