And we’re back! The interview club was graciously down to hack on Advent of Code 2023 Day 3 🙏🏼

Here’s the input for the puzzle:

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

The task is to add up all numbers that are touching a “symbol”, even if it’s diagonally. Below is the raw initial planning and psuedocode we came up with as a group:

// periods aren't symbols
// numbers adjacent to sumbols are part numbers
// symbols: +, #, *, $
// edge cases - symbols could have multiple numbers
 
import fs from 'fs'
 
fs.readFile("testinput.txt", 'utf-8', (err, data) => {
 
	// every time we encounter a symbol we could check in 8 directions
	//if you encounter one, add it to sum -
	// but 
	// other complexity - we need to prevent adding duplicate numbers - what if adjacent to TWO
	// 1. recognizing an entire number that a digit is part of
	// 2. prevent duplicate identification <- use a set? 
	// 3. tediously
	
	// list of index of the first of a group of contiguous numbers
	
	// 3 addresses for a single value
	// the value is an address in a array, or some data structure
	// OR delete all addresses for that value
	// const lines = data.split('\n')
  
	const digitIndexes: Map<number, number> = new Map()
	// index leading to sum of all contiguous integers in area
	
	const values: Map<number, number[]> = new Map()
		
	const matches = Array.from(data.matchAll(/\d+/g))
	const symbols = Array.from(data.matchAll(/[!@#$%^&*()]/g))
	for (const m of matches){
	  console.log(m[0])
	}
	for (const s of symbols){
	  console.log(s.index)
	}
 
	let result = 0
	console.log("test input success", result===4361)
})
 

After another 20 minutes:

// periods aren't sumbols
// numbers adjacent to sumbols are part numbers
// symbols: +, #, *, $
// edge cases - symbols could have multiple numbers
 
import fs from 'fs'
 
fs.readFile("testinput.txt", 'utf-8', (err, data) => {
 
  const lines = data.split('\n')
  const lineLength = lines.length
  const compass = [-lineLength - 1, -lineLength, -lineLength + 1, -1, +1, lineLength -1, lineLength, lineLength+1]
  const symbolIndexes: Set<number> = new Set()
  // index leading to sum of all contiguous integers in area
  // const values: Map<number, number[]> = new Map()// a sum/value that lists out all address for that value
  let result = 0
 
    data = data.replace('\n', '') // clean out the newlines
 
    const matches = Array.from(data.matchAll(/\d+/g))
    const symbols = Array.from(data.matchAll(/[!-+@#$%^&*()]/g))
 
    for (const s of symbols){
      symbolIndexes.add(s.index)
      console.log("symbol index find", data[s.index])
    }
 
    console.log("and those indexes are", [...symbolIndexes])
 
 
    matchLoop: for (const m of matches){
      // for each match, we have a number
      console.log(m.index)
      for(let d = 0; d < m[0].length; d++){
        // console.log(chars[0+d])
        for (let direction of compass){
          const newIndex = m.index + direction + d
          if (newIndex < 0 || newIndex > data.length ) continue
          if (symbolIndexes.has(newIndex)){
            result += parseInt(m[0])
            continue matchLoop
          }
        }
      }
    }
 
  console.log(result)
  console.log("test input success", result===4361)
})

Major Gotchas

  1. newline characters (\n)! These threw all my indexes off, but by 1 after every additional line. So that meant that the first character indexes were correct
  2. The digit loop: this one is more obvious, and it didn’t occur to me and I even have a false memory of writing it but at one point I realized I left it out. matchAll helps me out by finding the starting index of every contiguous group of digits, but it of course only provides the first index. I need to check each digit of it. This isn’t very hard to do, but of course I need to be remembered

Things I Learned

  1. Array.from(someString.matchAll(/[]/g)) is really useful for a case like this
  2. Calculating x,y neighbor coordinates using a single array really isn’t harder than using a double nested loop. Actually, it honestly feels simpler after doing it. Bonus points, it’s a bit more performant.
  3. Named loops - kinda cool! Much simpler to use than I thought, and they respect not only break but also continue
  4. I am a bit liable to lose the play - Krishna and I sort of co-created a cool idea to have these sister data structures where
    1. a) was a list of numbers with the index of each of their digits
    2. b) was a list of indexes of digits that mapped back to the numbers
    3. I thought this was pretty cool, but totally forgot about it and didn’t end up using it.
  5. If we create an array out of the iterators that regex.matchAll returns, like from '123'.matchAll(/\d+/g), it returns an object that sort of appears to have properties of box an object and an array! It has an index property, but it also can be indexed…seemingly. It can be indexed from [0] anyways. But uh, that’s cause it has a 0 property:
    1. image
    2. Not really sure what this is about.

BUT I SPEAK TOO SOON! I still didn’t get it

My solution worked for my test case but not for the full case.

So…Krishna came up with some good edge cases at the start of our collab. One was a symbol having multiple numbers - this isn’t an issue with the current algo, since each digit is potentially checked, and the symbols aren’t removed.

The other was a number having multiple symbols, or being repeated. Unless I misunderstood the instructions, I don’t think this is the case either. However it’s pretty likely I am missing something about the instructions.

Also, Advent said my answer was too low.

image

Welp. It turns out I WASN’T cleaning out the newlines after all somehow.

data = data.replace('\n', '')

Didn’t do it…but I’m not sure why. Because it seemed to have done the trick on the test data. Does anyone have any ideas on why this is?

This also doesn’t work by the way:

data = data.trim()

THIS however, does work:

data = data.replace(/\s+/g, '')

And voilè! That submission worked. Hm. Still can’t explain why the data.replace('\n', '') seemed to work for the test input and didn’t for the final input. Does anyone else know?

I learned that String.replace() will only replace the first occurrence of a value. What I was reaching for was String.replaceAll().

The reason the regex here does work is because it is using the g, or global flag. Without this flag, replace used with a regex expression would do the same thing.

I actually realized one-off JS function is a bit of a weakness of mine, so I wanted to list them out.

These will only return a single item by default:

 
// Strings
indexOf()
lastIndexOf()
search()
replace()
match()
 
// Arrays
find() // first element
findLast() // last element
findIndex() // index of first element
findLastIndex() // index of last element
 

Whereas here are their multi-element counterparts:

matchAll()
replaceAll()
filter() // sort of like find, but returns all matching elements
array.map((x, i) => ...).filter(i => i !== -1) // find can also be built in such a way that it returns indices instead of values

Situations where a multi-element function can be modified to be a single element function:

"hi_hello_hi".split('_', 1)
// This second argument changes the split behavior a lot
// It will split normally, but only up to the limit
 
const e = "egg laying wool milk pig"
 
let f = e.split(" ", 1)
// ["egg"]
 
f = e.split(" ", 2)
// ["egg", "laying"]
 
f = e.split(" ", 3)
// ["egg", "laying", "wool"]
 

image