Advent of Code 2023: Day 3, Part 2

Picture of Advent of Code Day 3 input text

I am doing Advent of Code 2023 in JavaScript/Node.js after using only Typescript for the past year just to remind myself of the differences. JSDoc has been really nice for bridging some of the gaps in my workflow that I had gotten used to with Typescript.

You can see all of my solutions at github.com/MildlyAggressiveFolk/AoC

My Solution

Part 2 of Day 3's challenge was to find all of the asterisks which had exactly 2 neighboring sets of numbers in any direction.

I began by finding all of the coordinates of the sets of numbers and the asterisks, then I looped through the asterisks returned coordinates of their neighbors. I cross referenced with the coordinates of the numbers, and if the neighbor was a number, I added its coordinates to a set (set chosen to avoid duplicates).

If the set had 2 entries, I knew that I had found one of the "gear ratios" so I grabbed the values, multiplied them together, and added them to an array, which I finally summed, giving me my answer.

const fs = require("fs")

const input = fs.readFileSync("./input.txt", "utf8")
const inputArr = input.split("\n")

const coords = getCoords(inputArr)
console.log(findAndSumGearRatios(...coords))

/**
 * Takes array of gear coordinates and returns sum of gear ratios
 * @param {[number, number][]} gearCoords
 * @returns {number}
 */
function findAndSumGearRatios(numCoords, gearCoords) {
  const gearRatios = []

  for (let gearCoord of gearCoords) {
    const neighborSet = new Set()
    const [row, col] = gearCoord
    const neighbors = getNeighbors(row, col)

    for (let neighbor of neighbors) {
      const numEntry = getNumEntry(...neighbor, numCoords)

      if (numEntry) {
        neighborSet.add(numEntry)
      }
    }
    if (neighborSet.size === 2) {
      const vals = []

      neighborSet.forEach((numCoord) => {
        vals.push(getValueFromNumCoord(numCoord))
      })
      gearRatios.push(vals.reduce((a, b) => a * b))
    }
  }

  return gearRatios.reduce((a, b) => a + b)
}

function getValueFromNumCoord(numCoord) {
  const [row, colArr] = numCoord
  const val = []

  for (let col of colArr) {
    val.push(inputArr[row][col])
  }

  return Number(val.join(""))
}

/**
 * Takes row and column and returns number entry if it exists
 * @param {number} row
 * @param {number} col
 * @returns {[number, number[]] | false} numCoord | false
 */

function getNumEntry(row, col, numCoords) {
  const numCoord = numCoords.find((entry) => entry[0] === row && entry[1].includes(col))

  if (numCoord) return numCoord
  return false
}

/**
 * Returns all neighbors of a given coordinate
 * @param {number} row
 * @param {number} col
 * @returns {[number, number][]}
 */
function getNeighbors(row, col) {
  const neighbors = []
  for (let i = -1; i <= 1; i++) {
    for (let j = -1; j <= 1; j++) {
      if (i !== 0 || j !== 0) neighbors.push([row + i, col + j])
    }
  }
  return neighbors
}

/**
 * Returns coordinates of all numbers and gears
 * @param {string[]} lines
 * @returns {[[number, number[]][], [number, number][]]} [numCoords, gearCoords]
 */
function getCoords(lines) {
  const numCoords = []
  const gearCoords = []
  for (let [index, line] of lines.entries()) {
    for (let i = 0; i < line.length; i++) {
      if (isDigit(line[i])) {
        const number = [i]
        for (let j = i + 1; j < line.length; j++) {
          if (isDigit(line[j])) {
            number.push(j)
            if (j === line.length - 1) {
              i = j
            }
          } else {
            i = j
            break
          }
        }
        numCoords.push([index, number])
      }
      if (line[i].charCodeAt(0) === 42) {
        gearCoords.push([index, i])
      }
    }
  }
  return [numCoords, gearCoords]
}

/**
 *
 * @param {string} char
 * @returns boolean
 */
function isDigit(char) {
  return char.charCodeAt(0) >= 48 && char.charCodeAt(0) <= 57
}