Advent of Code 2022, but in JS and point-free style: Day 5

Piotr Jaworski
6 min readDec 10, 2022

--

Photo by Elena Mozhvilo on Unsplash

The complete list of articles up to date:

In my previous article, we cleaned up the Elves’ camp before the arrival of the supply ship using Ramda.js and the point-free style. This time, we will unload the supply crates from the ship!

Let’s look at the task at hand: given the initial state of the crate stacks and a list of crate moves, we need to calculate the final state — and extract the top crate symbol from each stack. Let’s go!

import R from "ramda";

import data from "./data.txt";

R.pipe(
R.split("\n\n"),
console.log
)(data);

Since this time the shape of our data is a bit different (stack state and list of moves separated by an empty line), we start by splitting the long input string into two also pretty long strings, by an empty line.

import R from "ramda";

import data from "./data.txt";

const STACK_OFFSET = 4;

const flippedMap = R.flip(R.uncurryN(2, R.map));

const rotateMatrix = R.pipe(
R.juxt([
R.identity,
R.pipe(R.map(R.prop("length")), R.apply(Math.max), R.range(0))
]),
([arrays, range]) => R.map(R.pipe(R.prop, flippedMap(arrays)), range)
);

const getStacks = R.pipe(
R.split("\n"),
R.init,
R.reverse,
R.map(R.splitEvery(STACK_OFFSET)),
rotateMatrix,
R.map(R.pipe(R.map(R.replace(/\W/gi, "")), R.filter(R.identity)))
);

R.pipe(
R.split("\n\n"),
R.over(R.lensIndex(0), getStacks),
console.log
)(data);

First, we want to parse the stacks. This is a bit of a separate task of its own. The output of the R.split("\n\n") call is an array with a length of two. We want to pass two arguments into the next call (after we parse all the data), so we’re using the over function, which preserves the shape of the input, performing transformations on the parts of the input specified in the lens. For stacks, it’s the first entry in the input array.

Let’s take a look at the getStacks function. First, we split the string into lines. Then, using init, we discard the last line of the input, containing stack labels (we won’t need it anyway). Next, we reverse the order of the lines — since we’ll want the bottoms of our stacks at the beginning of the arrays we’re going to come up with. After that we split every line at every four characters (since each stack starts at every four characters). The next step is a bit tricky — we need to rotate our stack matrix. Now, we have an array for every row of the stacks, and we need an array for each stack!

Let’s look at the rotateMatrix function now. First, we need some additional data — an array with entries incrementing from zero and a length equal to the number of stacks. For that, we need to get the maximum of the number of entries in our stack rows. Since we need to pass it into the next function along with the original matrix, we do it using the juxt and identity function, exactly like in the previous articles.

The next part is a bit hard to grok (This is actually one of the two functions in this day’s challenge that I gave up on with sticking to the point-free style — I didn’t have more time to spend on trying to figure this out. If you have an idea on how to make it better — please let me now in the comments!). We map over the range array, so we’re creating a number of arrays equal to the number of stacks. Next we call functions in a pipe — first, we pass the prop function without arguments, so it receives the desired prop as the first argument — in our case it’s each of the entries from the range array, so it’s the index of a stack. After that, we pass the prop called with the first argument into a flipped map function, which already received the arrays argument (so the original stack rows). In short — for each column, we iterate over the rows and put them into the corresponding stacks.

The next step is pretty straightforward — we filter out in the entries that is not a letter (since this is the only thing that is of interest anyway) and filter out the empty entries in each stack (not every stack is of the same height). Next, we can proceed with parsing the moves:

import R from "ramda";

import data from "./data.txt";

const STACK_OFFSET = 4;

const flippedMap = R.flip(R.uncurryN(2, R.map));

const rotateMatrix = R.pipe(
R.juxt([
R.identity,
R.pipe(R.map(R.prop("length")), R.apply(Math.max), R.range(0))
]),
([arrays, range]) => R.map(R.pipe(R.prop, flippedMap(arrays)), range)
);

const getStacks = R.pipe(
R.split("\n"),
R.init,
R.reverse,
R.map(R.splitEvery(STACK_OFFSET)),
rotateMatrix,
R.map(R.pipe(R.map(R.replace(/\W/gi, "")), R.filter(R.identity)))
);

const getMoves = R.pipe(
R.split("\n"),
R.map(R.pipe(R.replace(/\D+/gi, " "), R.trim, R.split(" "), R.map(parseInt)))
);

R.pipe(
R.split("\n\n"),
R.pipe(R.over(R.lensIndex(0), getStacks), R.over(R.lensIndex(1), getMoves)),
console.log
)(data);

This, in comparison, is pretty simple. We split the string for moves into lines, and then we replace everything that is not a number (only numbers interest us at this point) into an empty space. Next, we trim each row and split it by those spaces, and call parseInt on each entry. In effect, we end up with an array of arrays containing three numbers, each. This will be sufficient to move the stacks around — those numbers represent the number of repetitions of each action, from which stack the package in question will be picked up and on which it’s going to be placed in a given movement.

