december 14, 2023

{ advent of code 2023 in javascript: day 3 }


tags: advent of code, algorithms, javascript

Day 3 was a tough one for me. My Advent of Code JavaScript solution today is a brute force approach, but it does get the job done.

Day 3 Challenge: Gear Ratios

You can find the day’s challenge, examples, and sample code input here.

On our way to visit the water source, we have to take a gondola. But much like the gondolas at my local ski resort, they are not working. In this case, it is because they are missing a part.

We have to parse through a spec sheet that looks like this:

467..114..
...*......
..35..633.
......#...
617*......
.....+.58.
..592.....
......755.
...$.*....
.664.598..

Any number adjacent to a symbol in any direction, including diagonally, is a part number. We have to find all these part numbers and add them together.

In the above example, the answer is 4361.

Tests for Part 1 in Jest

My brain could not get past a 2D array solution. Then I couldn’t be bothered to do the extra work to break it down into multiple functions so I wasn’t able to do great testing.

My test file looks like this:

describe("sumPartNumbers", function() {
    test("returns the correct sum of part numbers", async function() {
        expect(await sumPartNumbers(test_file)).toEqual(4361);
        expect(await sumPartNumbers(test_2)).toEqual(413);
        expect(await sumPartNumbers(test_3)).toEqual(925);
        expect(await sumPartNumbers(test_4)).toEqual(22);
    })
})

test_file is the sample data provided with today’s problem. The other test files are what I was using when my first test passed but my answer was still wrong. Those test cases are:

12.......*..
+.........34
.......-12..
..78........
..*....60...
78..........
.......23...
....90*12...
............
2.2......12.
.*.........*
1.1.......56

12.......*..
+.........34
.......-12..
..78........
..*....60...
78.........9
.5.....23..$
8...90*12...
............
2.2......12.
.*.........*
1.1..503+.56

97..
...*
100.

Solution for Part 1

My solution is a brute force approach. I literally wrote out every different case. The resulting function is embarrassingly long. As I was debugging, I thought of several better strategies and this is one that I would like to refactor into something nicer.

I’ve got a little about my live coding process here beneath the solution if you are in need of a laugh after your own day of solving.

/** sumPartNumbers: given a file path of part data, parse file, deterimine which
numbers are parts, and return the sum of all parts.
 */
async function sumPartNumbers(filepath) {
    const file = await fs.readFile(filepath, "utf-8");
    const lines = getLinesFromString(file);

    let sum = 0;

    for (let i = 0; i < lines.length; i++) {
        console.log("sum", sum, "line", lines[i]);
        for (let j = 0; j < lines[i].length; j++) {
            let currLine = lines[i];

            if (currLine[j] !== "." && isNaN(Number(currLine[j]))) {
                console.log("curr char", currLine[j]);
                let left = "";
                let middle = "";
                let right = "";

                // check top left
                if (i !== 0 && !(isNaN(Number(lines[i - 1][j - 1])))) {
                    // find start of number
                    let idx = j - 1;
                    while (!(isNaN(Number(lines[i - 1][idx])))) {
                        idx--
                    }

                    while (!(isNaN(Number(lines[i - 1][idx + 1])))) {
                        left += lines[i - 1][idx + 1];
                        idx++
                    }
                    console.log("top left", left);
                }

                // check top middle
                if (i !== 0 && !(isNaN(Number(lines[i - 1][j]))) && !left) {

                    let idx = j;
                    while (!(isNaN(Number(lines[i - 1][idx])))) {
                        middle += lines[i - 1][idx];
                        idx++;
                    }
                    console.log("top middle", middle);
                }

                // check top right
                if (
                    i !== 0
                    && !(isNaN(Number(lines[i - 1][j + 1])))
                    && !middle
                    && isNaN(Number(lines[i - 1][j]))
                ) {

                    let idx = j + 1;
                    while (!(isNaN(Number(lines[i - 1][idx])))) {
                        right += lines[i - 1][idx];
                        idx++;
                    }
                    console.log("top right", right);

                }

                sum = sum + Number(left) + Number(middle) + Number(right);
                left = "";
                middle = "";
                right = "";

                // check left
                if (!(isNaN(Number(lines[i][j - 1])))) {

                    // find start of number
                    let idx = j - 1;
                    while (!(isNaN(Number(lines[i][idx])))) {
                        idx--
                    }

                    while (!(isNaN(Number(lines[i][idx + 1])))) {
                        left += lines[i][idx + 1];
                        idx++
                    }
                    console.log("left", left);
                }

                // check right
                if (!(isNaN(Number(lines[i][j + 1])))) {

                    let idx = j + 1;
                    while (!(isNaN(Number(lines[i][idx])))) {
                        right += lines[i][idx];
                        idx++;
                    }
                    console.log("right", right);
                }

                sum = sum + Number(left) + Number(right);
                left = "";
                right = "";

                // check bottom left
                if (i !== lines.length - 1 && !(isNaN(Number(lines[i + 1][j - 1])))) {

                    // find start of number
                    let idx = j - 1;
                    while (!(isNaN(Number(lines[i + 1][idx])))) {
                        idx--
                    }

                    while (!(isNaN(Number(lines[i + 1][idx + 1])))) {
                        left += lines[i + 1][idx + 1];
                        idx++
                    }
                    console.log("bottom left", left);
                }

                // check bottom middle
                if (i !== lines.length - 1 && !(isNaN(Number(lines[i + 1][j])))) {

                    let idx = j;
                    while (!(isNaN(Number(lines[i + 1][idx]))) && !left) {
                        middle += lines[i + 1][idx];
                        idx++;
                    }
                    console.log("bottom middle", middle);
                }

                // check bottom right
                if (
                    i !== lines.length - 1
                    && !(isNaN(Number(lines[i + 1][j + 1])))
                    && !middle
                    && isNaN(Number(lines[i + 1][j]))
                ) {
                    let idx = j + 1;
                    while (!(isNaN(Number(lines[i + 1][idx])))) {
                        right += lines[i + 1][idx];
                        idx++;
                    }
                    console.log("bottom right", right);
                }

                sum = sum + Number(left) + Number(middle) + Number(right);
            }
        }
    }

    return sum;
}

