Count the trees along the way to the toboggan airport.
I tried to be quick, so at first I wrote this messy code:
const input = `
..##.......
#...#...#..
.#....#..#.
..#.#...#.#
.#...##..#.
..#.##.....
.#.#.#....#
.#........#
#.##...#...
#...##....#
.#..#...#.#
`
.trim()
.split('\n')
.map((row) => row.trim())
let x = 0
let result = 0
input.forEach((row) => {
if (row[x % row.length] === '#') result++
x += 3
})
console.log(result)
After verifying that the above code works, I refactored the mutable variables away:
const result = input.reduce(
(acc, row, i) => (row[(3 * i) % row.length] === '#' ? acc + 1 : acc),
0
)
console.log(result)
Same as Part 1, but there are many slopes, and their tree counts should be multiplied together.
We just™ need to
- slightly modify our reducer to correctly handle rows where
y > 1
- use another reducer for the multiplication.
Behold:
const slopes = [
{ x: 1, y: 1 },
{ x: 3, y: 1 },
{ x: 5, y: 1 },
{ x: 7, y: 1 },
{ x: 1, y: 2 },
]
function countTreesInSlope(slope) {
return input.reduce((acc, row, i) => {
if (i % slope.y !== 0) return acc
return row[((slope.x * i) / slope.y) % row.length] === '#' ? acc + 1 : acc
}, 0)
}
const result = slopes.reduce((acc, slope) => acc * countTreesInSlope(slope), 1)
console.log(result)
That's a bit sloppy (pun intended), but hey, it works.
The first line of the first reducer (the if
statement)
is basically filtering some rows.
So using filter()
before the reducer makes sense
and makes the code a bit cleaner:
function countTreesInSlope(slope) {
return input
.filter((row, i) => i % slope.y === 0)
.reduce(
(acc, row, i) =>
row[(slope.x * i) % row.length] === '#' ? acc + 1 : acc,
0
)
}
const result = slopes.reduce((acc, slope) => acc * countTreesInSlope(slope), 1)
console.log(result)
Finally, I replaced the reducer with another filter. The filter might not be as semantic as the reducer, but I think it makes the code cleaner:
function countTreesInSlope(slope) {
return input
.filter((row, i) => i % slope.y === 0)
.filter((row, i) => row[(slope.x * i) % row.length] === '#').length
}
const result = slopes.reduce((acc, slope) => acc * countTreesInSlope(slope), 1)
console.log(result)
I didn't learn anything new from doing the puzzle,
but later on this day
I learned something distantly relevant about reduce()
:
specifying initialValue
(the second argument) for a reducer is optional.
From
MDN's reduce()
page
about initialValue
:
A value to use as the first argument to the first call of the
callback
. If noinitialValue
is supplied, the first element in the array will be used as the initialaccumulator
value and skipped ascurrentValue
. Callingreduce()
on an empty array without aninitialValue
will throw aTypeError
.
I first saw this in Sam Selikoff's video Tailwind CSS Tips, Tricks & Best Practices. He wrote this piece of code (slightly adapted):
const aspectRatio = '16:9'
const paddingBottom = aspectRatio
.split(':')
.reduce((first, second) => second / first)
return <div style={{ paddingBottom: `${paddingBottom * 100}%` }} />
Fascinating!
I would have calculated paddingBottom
probably like this:
const aspectRatio = '16:9'
const [first, second] = aspectRatio.split(':')
const paddingBottom = second / first
But the reducer version is interesting.
Albeit still a bit confusing
as I only just learned that specifying initialValue
is optional.
But interesting!
One thing to keep in mind is the last sentence from the MDN quote:
Calling
reduce()
on an empty array without aninitialValue
will throw aTypeError
.