Day 3Gear Ratios

View puzzle description on Advent of Code

This 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;