Obviously writing a brute force approach is frustrating. But after fine tuning all of these different conditionals, I was so close. Everything was iterating as I wanted to and adding correctly…except for this situation:

...*......
..35..633.

The 35 was added as expected, but then on the middle bottom check (that’s the technical term for it), it was adding 5 again. So what to do about this?

I thought about turning my top and bottom checks into if...else if statements. That partially works unless we have this combo:

...*....
846.398.

In this case, both the bottom left and bottom right are valid.

I could add a variable that tracks what that I already have a left or middle variable and then act accordingly. But what might be clearer is to have 3 separate variables to track the number that I am adding to the string. I can use these for the conditionals and simply add all 3 to the sum.

After some refactoring, all of my tests were passing. All of the tests I could find on the AdventOfCode subreddit were passing. The thing that wasn’t passing? The actual answer. My submission was too high.

Finally I just went line by line in the input file with some console logs and a calculator.

Here is the test that finally revealed my mistake (that 664 specifically):

...$..*...
..664.598.

:facepalm

I literally wrote out above how this could be a problem. I added code that accounted for a number to the bottom left and bottom right, but reasoned to myself that if there was already a bottom left number, a bottom left number would have to a different number.

I cleaned up my conditionals on those cases and there we go. Success (and it only took 2 hours)!

Part 2 Tests and Solution

In my earlier perusal of Reddit, I heard that part 2 was relatively simple tonight. Now we are looking specifically for the “*” symbol surrounded by exactly 2 numbers. This represents a gear and the product of the two numbers is our gear ratio. We add all those ratios together and the sum is our answer.

This was pretty straightforward. Because I had put everything into one unwieldy function, I had to pretty much just make the same function again. The changes I made were to look for only the “*” instead of any symbol and to collect my numbers around each gear into an array. If the array length equaled 2, I added their product to my running total.

The test case I added:

describe("sumGearRatios", function() {
    test("returs the correct sum of gear ratios", async function() {
        expect(await sumGearRatios(test_file)).toEqual(467835);
    })
})

The rewrite of my function:

