diff --git a/2020/day-01/expenseValidation.js b/2020/day-01/expenseValidation.js new file mode 100644 index 0000000..a514b83 --- /dev/null +++ b/2020/day-01/expenseValidation.js @@ -0,0 +1,63 @@ + +/** + * Validates a list of records by comparing every combination + * to the checksum. Stops when the first match is found + * @param {array} records List of records to check + * @param {int} checksum The target sum that records should add up to + * @param {int} goal The number of records we hope to find + */ +const validateRecords = (records, checksum = 2020, goal = 2) => { + const results = [] + + // Intentionally using `function()` instead of `() =>` because + // the thisArg won't get passed to the find callback otherwise + // https://stackoverflow.com/questions/46639131/javascript-array-prototype-find-second-argument-thisarg-not-working + function matcher (record) { + this.depth = this.depth || 1 // depth tracking starts at level 1 + this.tracker = this.tracker || 0 // for basic sums, start counter at 0 + const subTotal = this.tracker + record + // Found a match in the specified with desired qty of results, don't keep searching! + if (subTotal === this.target && this.depth >= goal) { + results.push(record) + return true + } + // When subtotal exceeds target, return immediately and don't waste time + // on more loops that won't get results + if (subTotal > this.target) { + return false + } + // If we're already at max depth, don't waste time on more loops + if (this.depth >= this.maxDepth) { + return false + } + // Check the next level down + const res = records.find(matcher, { + maxDepth: this.maxDepth, + target: this.target, + depth: this.depth + 1, + tracker: this.tracker + record + }) + // Propogate maches back up the recursion chain, capturing each + if (res) { + results.push(record) + return true + } + // Nothing found with this combo, step to the next sibling + return false + } + + // Parse the records to find results + records.find(matcher, { + maxDepth: goal, + target: checksum + }) + + if (results.length < 2) { + throw new Error('Couldn\'t find a checksum match') + } + return results +} + +module.exports = { + validateRecords +} diff --git a/2020/day-01/expenseValidation.test.js b/2020/day-01/expenseValidation.test.js new file mode 100644 index 0000000..0e2258e --- /dev/null +++ b/2020/day-01/expenseValidation.test.js @@ -0,0 +1,78 @@ +/* eslint-env mocha */ +const { expect } = require('chai') +const { validateRecords } = require('./expenseValidation') + +/** + * Sum all the values in an array + */ +const arrSum = (arr) => arr.reduce((x, y) => x + y, 0) +/** + * Multiply all the values in an array + */ +const arrMult = (arr) => arr.reduce((x, y) => x * y, 1) +const testData = [ + 1721, + 979, + 366, + 299, + 675, + 1456 +] + +describe('--- 2020 Day 1: Report Repair ---', () => { + describe('Part 1', () => { + describe('validateRecords()', () => { + it('it finds the two records that sum to 2020', () => { + const expected = [1721, 299] + const results = validateRecords(testData, undefined, 2) + // Should be 2 results + expect(results.length).to.equal(2) + // Result order is unnecessary, but all expected hould be in the result set + expected.forEach(result => { + expect(testData.indexOf(result)).to.be.greaterThan(-1) + }) + // Results add up to the checksum + expect(arrSum(results)).to.equal(2020) + // Confirm the multiplied total + expect(arrMult(results)).to.equal(514579) + }) + it('it supports specifying an alternate checksum', () => { + const expected = [testData[3], testData[5]] + const checksum = arrSum(expected) + const results = validateRecords(testData, checksum) + // Should be 2 results + expect(results.length).to.equal(2) + // Result order is unnecessary, but all expected hould be in the result set + expected.forEach(result => { + expect(results.indexOf(result)).to.be.greaterThan(-1) + }) + // Results add up to the checksum + expect(arrSum(results)).to.equal(checksum) + }) + it('Throws an error when no records provided', () => { + expect(validateRecords).to.throw() + }) + it('Throws an error when no records match checksum', () => { + expect(() => validateRecords([1, 2, 3], 100)).to.throw('Couldn\'t find a checksum match') + }) + }) + }) + describe('Part 2', () => { + describe('validateRecords()', () => { + it('it can find a specified number of records adding up to 2020', () => { + const expected = [979, 366, 675] + const results = validateRecords(testData, undefined, 3) + // Should same number of results + expect(results.length).to.equal(expected.length) + // Result order is unnecessary, but all expected hould be in the result set + expected.forEach(result => { + expect(testData.indexOf(result)).to.be.greaterThan(-1) + }) + // Results add up to the checksum + expect(arrSum(results)).to.equal(2020) + // Confirm the multiplied total + expect(arrMult(results)).to.equal(241861950) + }) + }) + }) +}) diff --git a/2020/day-01/index.js b/2020/day-01/index.js new file mode 100644 index 0000000..a7e4223 --- /dev/null +++ b/2020/day-01/index.js @@ -0,0 +1 @@ +require('./solution') diff --git a/2020/day-01/input.txt b/2020/day-01/input.txt new file mode 100644 index 0000000..e297f2b --- /dev/null +++ b/2020/day-01/input.txt @@ -0,0 +1,200 @@ +1539 +1914 +1866 +1407 +1706 +1423 +1834 +1700 +1573 +1486 +1743 +1394 +1693 +1705 +1530 +1811 +1626 +1473 +1901 +1481 +1527 +1841 +1891 +1750 +1343 +1899 +401 +1896 +1627 +1593 +1541 +874 +1484 +1210 +1692 +1963 +1964 +1780 +671 +1862 +1393 +1309 +1740 +1831 +1932 +1185 +1979 +1504 +1663 +1610 +1494 +1511 +1103 +1738 +1816 +1871 +1545 +1595 +1784 +1412 +1815 +1998 +1783 +1770 +1426 +1699 +1416 +1880 +1612 +1989 +1360 +1869 +1762 +1690 +1999 +1990 +1521 +1730 +703 +1463 +1670 +1472 +1413 +1669 +1502 +1548 +1475 +1694 +1314 +1980 +980 +1667 +890 +1569 +1456 +1406 +1924 +1973 +1965 +1533 +1827 +2000 +1847 +1520 +1729 +1512 +1555 +1566 +1505 +1672 +1169 +1835 +1850 +1493 +1861 +1288 +1675 +1676 +1556 +1320 +1757 +1870 +1642 +1903 +1372 +1967 +1894 +176 +1908 +1418 +1535 +1487 +1496 +1491 +1611 +1970 +1758 +1563 +1766 +1629 +1937 +1763 +1829 +1772 +1632 +1517 +1736 +1971 +1721 +1716 +1429 +1408 +1560 +1958 +1359 +1890 +1825 +1536 +1819 +1697 +1887 +1832 +2005 +892 +1471 +1425 +1677 +1673 +1128 +1878 +1062 +1470 +1875 +1854 +1518 +1568 +1919 +256 +1532 +1711 +1944 +1344 +1330 +1636 +1957 +1709 +1551 +1983 +1674 +1671 +1959 +1760 +1689 +1767 +1477 +1589 +1897 +1144 +1982 +1544 \ No newline at end of file diff --git a/2020/day-01/solution.js b/2020/day-01/solution.js new file mode 100644 index 0000000..92ee010 --- /dev/null +++ b/2020/day-01/solution.js @@ -0,0 +1,37 @@ +const fs = require('fs') +const path = require('path') +const filePath = path.join(__dirname, 'input.txt') +const { validateRecords } = require('./expenseValidation') +const { inputToArray } = require('../../2018/inputParser') + +fs.readFile(filePath, { encoding: 'utf8' }, (err, initData) => { + if (err) throw err + + initData = inputToArray(initData.trim()) + .map(el => Number(el)) + + const resetInput = () => { + // Deep copy to ensure we aren't mutating the original data + return JSON.parse(JSON.stringify(initData)) + } + + const part1 = () => { + const data = resetInput() + return validateRecords(data) // Find 2 results for 2020 + .reduce((total, res) => total * res, 1) + } + + const part2 = () => { + const data = resetInput() + return validateRecords(data, undefined, 3) // Find 3 results for 2020 + .reduce((total, res) => total * res, 1) + } + const answers = [] + answers.push(part1()) + answers.push(part2()) + + answers.forEach((ans, idx) => { + console.info(`-- Part ${idx + 1} --`) + console.info(`Answer: ${ans}`) + }) +})