Completed Advent Of Code: Day One

Murtuzaali Surti
Murtuzaali Surti

• 5 min read

Advent of Code 2023 is here and I just completed its day-one challenge. Overall, it was fun to play with strings and numbers in javascript. Without any further ado, let's explore the solution.

The challenge is divided into two parts. The first part is quite simple but the second one adds a cherry on top of the cake.

Part 1 #

From a given list of strings, I had to find the digits, combine the first and last digit in order of occurrence in the string and them sum it up all. To simplify, for a given string gh34s1, the output will be 31 with 3 as the first digit, 1 as the last digit and 31 as their concatenation.

So, for two strings gh34s1 and d7hs23 the sum of their digits will be 31 + 23 = 54.

I stored the input in a .txt file and then looped over line by line to get the string from each line.

import readline from 'readline'
import fs from 'fs'
import path from 'path'

async function readFileByLine() {
    const lines = []
    const readInterface = readline.createInterface({
        input: fs.createReadStream(path.resolve('day-1/input.txt')),
        output: process.stdout,
        terminal: false
    });
    for await (const line of readInterface) {
        lines.push(line)
    }
    return lines
}

(async () => {
    const lines = await readFileByLine()
    // code
})()

Then, to parse the digits in the string and to validate if they are numbers, I used zod. There was a simple way to sort the first and last digits but I couldn't figure it out until I started with Part-2.

const calibrationByLine = lines.map((line, i) => {
    const lineArr = line.split('')
    const firstAndLastNumber = {
        first: null,
        last: null
    }

    for (const [index, char] of lineArr.entries()) {
        if (!z.nan().safeParse(parseInt(char, 10)).success) {
            firstAndLastNumber.first === null ? firstAndLastNumber.first = char : firstAndLastNumber.last = char;
        }
    };

    firstAndLastNumber.last === null && (firstAndLastNumber.last = firstAndLastNumber.first);
    return parseInt(`${firstAndLastNumber.first}${firstAndLastNumber.last}`, 10)
})

const result = calibrationByLine.reduce((acc, curr) => acc + curr, 0)
console.log(result)

This approach gave me the correct answer for sure but there was a better approach. Yes, regex!

Part-2 #

Part-2 had me thinking that my solution was correct, but in fact it wasn't. Lets see what was it. So, the second part introduced a twist in the story, not only the digits were represented as integers in the string, but also their alphabetical synonyms ("one", "two", "three") were also a part of it.

Regex was the answer. I tried /(one|two|three|four|five|six|seven|eight|nine)/g as a regular expression along with matchAll() to find all occurrences of the alphabetical number representations and it seemed to work.

Then, I stored the digits along with their indexes in an array of objects by mapping the alphabetical numbers to their respective integers and sorted them in an ascending order based on the index to get the first and last digits.

const regex = new RegExp('(one|two|three|four|five|six|seven|eight|nine)', 'g')
const stringToIntMap = new Map([["one", 1], ["two", 2], ["three", 3], ["four", 4], ["five", 5], ["six", 6], ["seven", 7], ["eight", 8], ["nine", 9]])

const calibrationByLine = lines.map((line, i) => {
    const allDigitsInLine = []

    for (const match of line.matchAll(regex)) {
        allDigitsInLine.push({
            digit: stringToIntMap.get(match[1]),
            index: match.index
        })
    }

    const lineArr = line.split('')
    const firstAndLastNumber = {
        first: {
            digit: null,
            index: null
        },
        last: {
            digit: null,
            index: null
        }
    }

    for (const [index, char] of lineArr.entries()) {
        if (!z.nan().safeParse(parseInt(char, 10)).success) {
            allDigitsInLine.push({
                digit: char,
                index
            })
        }
    };

    const sortedDigitsAccordingToIndex = allDigitsInLine.sort((a, b) => a.index - b.index)

    firstAndLastNumber.first = {
        ...sortedDigitsAccordingToIndex[0]
    }
    firstAndLastNumber.last = {
        ...sortedDigitsAccordingToIndex[sortedDigitsAccordingToIndex.length - 1]
    }

    firstAndLastNumber.last.digit === null && (firstAndLastNumber.last.digit = firstAndLastNumber.first.digit);
    return parseInt(`${firstAndLastNumber.first.digit}${firstAndLastNumber.last.digit}`, 10)
})

const result = calibrationByLine.reduce((acc, curr) => acc + curr, 0)
console.log(result)

What it would do is, for a string like d3two4eight, the first digit it will select is 3 and the last as 8. So, the output for this string would be 38. So far so good. I submitted the answer and it was incorrect. There was a catch.

The input also contained strings such as 3eightwo. The alphabetical parts of eight and two overlap. And so, my assumption was to ignore the latter digit and just keep eight which would make the output as 38. But the correct output should be 32 after taking into account the overlapping representations.

After a bit of googling I found out about how to find overlapping matches using the lookahead assertion in regular expressions.

Modified the regex to this and I got the perfect answer.

new RegExp('(?=(one|two|three|four|five|six|seven|eight|nine))', 'gm')

And that's it. Through this challenge I learned about regex more than I could have normally. Here's the entire solution:

import readline from 'readline'
import fs from 'fs'
import path from 'path'
import { z } from 'zod'

const regex = new RegExp('(?=(one|two|three|four|five|six|seven|eight|nine))', 'gm')
const stringToIntMap = new Map([["one", 1], ["two", 2], ["three", 3], ["four", 4], ["five", 5], ["six", 6], ["seven", 7], ["eight", 8], ["nine", 9]])

async function readFileByLine() {
    const lines = []

    const readInterface = readline.createInterface({
        input: fs.createReadStream(path.resolve('day-1/input.txt')),
        output: process.stdout,
        terminal: false
    });
    for await (const line of readInterface) {
        lines.push(line)
    }

    return lines
}

(async () => {
    // reading file line by line and storing it in an array - each line contains a string
    const lines = await readFileByLine()

    // looping over each string
    const calibrationByLine = lines.map((line, i) => {
        const allDigitsInLine = []

        // finding alphabetical digits, mapping them to their integers, and storing them along with index
        for (const match of line.matchAll(regex)) {
            allDigitsInLine.push({
                digit: stringToIntMap.get(match[1]),
                index: match.index
            })
        }

        const lineArr = line.split('')
        const firstAndLastNumber = {
            first: {
                digit: null,
                index: null
            },
            last: {
                digit: null,
                index: null
            }
        }

        // finding integers and storing them along with index
        for (const [index, char] of lineArr.entries()) {
            if (!z.nan().safeParse(parseInt(char, 10)).success) {
                allDigitsInLine.push({
                    digit: char,
                    index
                })
            }
        };

        // sorting integers based on the index - ascending
        const sortedDigitsAccordingToIndex = allDigitsInLine.sort((a, b) => a.index - b.index)

        firstAndLastNumber.first = {
            ...sortedDigitsAccordingToIndex[0]
        }
        firstAndLastNumber.last = {
            ...sortedDigitsAccordingToIndex[sortedDigitsAccordingToIndex.length - 1]
        }

        firstAndLastNumber.last.digit === null && (firstAndLastNumber.last.digit = firstAndLastNumber.first.digit);

        // concatenating digits and converting into integer
        return parseInt(`${firstAndLastNumber.first.digit}${firstAndLastNumber.last.digit}`, 10)
    })

    // summing up the result of every string
    const result = calibrationByLine.reduce((acc, curr) => acc + curr, 0)
    console.log(result)
})()

The execution time for 1000 strings turns out to be:

executed: 94.047 ms

App Defaults 2023 — What I use

Previous

Advent Of Code 2023 - Day Two Solution

Next