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.