Skip to content
Merged
117 changes: 117 additions & 0 deletions 2023/day-01/checksum.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* Generates a checksum for a string by concatenating
* the first and last digits found in the string
* @param {string} data of a single line
*/
const checksumLine = (data) => {
const parsed = data.replaceAll(/([^0-9])/g, '') // trim non-numeric characters
let result = ''
if (parsed.length === 1) { // some strings only have a single digit
result = `${parsed}${parsed}`
} else {
result = `${parsed[0]}${parsed[parsed.length - 1]}`
}
return parseInt(result)
}

const lazyNums = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine']
const lazyReg = new RegExp(lazyNums.join('|'))
const lazyNumsReversed = lazyNums.map((num) => num.split('').reverse().join(''))
const lazyReversReg = new RegExp(lazyNumsReversed.join('|'))

const lookupPosition = (match) => {
const idx = lazyNums.indexOf(match)
if (idx > 9) {
return idx - 9
}
return idx
}

const lookupPositionReversed = (match) => {
const reverseMatch = match.split('').reverse().join('')
const idx = lazyNums.indexOf(reverseMatch)
if (idx > 9) {
return idx - 9
}
return idx
}

const lazyChecksumLine = (data) => {
let first = ''
data.replace(lazyReg, (match) => {
first = lookupPosition(match)
return match // reinsert so we don't bork the data string
})
// find last matching digit by reversing the string and searching backwards
let last = ''
data = data.split('').reverse().join('')
data.replace(lazyReversReg, (match) => {
last = lookupPositionReversed(match)
return match // reinsert so we don't bork the data string
})

return parseInt(`${first}${last}`)
}

/**
* Generates the checksum for an entire set
* @param {array} set of lines containing data
*/
const checksumSet = (set, sanitize = false, lazy = false) => {
let filter = (data) => data
if (sanitize) {
filter = sanitizeLine
}

let checksum = checksumLine
if (lazy) {
checksum = lazyChecksumLine
}

return set.reduce((total, current) => {
return total + checksum(
filter(current)
)
}, 0)
}

const numbers = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine']
const numbersReversed = numbers.map((num) => num.split('').reverse().join(''))
const reg = new RegExp(numbers.join('|'))
const regGlobal = new RegExp(numbers.join('|'), 'g')
const regReversed = new RegExp(numbersReversed.join('|'))

// Sanitizes using a single-pass regex replace all
// Produces 53885 which is incorrect for part 2
const byRegex = (data) => {
return data.replaceAll(regGlobal, (matched) => numbers.indexOf(matched) + 1)
}

// Sanitizes by replacing just the first and last text digits, in case shared letters is supposed to work
// Produces 53853 which is too low for part 2
const byFirstLast = (data) => {
// sanitize first matching digit
data = data.replace(reg, (matched) => numbers.indexOf(matched) + 1)
// sanitize last matching digit by reversing the string and searching backwards
data = data.split('').reverse().join('')
data = data.replace(regReversed, (matched) => numbersReversed.indexOf(matched) + 1)

// return original order
return data.split('').reverse().join('')
}

/**
* Sanitzizes a line by replacing spelled-out numbers with data
* @param {string} data line of input to sanitize
*/
const sanitizeLine = (data, method = 'byFirstLast') => {
const methods = {
none: (data) => data,
byFirstLast,
byRegex
}

return methods[method](data)
}

module.exports = { checksumLine, checksumSet, sanitizeLine, lazyChecksumLine }
122 changes: 122 additions & 0 deletions 2023/day-01/checksum.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/* eslint-env mocha */
const { expect } = require('chai')
const { checksumSet, checksumLine, sanitizeLine, lazyChecksumLine } = require('./checksum')
const fs = require('fs')
const path = require('path')
const filePath = path.join(__dirname, 'input.txt')
const { inputToArray } = require('../../2018/inputParser')

describe('--- Day 1: Trebuchet?! ---', () => {
describe('Part 1', () => {
describe('checksum', () => {
it('calculates the checksum for a string by concatentating the first and last number', () => {
// provided
expect(checksumLine('1abc2')).to.equal(12)
expect(checksumLine('pqr3stu8vwx')).to.equal(38)
expect(checksumLine('a1b2c3d4e5f')).to.equal(15)
})
it('handles the edge case of a line with only a single digit', () => {
// provided
expect(checksumLine('treb7uchet')).to.equal(77)
})
})
describe('checksumSet', () => {
it('calculates the checksum for a set of lines by summing the checksum of each line', () => {
// provided
const set = ['1abc2', 'pqr3stu8vwx', 'a1b2c3d4e5f', 'treb7uchet']
expect(checksumSet(set)).to.equal(142)
})
})
})
describe('Part 2', () => {
describe('sanitizeLine', () => {
const data = [
'two1nine',
'eightwothree',
'abcone2threexyz',
'xtwone3four',
'4nineeightseven2',
'zoneight234',
'7pqrstsixteen'
]
const result = [29, 83, 13, 24, 42, 14, 76]
it('cleans up a string when digits are spelled out', () => {
const set = JSON.parse(JSON.stringify(data))
for (let x = 0; x < set.length; x++) {
expect(checksumLine(sanitizeLine(set[x]))).to.equal(result[x])
// expect(checksumLine(sanitizeLine(set[x], 'sanitizeByRegex'))).to.equal(result[x])
// expect(checksumLine(sanitizeLine(set[x], 'sanitizeFirstLast'))).to.equal(result[x])
}
})
it('allows for skipping sanitation', () => {
const set = JSON.parse(JSON.stringify(data))
for (let x = 0; x < set.length; x++) {
expect(sanitizeLine(set[x], 'none')).to.equal(data[x])
}
})
})
describe('checksumSet', () => {
const data = [
'two1nine',
'eightwothree',
'abcone2threexyz',
'xtwone3four',
'4nineeightseven2',
'zoneight234',
'7pqrstsixteen'
]
it('can sanitize', () => {
expect(checksumSet(data, true)).to.equal(281)
})
})
describe('lazyChecksumLine', () => {
const data = [
'two1nine',
'eightwothree',
'abcone2threexyz',
'xtwone3four',
'4nineeightseven2',
'zoneight234',
'7pqrstsixteen'
]
const result = [29, 83, 13, 24, 42, 14, 76]
it('can match text or numeric for checksum calcs', () => {
const set = JSON.parse(JSON.stringify(data))
for (let x = 0; x < set.length; x++) {
expect(lazyChecksumLine(set[x])).to.equal(result[x])
}
})
})

describe.skip('integeration', () => {
let initData
before((done) => {
fs.readFile(filePath, { encoding: 'utf8' }, (err, rawData) => {
if (err) throw err
initData = inputToArray(rawData.trim())
// Deep copy to ensure we aren't mutating the original data
// data = JSON.parse(JSON.stringify(initData))
done()
})
})

it('is not done without sanitation, since that matches part 1', () => {
const data = JSON.parse(JSON.stringify(initData))
const result = checksumSet(data, false, true)
expect(result).to.not.equal(54953)
})

it('is not done with a one-time regex', () => {
const data = JSON.parse(JSON.stringify(initData))
const result = checksumSet(data, false, true)
expect(result).to.not.equal(53885) // result of one-time regex
})

it('is not done by sanitizing just the first and last strings', () => {
const data = JSON.parse(JSON.stringify(initData))
const result = checksumSet(data, false, true)
expect(result).to.not.equal(53853) // result of first/last substitution onlye
})
})
})
})
3 changes: 3 additions & 0 deletions 2023/day-01/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// eslint-disable-next-line no-unused-vars
const console = require('../helpers')
require('./solution')
Loading