Advent of Code 2023 - Day 1 Review
It’s that time of the year again. Advent of Code 2023 is here! I’m going to be doing a review of each day’s puzzle and my solution.
Through this year’s AoC, I hope to achieve the following:
- get proficient in Go, Python and Kotlin
- refine my problem solving skills
- and most importantly, have fun!
Day 1: Trebuchet?!
Here’s brief summary of the puzzle:
I will be given calibration document or line separated stream of characters. Here’s how input looks like:
1abc2
pqr3stu8vwx
a1b2c3d4e5f
treb7uchet
Basically my job is to find first and last numeric characters in each line and sum them up. Since part1 and part2 have different calibration logic, I will divide my solution into two parts.
Part 1
Given each line, my job is to find numeric characters (i.e.[0-9]) within the stream and concatennate first and last numeric characters. This seems doable using regex or just good old string manipulation using loops and conditionals, but for this particular example I will try without regex.
func main() {
file, err := os.Open("ex/day1/input.txt")
if err != nil {
panic(err)
}
defer file.Close()
var sum int
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
result, err := calibrate(line)
if err != nil {
continue
}
sum += result
}
fmt.Println(sum)
}
func calibrate(input string) (int, error) {
var first, last int
for i, c := range input {
if unicode.IsDigit(c) {
if first == 0 {
first = i + 1
}
last = i + 1
}
}
if first == 0 {
return 0, fmt.Errorf("no digits found")
}
firstDigit := string(input[first-1])
lastDigit := string(input[last-1])
return strconv.Atoi(firstDigit + lastDigit)
}
Part 1 seems rather straightforward. I read each line from the file and pass it to calibrate function. calibrate function iterates through each character in the string and checks if it’s a digit. If it is, it will store the index of the first and last digit. Please note that I’m adding 1 to the indices because I want to create a case where first == 0 to indicate that no digits were found. Then simply convert rune literal to string and concatenate them, and finally parsing the concatenated string to integer to be added to the sum.
Part 2
Part 2 is a bit more tricky.
Here’s additional instruction for part 2:
Your calculation isn’t quite right. It looks like some of the digits are actually spelled out with letters: one, two, three, four, five, six, seven, eight, and nine also count as valid “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.
Well not so different from part 1, eh? I just need to find a way to convert spelled out numbers to digits. I could use a map to store spelled out numbers and their corresponding digits.
... // same as part 1
var SPELLED_OUT = map[string]string{
"one": "1",
"two": "2",
"three": "3",
"four": "4",
"five": "5",
"six": "6",
"seven": "7",
"eight": "8",
"nine": "9",
}
func calibrate(input string) (int, error) {
var first, last int
for i, c := range input {
if unicode.IsDigit(c) {
if first == 0 {
first = i + 1
}
last = i + 1
}
}
if first == 0 {
return 0, fmt.Errorf("no digits found")
}
firstDigit := string(input[first-1])
lastDigit := string(input[last-1])
return strconv.Atoi(firstDigit + lastDigit)
}
Here’s the example input presented in the puzzle:
That was easy. Right? well, not quite. There’s was some hair-pulling moment for me where I was getting the correct result for the example input, but not with the actual input. And this case is not appearent in the example input. Consider the following input:
eighthree
onetwone
I once thought output for each of the input above should be 88, 12 respectively, but it turns out that the correct output is 83, 11. I would have never figured this out until I read this reddit post.
In short, any number with overlaps should also be calibrated to the list, without consuming the previous match. That said, I needed either to use positive lookahead with regex, or use some sort of sliding window technique.
Note for REGEX solution
Unfortunately, Go uses RE2 regex engine which doesn’t support lookahead/lookbehind due to performance reasons. While theoretically it is possible to emulate the similar behavior either using external package, I simply wrote in JavaScript to solve the problem.
const readline = require("node:readline/promises");
const fs = require("node:fs");
// matchAll requires regex to have global flag
const REGEX = /(?=(one|two|three|four|five|six|seven|eight|nine|\d))/g;
const WORDS = {
one: "1",
two: "2",
three: "3",
four: "4",
five: "5",
six: "6",
seven: "7",
eight: "8",
nine: "9",
};
async function main() {
const fileStream = fs.createReadStream("ex/day1/input.txt");
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity,
});
let result = 0;
for await (const line of rl) {
// note that matchAll returns an iterator which yields matcher
// whose first element is the actual matching pattern and the rest are metadata.
const matches = [...line.matchAll(REGEX)].map(matchParser);
const [first, last] = [matches[0], matches[matches.length - 1]];
if (!first || !last) continue;
result += Number(String(first).concat(String(last)));
}
console.log(result);
}
function matchParser(match) {
const word = match[1];
return WORDS[word] || word;
}
main();