LD
Change your colour scheme

Advent of Code 2023: Day One

Published:

Advent of Code is here, and once again I’m going to attempt it. Last year I got to about Day 11 before giving up. This year, I’ll try and beat that (but no promises). You can follow my progress with the Advent of Code tag, or by subscribing to the RSS feed, and the code is on my Git.

As expected, this post (and the subsequent ones) will contain lots of spoilers for Advent of Code. Read at your own peril.

Language

Originally, I was planning to do it in PICO-8. But that sounded too much like hard work, so I’m doing it in Typescript.

Part One

The first part was relatively easy, given a bunch of strings that may-or-may not contain numbers, pick the first and last number, smoosh them together, and then sum them all. An example string might be gasdad15asd5, which should produce 15.

Easy-peasy. I just use a really simple Regex, and then pick the results out:

function parseLine(line: string): number {
const pattern = /\d/g;
const matches = line.match(pattern);

if (!matches.length) return 0;

const valueStr = `${matches[0]}${matches[matches.length - 1]}`;
return parseInt(valueStr);
}

function calculate() {
const lines = fs.readFileSync('./path/to/input.txt').toString('utf-8').split('\n');
console.log(lines.reduce((total, line) => total + parseLine(line), 0));
}

That gave me the correct answer, and I was able to move onto Part 2.

Part Two

This was a doozy. Now, we also need to parse the written forms of the numbers, e.g. one, two, three. I extended my Regex to capture these too, and then added a parser function that would convert the written number to the digit:

function parseDigit(digit: string): string {
switch (digit) {
case "one": return "1";
case "two": return "2";
case "three": return "3";
case "four": return "4";
case "five": return "5";
case "six": return "6";
case "seven": return "7";
case "eight": return "8";
case "nine": return "9";
default: return digit;
}
}

function parseLine(line: string): number {
const pattern = /\d|one|two|three|four|five|six|seven|eight|nine/g;
const matches = line.match(pattern);

if (!matches.length) return 0;

const converted = matches.map(parseDigit);
const valueStr = `${converted[0]}${converted[converted.length - 1]}`;
return parseInt(valueStr);
}

Everything looked good, the tests passed but… nope. Wrong result.

Regex fun

It turned out that the test cases had no examples for when the digits overlap. For example, oneight should be 18, but I was getting 11. That meant that I was always getting the wrong result. This is because standard global pattern matching consumes the string - so once I’ve parsed one, the remaining string is ight, which gets discarded.

To get around this, I had to add a look-ahead with capture group to my Regex, and then use string.matchAll(), rather than string.match() - because string.match() ignores capture groups:

function parseLine(line: string): number {
const pattern = /(?=(\d|one|two|three|four|five|six|seven|eight|nine))/g;
const matches = line.matchAll(pattern);

if (!matches) return 0;

const converted = [...matches].flatap(match => match.map(parseDigit)); // Matches is an iterator of RegExpMatchArrays, this converts it to String[]
const valueStr = `${converted[0]}${converted[converted.length - 1]}`;
return parseInt(valueStr);
}

And finally, that worked. This made it sound easier than it was, in reality I spent a good 30-40 minutes scratching my head, and even switched languages (originally I was using Rust, like last year). But anyway, it’s done, and at least I can move onto Day Two.

Tags:

About the author

My face

I'm Lewis Dale, a software engineer and web developer based in the UK. I write about writing software, silly projects, and cycling. A lot of cycling. Too much, maybe.

Responses