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

Piotr Jaworski
4 min readDec 7, 2022

--

Photo by Elena Mozhvilo on Unsplash

The complete list of articles up to date:

In my previous article, we played Rock-Paper-Scissors with some Elves using nothing but Ramda.js and the point-free style approach. This time, we will look into the Elves' backpacks, looking for duplicate items.

As in the previous articles, I’m going to put the input data in a data.txt file, import it, and create a pipeline of functions using Ramda. Let’s also split the input into separate lines:

import R from "ramda";

import data from "./data.txt";

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

The next step is a bit tricky: we need to split each line in half, resulting in two strings of the same length:

import R from "ramda";

import data from "./data.txt";

const flippedDivide = R.flip(R.uncurryN(2, R.divide));

R.pipe(
R.split("\n"),
R.map(
R.pipe(
R.juxt([
R.pipe(R.prop("length"), flippedDivide(2)),
R.identity
]),
R.apply(R.splitAt),
R.map(R.split("")),
R.apply(R.intersection)
)
),
console.log
)(data);

Let’s take a look at the nested pipe call — since we’re going to need two things to split the string in half (the string itself and its length), we’re going to use juxt (which I already introduced in the previous article). The first function passed to juxt extracts the length of a string and divides it by two; the second one passes the string further on. The order of the functions passed to juxt is not random — it’s what we need to pass to splitAt to split the string at a given index (which is half the length we calculated earlier).

Next up, we can proceed to split the strings into separate letters (with the split("") call), and then we pass both letter arrays to intersection, which returns an array with all the entries which are present in both input arrays. Which is precisely what we needed to do!

Next, since we now have an array of arrays of letters, we need to do one minor transformation:

import R from "ramda";

import data from "./data.txt";

const flippedDivide = R.flip(R.uncurryN(2, R.divide));

R.pipe(
R.split("\n"),
R.map(
R.pipe(
R.juxt([
R.pipe(R.prop("length"), flippedDivide(2)),
R.identity
]),
R.apply(R.splitAt),
R.map(R.split("")),
R.apply(R.intersection)
)
),
R.unnest,
console.log
)(data);

Ramda’s unnest simply, well, unnests nested arrays, so we now have an array of letters on which we can calculate the priority value of each letter:

import R from "ramda";

import data from "./data.txt";

const flippedDivide = R.flip(R.uncurryN(2, R.divide));

const letterToCharCode = R.call(R.invoker(1, "charCodeAt"), 0);

const UPPERCASE_OFFSET = 38;
const LOWERCASE_OFFSET = 96;

const UPPERCASE_RANGE = R.range(65, 97);
const LOWERCASE_RANGE = R.range(97, 123);

const flippedSubtract = R.flip(R.uncurryN(2, R.subtract));
const flippedIncludes = R.flip(R.uncurryN(2, R.includes));

R.pipe(
R.split("\n"),
R.map(
R.pipe(
R.juxt([
R.pipe(R.prop("length"), flippedDivide(2)),
R.identity
]),
R.apply(R.splitAt),
R.map(R.split("")),
R.apply(R.intersection)
)
),
R.unnest,
R.map(
R.pipe(
letterToCharCode,
R.cond([
[flippedIncludes(UPPERCASE_RANGE), flippedSubtract(UPPERCASE_OFFSET)],
[flippedIncludes(LOWERCASE_RANGE), flippedSubtract(LOWERCASE_OFFSET)]
])
)
),
console.log
)(data);

Well, that’s a lot of new code. Let’s see what happened here. The task states that:

  • Lowercase item types a through z have priorities 1 through 26.
  • Uppercase item types A through Z have priorities 27 through 52.

Given that, first, we need to translate a letter into a numeric value. Let’s use the ASCII table char code as a base. We create the following custom function:

const letterToCharCode = R.call(R.invoker(1, "charCodeAt"), 0);

The invoker function allows calling an object method on any target (of course, calling it on an object that doesn’t have this method will result in an error). All we need to do is provide the arity of the method and its name. Since we’re only working on letters (or, more precisely: strings with a length of 1), we’re only interested in the char code at the index of 0. Hence, we can use Ramda’s call function to already supply the first argument of 0 into the charCodeAt method — now, we simply need to call the result method with the string in question.

Now we can pass the “value” of the letter into the cond call (I also describe call in the previous article). Let’s take a look:

const UPPERCASE_OFFSET = 38;
const LOWERCASE_OFFSET = 96;

const UPPERCASE_RANGE = R.range(65, 97);
const LOWERCASE_RANGE = R.range(97, 123);

const flippedSubtract = R.flip(R.uncurryN(2, R.subtract));
const flippedIncludes = R.flip(R.uncurryN(2, R.includes));

// ...

R.cond([
[flippedIncludes(UPPERCASE_RANGE), flippedSubtract(UPPERCASE_OFFSET)],
[flippedIncludes(LOWERCASE_RANGE), flippedSubtract(LOWERCASE_OFFSET)]
])

That’s a lot of new constants and functions, but it’s really simple. Since the priority value for each letter defined in the task is not equal to the ASCII char code, and furthermore, unlike in the ASCII table, in the task, the uppercase letters have a higher value than the lowercase ones, we need to provide a translation mechanism for each of the sets. For that, we need to know the offsets (the priority value is lower than the ASCII char code by 38 and by 96 for uppercase and lowercase letters, respectively) and the char code ranges for each of the sets (to be able to determine which set are we dealing with). We also need the flipped versions of includes and subtract functions.

We pass each letter to the cond call, and if the UPPERCASE_RANGE includes the letter, we subtract the UPPERCASE_OFFSET, resulting in the priority value defined in the task. Now we have an array of all the values! Only one thing left to do (which shouldn’t be a surprise by now):

import R from "ramda";

import data from "./data.txt";

const flippedDivide = R.flip(R.uncurryN(2, R.divide));

const letterToCharCode = R.call(R.invoker(1, "charCodeAt"), 0);

const UPPERCASE_OFFSET = 38;
const LOWERCASE_OFFSET = 96;

const UPPERCASE_RANGE = R.range(65, 97);
const LOWERCASE_RANGE = R.range(97, 123);

const flippedSubtract = R.flip(R.uncurryN(2, R.subtract));
const flippedIncludes = R.flip(R.uncurryN(2, R.includes));

R.pipe(
R.split("\n"),
R.map(
R.pipe(
R.juxt([
R.pipe(R.prop("length"), flippedDivide(2)),
R.identity
]),
R.apply(R.splitAt),
R.map(R.split("")),
R.apply(R.intersection)
)
),
R.unnest,
R.map(
R.pipe(
letterToCharCode,
R.cond([
[flippedIncludes(UPPERCASE_RANGE), flippedSubtract(UPPERCASE_OFFSET)],
[flippedIncludes(LOWERCASE_RANGE), flippedSubtract(LOWERCASE_OFFSET)]
])
)
),
R.sum,
console.log
)(data);

Et voilà! This allows us to discover the answer to Day 3 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!

--

--