Day 3Gear Ratios
View puzzle description on Advent of CodeThis one took a fair bit of thinking, but in the end I settled on what seemed like the most obvious (naive?) solutions.
The solutions to both parts are written in a very imperative style. Usually, I tend to write — or at least think — in an imperative style initially, then go back and refactor to a more functional style once I've got a working solution and a passing test suite to work against. I didn't do that here as I'm not sure of the best way to elegantly refactor these loops — especially the while loops — into a more declarative style. At a high level the code could still be considered functional as all of the functions are pure, but the loops are a bit of a mess and miss out on that declarative magic functional programming is known for.
One thing to note is that I had an off-by-one error in part 2 which I still don't particularly understand. The below line needed tweaking to get the correct answer:
// Part 1
const endColumn = clampColumn(column + numberLength + 1);
// Part 2
const endColumn = clampColumn(column + numberLength);
I'd have expected the + 1
to be required in both cases as the endColumn
variable is only used to define the search area for the special character. The way I use the variable is different between parts (a slice vs a for loop), but I still don't fully understand why the change was needed. I'll have to revisit this at some point as, at time of writing, I don't have any more time to spend on it.
import { clamp, filter, map, nth, pipe, split, sum } from 'rambda';type LocatedNumber = [Row: number, Column: number, Number: number];const isNumber = (x: string): boolean => !isNaN(Number(x));const locateNumbers = (lines: string[]): LocatedNumber[] => {let numbers: LocatedNumber[] = [];for (let row = 0; row < lines.length; row++) {const line = lines[row];for (let column = 0; column < line.length; column++) {let currentChar = line[column];let prevChar = column === 0 ? '.' : line[column - 1];let charIdx = column;let numberString = '';if (isNumber(prevChar)) {continue;}while (isNumber(currentChar) && charIdx < line.length) {numberString += currentChar;charIdx++;currentChar = line[charIdx];}if (numberString.length > 0) {const number = Number(numberString);numbers.push([row, column, number]);}}}return numbers;};const validateNumber = (lines: string[]) => {const rowsCount = lines.length;const columnsCount = lines[0].length;const clampRow = clamp(0, rowsCount - 1);const clampColumn = clamp(0, columnsCount - 1);return ([row, column, number]: LocatedNumber): boolean => {const numberLength = number.toString().length;const startRow = clampRow(row - 1);const endRow = clampRow(row + 1);const startColumn = clampColumn(column - 1);const endColumn = clampColumn(column + numberLength + 1);let currentRow = startRow;let allCharacters = '';while (currentRow <= endRow) {allCharacters += lines[currentRow].slice(startColumn, endColumn);currentRow++;}return /[^0-9\.]/g.test(allCharacters);};};const solution = (input: string): number => {const lines = split('\n')(input);const validator = validateNumber(lines);return pipe(split('\n'),locateNumbers,filter(validator),map(x => x[2]),sum,)(input);};export default solution;