Next, let’s process the actual package movements:

import R from "ramda";

import data from "./data.txt";

const STACK_OFFSET = 4;

const flippedMap = R.flip(R.uncurryN(2, R.map));

const rotateMatrix = R.pipe(
R.juxt([
R.identity,
R.pipe(R.map(R.prop("length")), R.apply(Math.max), R.range(0))
]),
([arrays, range]) => R.map(R.pipe(R.prop, flippedMap(arrays)), range)
);

const getStacks = R.pipe(
R.split("\n"),
R.init,
R.reverse,
R.map(R.splitEvery(STACK_OFFSET)),
rotateMatrix,
R.map(R.pipe(R.map(R.replace(/\W/gi, "")), R.filter(R.identity)))
);

const getMoves = R.pipe(
R.split("\n"),
R.map(R.pipe(R.replace(/\D+/gi, " "), R.trim, R.split(" "), R.map(parseInt)))
);

const pop = R.flip(R.uncurryN(2, R.invoker(1, "pop")));
const push = R.flip(R.uncurryN(2, R.invoker(1, "push")));

const movePackages = (stacks, [howMany, from, to]) =>
R.pipe(
R.range(0),
R.forEach(
R.pipe(pop(R.prop(from - 1, stacks)), push(R.prop(to - 1, stacks)))
),
R.always(stacks)
)(howMany);

R.pipe(
R.split("\n\n"),
R.pipe(R.over(R.lensIndex(0), getStacks), R.over(R.lensIndex(1), getMoves)),
R.apply(R.reduce(movePackages)),
console.log
)(data);

To move the packages around, we’ll need functions to pop and push them in and out of the stacks. We’re using the invoker function that we already used before, but this time we’re doing something a bit strange. Normally, pop would have an arity of 0 (just as the original Array.prototype.pop method), but we need an additional empty argument to be able to call it without using a lambda function in one of the next steps.

Next, we can concentrate on moving the packages. We’ll be using the reduce function, which is the Ramda equivalent of the built-in Array method. We use reduce, because that allows us to pass the original stacks state as the initial reduce state. The second reason is because the iterator function in reduce naturally uses two arguments, and we need those for our stacks state and the current move description.

The movePackage functions is pretty straightforward (apart from the fact that it’s the second — and last — non point-free style function today; if you have an idea how to convert it to such — feel free to post it in the comments!). We start with howMany, which defines the number of repetitions. We create a range with an appropriate number of entries. Then for each of the entries in the range we pop a package from the from stack and push it into the to stack. One thing that we need to remember is to return the stacks list at the end of each step, for which we use the always function.

The next two steps are very simple, so let’s handle them in the same incrementation of the listing:

import R from "ramda";

import data from "./data.txt";

const STACK_OFFSET = 4;

const flippedMap = R.flip(R.uncurryN(2, R.map));

const rotateMatrix = R.pipe(
R.juxt([
R.identity,
R.pipe(R.map(R.prop("length")), R.apply(Math.max), R.range(0))
]),
([arrays, range]) => R.map(R.pipe(R.prop, flippedMap(arrays)), range)
);

const getStacks = R.pipe(
R.split("\n"),
R.init,
R.reverse,
R.map(R.splitEvery(STACK_OFFSET)),
rotateMatrix,
R.map(R.pipe(R.map(R.replace(/\W/gi, "")), R.filter(R.identity)))
);

const getMoves = R.pipe(
R.split("\n"),
R.map(R.pipe(R.replace(/\D+/gi, " "), R.trim, R.split(" "), R.map(parseInt)))
);

const pop = R.flip(R.uncurryN(2, R.invoker(1, "pop")));
const push = R.flip(R.uncurryN(2, R.invoker(1, "push")));

const movePackages = (stacks, [howMany, from, to]) =>
R.pipe(
R.range(0),
R.forEach(
R.pipe(pop(R.prop(from - 1, stacks)), push(R.prop(to - 1, stacks)))
),
R.always(stacks)
)(howMany);

R.pipe(
R.split("\n\n"),
R.pipe(R.over(R.lensIndex(0), getStacks), R.over(R.lensIndex(1), getMoves)),
R.apply(R.reduce(movePackages)),
R.map(R.last),
R.join(""),
console.log
)(data);

We map the stacks into their last (so — topmost) packages, and — since those are actually letters — we can join them into a single string. Which, by complete accident, is the answer to the Day 5 of 2022’s Advent of Code!

A working example can be found in this CodeSandbox. Follow me for articles covering the next days! Also — that’s only the first half of the solution — share yours in the comments!

I mentor software developers. Drop me a line on MentorCruise for long-term mentorship or on CodeMentor for individual sessions.

--

--