december 14, 2023

{ advent of code 2023 in javascript: day 4, part 1 }


tags: javascript, algorithms, advent of code

Today started out strong and then fell apart. For Day 4 of Advent of Code in JavaScript, I have tests and a solution for Part 1, but I am still working on Part 2.

I am posting Part 1 for now. If I am able to solve it, Part 2 might come tomorrow otherwise this might be it for my Advent of Code.

Day 4 Challenge: Scratchcards

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

I am affectionately calling this one “elf with a gambling problem.” I knew there would be a lot of scratchcards but I had a good laugh when I opened the input file and thought of an elf buying 218 scratchcards.

Anyway, today we are helping an elf tally up his lotto winnings so that we can borrow his boat. Here is a sample.

Card 1: 41 48 83 86 17 | 83 86  6 31 17  9 48 53
Card 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19
Card 3:  1 21 53 59 44 | 69 82 63 72 16 21 14  1
Card 4: 41 92 73 84 69 | 59 84 76 51 58  5 54 83
Card 5: 87 83 26 28 32 | 88 30 70 12 93 22 82 36
Card 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11

Each card has a set of winning numbers and a set of our numbers. We determine how many matches we have to the winning numbers, score the card, and return the sum of all scores. The answer to our sample is 13.

Tests for Part 1 in Jest

Today was relatively straightforward with breaking up the code and testing. I planned out these tests. I was a little worried that not writing out all the test cases would hurt me, but this worked fine for part 1.

describe("sumScratcherScores", function() {
    test("returs the correct sum of scratcher scores", async function() {
        expect(await sumScratcherScores(test_file)).toEqual(13);
    })
})

describe("parseScratcher", function() {
    test("returns the correct arrays from a line of scratcher info", function() {
        const line_1 = "Card 1: 41 48 83 86 17 | 83 86  6 31 17  9 48 53";
        const line_2 = "Card 3:  1 21 53 59 44 | 69 82 63 72 16 21 14  1";
        const line_3 = "Card 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11";

        expect(parseScratcher(line_1))
          .toEqual([[41, 48, 83, 86, 17], [83, 86, 6, 31, 17, 9, 48, 53]])
        expect(parseScratcher(line_2))
          .toEqual([[1, 21, 53, 59, 44], [69, 82, 63, 72, 16, 21, 14, 1]])
        expect(parseScratcher(line_3))
          .toEqual([[31, 18, 13, 56, 72], [74, 77, 10, 23, 35, 67, 36, 11]])
    })
})

describe("scoreScratcher", function() {
    test("returns correct scratcher score", function() {
        const scratcher_1 = [[41, 48, 83, 86, 17], [83, 86, 6, 31, 17, 9, 48, 53]];
        const scratcher_2 = [[1, 21, 53, 59, 44], [69, 82, 63, 72, 16, 21, 14, 1]];
        const scratcher_3 = [[31, 18, 13, 56, 72], [74, 77, 10, 23, 35, 67, 36, 11]];

        expect(scoreScratcher(scratcher_1[0], scratcher_1[1])).toEqual(8);
        expect(scoreScratcher(scratcher_2[0], scratcher_2[1])).toEqual(2);
        expect(scoreScratcher(scratcher_3[0], scratcher_3[1])).toEqual(0);
    })
})

Solution for Part 1

/** sumScratcherScores: given a path to a file of scratcher card info, parse the
file and return the sum of scores of every card. 
*/
async function sumScratcherScores(filepath) {
    const file = await fs.readFile(filepath, "utf-8");
    const lines = getLinesFromString(file);

    let sum = 0;

    for (let line of lines) {
        const [ winningNums, myNums ] = parseScratcher(line);
        sum += scoreScratcher(winningNums, myNums);
    }

    return sum;
}

/** parseScratcher: given a line of scratcher info, parse into two arrays: one
 * of winning nums and one of nums on the scratcher.
 * ex.  Card 1: 41 48 83 | 83 86  6 31 17  9 -> [41, 48, 83], [83, 6, 31, 14, 9]    
 */
function parseScratcher(scratcher) {
    // convert string to two arrays
    const scratcherNums = scratcher.slice(scratcher.indexOf(":") + 1);
    const winningAndMyNums = scratcherNums.split(" | ");
    let winningNums = winningAndMyNums[0].split(" ");
    let myNums = winningAndMyNums[1].split(" ");

    // parse arrays to remove empty elements
    winningNums = winningNums.filter(num => num).map(num => Number(num));
    myNums = myNums.filter(num => num).map(num => Number(num));

    return [winningNums, myNums];
}

/** scoreScratcher: given two arrays of nums, determines how many numbers in the
 * first array are present in the second array. Returns a score of 1 for the
 * first match and doubles that for every additional match.
 * ex. [41, 48, 83], [83, 6, 41, 14, 9] --> 2
 */
function scoreScratcher(winningNums, myNums) {
    let matchingNumCount = 0;

    for (let num of winningNums) {
        if (myNums.includes(num)) {
            matchingNumCount++;
        }
    }

    if (matchingNumCount) {
        return 2 ** (matchingNumCount - 1)
    } else {
        return 0;
    }   
}

This was pleasant after yesterday. The only bug I had was that I initially wrote a for...in loop instead of a for...of loop. It’s been a hectic day and a console.log solved that for me in a minute.

Part 2 Tests and Solution

Maybe?

I’ve tried a few strategies but they became more and more unhinged. It seems to be time to call it, at least for the evening.

UPDATE: I did come back the next morning with a solution for Day 4, Part 2. It’s amazing what a night of sleep can do.

Today’s Takeaways

It has become evident how important naming is over the past few days. Even beyond naming, I’ve realized the benefit of having an idea of what something is.

For instance, so many algorithm problems start out, “You are given an array of numbers,” or “if you have a string of random characters.”

But what are those things?

I have always gone into coding challenges with the mindset that I am working with abstract data. Even when I can be more specific, something in my mind just goes, “Solving algorithm? Better call the variable string.”

That’s how I started off AoC with variables like string, nums, etc. And then I proceeded to get extremely confused until I went back and fixed them.

Advent of Code sets us up to have actual things to work with. The problem statement even bolds them to show what they are. Like, yes, I am still working with a string of random numbers, but this represents a scratcher with winningNums and myNums. I’m parsing a scratcher or calculating a score.

When I switched my thinking to use these terms from the beginning — my pseudo-code has them today — it became much easier to follow my own process.

So now I’m wondering if I can apply this idea when I have those questions that begin, “You are given a list of numbers…” I might make myself a list of things that are good representation of common algorithms so that I can pull them out when I get more abstract questions.

Besides, if an interviewer tells me, “You have an array of arrays…” and I say, “I’m going to use the mental model of a box of doughnuts to represent this 2D array,” you know I am going to be a good coworker.