/** sumGearRatios: given a file path of part data, parse file, deterimine which
symbols are gears (exactly two numbers attached to a *), and return the sum of
all gear ratios.
*/
async function sumGearRatios(filepath) {
    const file = await fs.readFile(filepath, "utf-8");
    const lines = getLinesFromString(file);

    let sum = 0;

    for (let i = 0; i < lines.length; i++) {
        for (let j = 0; j < lines[i].length; j++) {
            let currLine = lines[i];

            if (currLine[j] === "*") {
                console.log("curr char", currLine[j]);
                let nums = [];
                let left = "";
                let middle = "";
                let right = "";

                // check top left
                if (i !== 0 && !(isNaN(Number(lines[i - 1][j - 1])))) {
                    // find start of number
                    let idx = j - 1;
                    while (!(isNaN(Number(lines[i - 1][idx])))) {
                        idx--
                    }

                    while (!(isNaN(Number(lines[i - 1][idx + 1])))) {
                        left += lines[i - 1][idx + 1];
                        idx++
                    }
                    console.log("top left", nums);
                }

                // check top middle
                if (i !== 0 && !(isNaN(Number(lines[i - 1][j]))) && !left) {

                    let idx = j;
                    while (!(isNaN(Number(lines[i - 1][idx])))) {
                        middle += lines[i - 1][idx];
                        idx++;
                    }
                    console.log("top middle", middle);
                }

                // check top right
                if (
                    i !== 0
                    && !(isNaN(Number(lines[i - 1][j + 1])))
                    && !middle
                    && isNaN(Number(lines[i - 1][j]))
                ) {

                    let idx = j + 1;
                    while (!(isNaN(Number(lines[i - 1][idx])))) {
                        right += lines[i - 1][idx];
                        idx++;
                    }
                    console.log("top right", right);

                }

                if (left) nums.push(left);
                if (middle) nums.push(middle);
                if (right) nums.push(right);

                left = "";
                middle = "";
                right = "";

                // check left
                if (!(isNaN(Number(lines[i][j - 1])))) {

                    // find start of number
                    let idx = j - 1;
                    while (!(isNaN(Number(lines[i][idx])))) {
                        idx--
                    }

                    while (!(isNaN(Number(lines[i][idx + 1])))) {
                        left += lines[i][idx + 1];
                        idx++
                    }
                    console.log("left", left);
                }

                // check right
                if (!(isNaN(Number(lines[i][j + 1])))) {

                    let idx = j + 1;
                    while (!(isNaN(Number(lines[i][idx])))) {
                        right += lines[i][idx];
                        idx++;
                    }
                    console.log("right", right);
                }

                if (left) nums.push(left);
                if (right) nums.push(right);

                left = "";
                right = "";

                // check bottom left
                if (i !== lines.length - 1 && !(isNaN(Number(lines[i + 1][j - 1])))) {

                    // find start of number
                    let idx = j - 1;
                    while (!(isNaN(Number(lines[i + 1][idx])))) {
                        idx--
                    }

                    while (!(isNaN(Number(lines[i + 1][idx + 1])))) {
                        left += lines[i + 1][idx + 1];
                        idx++
                    }
                    console.log("bottom left", left);
                }

                // check bottom middle
                if (i !== lines.length - 1 && !(isNaN(Number(lines[i + 1][j])))) {

                    let idx = j;
                    while (!(isNaN(Number(lines[i + 1][idx]))) && !left) {
                        middle += lines[i + 1][idx];
                        idx++;
                    }
                    console.log("bottom middle", middle);
                }

                // check bottom right
                if (
                    i !== lines.length - 1
                    && !(isNaN(Number(lines[i + 1][j + 1])))
                    && !middle
                    && isNaN(Number(lines[i + 1][j]))
                ) {
                    let idx = j + 1;
                    while (!(isNaN(Number(lines[i + 1][idx])))) {
                        right += lines[i + 1][idx];
                        idx++;
                    }
                    console.log("bottom right", right);
                }

                if (left) nums.push(left);
                if (middle) nums.push(middle);
                if (right) nums.push(right);

                //check valid gear and add ratio
                if (nums.length === 2) {
                    sum += (nums[0] * nums[1]);
                }
            }
        }
    }
    return sum;
}

Today’s Takeaways

It is always, always worth trying to break functions down. Yes, I would have had to pass a few extra things in given my 2D array approach and I don’t know that my particular bug would have been that much more obvious, but navigating my code would have been so much more pleasant.

Today also brought me into contact with a lot of other people’s code. Like always, Reddit is both great and disheartening. I saw some very neat solutions, I saw solutions that took only a few lines of code, I saw test cases that I hadn’t begun to think of.

I’ve seen plenty of junior developers lamenting this very thing. The feeling of success after figuring out a problem that took hours quickly replaced by the feeling of defeat of finding out that there is a much smarter way.

But hey, I figured it out regardless, I had mostly a good time, and no one is actually paying me to help incompetent elves fix global warming. I did learn something, I can always go review the answers others posted, and my imposter syndrome can take it’s own holiday.