diff --git a/2019/day-03/input.txt b/2019/day-03/input.txt new file mode 100644 index 0000000..4c3baef --- /dev/null +++ b/2019/day-03/input.txt @@ -0,0 +1,2 @@ +R1000,D940,L143,D182,L877,D709,L253,U248,L301,U434,R841,U715,R701,U92,R284,U115,R223,U702,R969,U184,L992,U47,L183,U474,L437,D769,L71,U96,R14,U503,R144,U432,R948,U96,L118,D696,R684,U539,L47,D851,L943,U606,L109,D884,R157,U946,R75,U702,L414,U347,R98,D517,L963,D177,R467,D142,L845,U427,R357,D528,L836,D222,L328,U504,R237,U99,L192,D147,L544,D466,R765,U845,L267,D217,L138,U182,R226,U466,R785,U989,R55,D822,L101,U292,R78,U962,R918,U218,L619,D324,L467,U885,L658,U890,L764,D747,R369,D930,L264,D916,L696,U698,R143,U537,L922,U131,R141,D97,L76,D883,R75,D657,R859,U503,R399,U33,L510,D318,L455,U128,R146,D645,L147,D651,L388,D338,L998,U321,L982,U150,R123,U834,R913,D200,L455,D479,L38,U860,L471,U945,L946,D365,L377,U816,R988,D597,R181,D253,R744,U472,L345,U495,L187,D443,R924,D536,R847,U430,L145,D827,L152,D831,L886,D597,R699,D751,R638,D580,L488,D566,L717,D220,L965,D587,L638,D880,L475,D165,L899,U388,R326,D568,R940,U550,R788,D76,L189,D641,R629,D383,L272,D840,L441,D709,L424,U158,L831,D576,R96,D401,R425,U525,L378,D907,L645,U609,L336,D232,L259,D280,L523,U938,R190,D9,L284,U941,L254,D657,R572,U443,L850,U508,L742,D661,L977,U910,L190,U626,R140,U762,L673,U741,R317,D518,R111,U28,R598,D403,R465,D684,R79,U725,L556,U302,L367,U306,R632,D550,R89,D292,R561,D84,L923,D109,L865,D880,L387,D24,R99,U934,L41,U29,L225,D12,L818,U696,R652,U327,L69,D773,L618,U803,L433,D467,R840,D281,R161,D400,R266,D67,L205,D94,R551,U332,R938,D759,L437,D515,L480,U774,L373,U478,R963,D863,L735,U138,L580,U72,L770,U968,L594 +L990,D248,L833,U137,L556,U943,R599,U481,R963,U812,L825,U421,R998,D847,R377,D19,R588,D657,R197,D354,L548,U849,R30,D209,L745,U594,L168,U5,L357,D135,R94,D686,R965,U838,R192,U428,L861,U354,R653,U543,L633,D508,R655,U575,R709,D53,L801,D709,L92,U289,L466,D875,R75,D448,R576,D972,L77,U4,L267,D727,L3,D687,R743,D830,L803,D537,L180,U644,L204,U407,R866,U886,R560,D848,R507,U470,R38,D652,R806,D283,L836,D629,R347,D679,R609,D224,L131,D616,L687,U181,R539,D829,L598,D55,L806,U208,R886,U794,L268,D365,L145,U690,R50,D698,L140,D512,L551,U845,R351,U724,R405,D245,L324,U181,L824,U351,R223,D360,L687,D640,L653,U158,R786,D962,R931,D151,R939,D34,R610,U684,L694,D283,R402,D253,R388,D195,R732,U809,R246,D571,L820,U742,L507,U967,L886,D693,L273,U558,L914,D122,R146,U788,R83,U149,R241,U616,R326,U40,L192,D845,L577,U803,L668,D443,R705,D793,R443,D883,L715,U757,R767,D360,L289,D756,R696,D236,L525,U872,L332,U203,L152,D234,R559,U191,R340,U926,L746,D128,R867,D562,L100,U445,L489,D814,R921,D286,L378,D956,L36,D998,R158,D611,L493,U542,R932,U957,R55,D608,R790,D388,R414,U670,R845,D394,L572,D612,R842,U792,R959,U7,L285,U769,L410,D940,L319,D182,R42,D774,R758,D457,R10,U82,L861,D901,L310,D217,R644,U305,R92,U339,R252,U460,R609,D486,R553,D798,R809,U552,L183,D238,R138,D147,L343,D597,L670,U237,L878,U872,R789,U268,L97,D313,R22,U343,R907,D646,L36,D516,L808,U622,L927,D982,L810,D149,R390,U101,L565,U488,L588,U426,L386,U305,R503,U227,R969,U201,L698,D850,R800,D961,R387,U632,R543,D541,R750,D174,R543,D237,R487,D932,R220 \ No newline at end of file diff --git a/2019/day-03/solution.js b/2019/day-03/solution.js new file mode 100644 index 0000000..e923220 --- /dev/null +++ b/2019/day-03/solution.js @@ -0,0 +1,16 @@ +const fs = require('fs') +const path = require('path') +const filePath = path.join(__dirname, 'input.txt') +const { findWireIntersections, getClosesetIntersection } = require('./wires') +const { linesToArray } = require('../../2018/inputParser') + +fs.readFile(filePath, { encoding: 'utf8' }, (err, data) => { + if (err) throw err + + data = linesToArray(data.trim()) + + const answer = getClosesetIntersection(findWireIntersections(data)).distance + + console.log('-- Part 1 --') + console.log(`Answer: ${answer}`) +}) diff --git a/2019/day-03/wires.js b/2019/day-03/wires.js new file mode 100644 index 0000000..5337af8 --- /dev/null +++ b/2019/day-03/wires.js @@ -0,0 +1,121 @@ +const intersection = require('path-intersection') +const { distance } = require('../../2018/day-06/coordinates') // Manhattan distance function from last year + +const elfWireToSVGPath = (path) => { + const replacements = { + R: 'h', // R(ight) becomes relative positive horizontal lineto + L: 'h-', // L(eft) becomes relative negative horizontal lineto + U: 'v-', // U(p) becomes relative negative vertical line + D: 'v', // D(own) becomes relative positive vertical line + ',': ' ' // Separators are done with whitespace + } + path = path.trim() + + const pattern = new RegExp(Object.keys(replacements).join('|'), 'gi') + path = path.replace(pattern, (match) => { + return replacements[match] + }) + + return `M0,0 ${path}` +} + +const findWireIntersections = (wires) => { + wires = wires.map(elfWireToSVGPath) + const ints = intersection( + ...wires + ).map((point) => { + return { x: parseInt(point.x), y: parseInt(point.y) } + }) + + return ints.sort(isCloser.manhattan) +} + +const isCloser = { + manhattan: (intA, intB) => { + const origin = { x: 0, y: 0 } + intA.distance = distance(origin, intA) + intB.distance = distance(origin, intB) + if (intA.distance < intB.distance) { + return -1 + } + if (intA.distance > intB.distance) { + return 1 + } + if (intA.distance === intB.distance) { + return 0 + } + } +} + +const advance = ({ segment, remaining, distance, current }) => { + // Track the step + switch (direction) { + case 'U': // Up + current.y += -dimension + break + case 'D': // Down + current.y += dimension + break + case 'R': // Right + current.x += dimension + break + case 'L': // Left + current.x += -dimension + } + remaining += -1 + distance++ +} + +const getIntersectionWireDistance = ({ intersection, wires }) => { + intersection.wireDistance = 0 + + wires.reduce((wire) => { + const segments = wire.split(',') + const current = { x: 0, y: 0 } + const distance = 0 + + segments.forEach((segment, idx) => { + const direction = segment.slice(0, 1) + const length = parseInt(segment.slice(1)) + for (let d = 0; d < length; d++) { + advance({ direction, remaining, distance, current }) + // Reached the desired intersection, stop counting and track result + if (current.x === intersection.x && current.y === intersection.y) { + intersection.wireDistance += distance + break + } + } + }) + }, 0) + + return intersection.wireDistance +} + +const getClosesetIntersection = ({ + intersections, + method = 'manhattan' +}) => { + intersections.sort(isCloser[method]) + + // TODO: Remove workaround for bug in SVG intersection library + // https://github.com/bpmn-io/path-intersection/issues/10 + // + // The shared origin inconsistently shows up in the intersection list + if (parseInt(intersections[0].x) === 0 && parseInt(intersections[0].y) === 0) { + // Skip the shared origin since all wires start at origin + return intersections[1] + } + return intersections[0] +} + +const getClosesetIntersectionByWire = (intersections) => { + intersections.sort(isCloserByWire) +} + +module.exports = { + elfWireToSVGPath, + findWireIntersections, + getClosesetIntersection, + getIntersectionWireDistance, + getClosestIntersectionByWireDistance +} diff --git a/2019/day-03/wires.test.js b/2019/day-03/wires.test.js new file mode 100644 index 0000000..dc7ab23 --- /dev/null +++ b/2019/day-03/wires.test.js @@ -0,0 +1,91 @@ +/* eslint-env mocha */ +const expect = require('chai').expect +const { elfWireToSVGPath, findWireIntersections, getClosesetIntersection, getIntersectionWireDistance, getClosesetIntersectionByWire } = require('./wires') + +describe('--- 2019 Day 3: Crossed Wires ---', () => { + describe('Part 1', () => { + describe('elfWiresToSVGPath()', () => { + it('converts elfwire syntax to svg path syntax', () => { + const wire = 'R75,D30,R83,U83,L12,D49,R71,U7,L72' + const expected = 'M0,0 h75 v30 h83 v-83 h-12 v49 h71 v-7 h-72' + const actual = elfWireToSVGPath(wire) + expect(actual).to.equal(expected) + }) + }) + describe('findWireIntersections()', () => { + it('finds the intersection points of multiple wires', () => { + const wires = [ + 'R8,U5,L5,D3', + 'U7,R6,D4,L4' + ] + const expected = [ + { x: 0, y: 0, distance: 0 }, + { x: 3, y: -3, distance: 6 }, + { x: 6, y: -5, distance: 11 } + ] + const actual = findWireIntersections(wires) + expect(actual).to.deep.equal(expected) + }) + }) + describe('getClosestIntersection()', () => { + it('finds the closest intersection in a list of intersections', () => { + const intersections = [ + { x: 23, y: 45 }, + { x: 48, y: -10 }, + { x: 3, y: 3 }, + { x: 3, y: 3 } + ] + const expected = { x: 3, y: 3, distance: 6 } + const actual = getClosesetIntersection(intersections) + expect(actual).to.deep.equal(expected) + }) + }) + it('can be used to find the distance to the closest intersection', () => { + const wiresets = [ + [ + 'R75,D30,R83,U83,L12,D49,R71,U7,L72', + 'U62,R66,U55,R34,D71,R55,D58,R83' + ], [ + 'R98,U47,R26,D63,R33,U87,L62,D20,R33,U53,R51', + 'U98,R91,D20,R16,D67,R40,U7,R15,U6,R7' + ] + ] + const distances = [159, 135] + wiresets.forEach((wires, idx) => { + expect(getClosesetIntersection(findWireIntersections(wires)).distance).to.equal(distances[idx]) + }) + }) + describe('getIntersectionWireDistance()', () => { + it('calculates the summed total wire length to reach the specified intersection', () => { + const wires = [ + 'R8,U5,L5,D3', + 'U7,R6,D4,L4' + ] + const expected = [ + 40, 30 + ] + const intersections = findWireIntersections(...wires) + const actual = intersections.map((inter) => getIntersectionWireDistance({ wires, intersection: inter })) + expect(actual).to.deep.equal(expected) + }) + }) + describe('getClosestIntersectionByWireDistance()', () => { + it('can be used to find the wire distance to the closest intersection', () => { + const wiresets = [ + [ + 'R75,D30,R83,U83,L12,D49,R71,U7,L72', + 'U62,R66,U55,R34,D71,R55,D58,R83' + ], [ + 'R98,U47,R26,D63,R33,U87,L62,D20,R33,U53,R51', + 'U98,R91,D20,R16,D67,R40,U7,R15,U6,R7' + ] + ] + const distances = [610, 410] + wiresets.forEach((wires, idx) => { + const intersections = findWireIntersections(wires) + expect(getClosesetIntersectionByWire({ intersections, wires }).distance).to.equal(distances[idx]) + }) + }) + }) + }) +}) diff --git a/index.js b/index.js index 8bc439d..0314d0a 100644 --- a/index.js +++ b/index.js @@ -1 +1 @@ -require('./2019/day-02/solution') +require('./2019/day-03/solution') diff --git a/package-lock.json b/package-lock.json index 3bc796e..b2be39e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7854,6 +7854,11 @@ "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", "dev": true }, + "path-intersection": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-intersection/-/path-intersection-2.0.1.tgz", + "integrity": "sha512-tvqxG1oZoq1q7QidC5REg6tafAA38X4VvB5ZLzPAl/nIoh05dmYegTJUwNXOyJ7D3Qk/rN6KsZrbSPAyHx/AiA==" + }, "path-is-absolute": { "version": "1.0.1", "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", diff --git a/package.json b/package.json index dacdf9a..7eee33d 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "index.js", "private": true, "scripts": { + "start": "node index.js", "pretest": "npm run lint", "test": "nyc mocha \"./20*/**/*.test.js\"", "posttest": "nyc report --reporter=html --reporter=text-lcov > coverage.lcov", @@ -35,5 +36,8 @@ "nyc": "^14.1.1", "semantic-release": "^15.13.31", "standard": "^14.3.1" + }, + "dependencies": { + "path-intersection": "^2.0.1" } }