In my previous article, we checked the Elves’ backpacks for duplicate items using Ramda.js and the point-free style. This time, we’re going to help the Elves with cleaning up their camp before the ship with supplies arrives!
We’re going to help by cross-checking assignments and checking which ones overlap entirely. Let’s take a look at the solution code:
import R from "rambda";
import data from "./data.txt";
R.pipe(
splitIntoLines,
splitLinesByComa,
translateRangeStringsToArrays,
filterCompletelyIncluded,
getLength,
console.log
)(data);
Well, that was easy! Thank you for reading along. If you have any questions, please post them in the comments! On a more serious note, this time, we’re going to tackle the problem with a slightly different approach.
Let’s take a look at the code above. The first thing you probably noticed is that this is almost like pseudo-code — it reads almost like plain English! That’s one of the greatest perks of the point-free style — it enforces splitting the logic into chunks that have a sense on their own, which means they can be separated and named.
Let’s take a look at the first function, splitIntoLines
. This is nothing new. As usual, we store the input as a multiline string, so we need to split it into an array of lines before we do anything else.
import R from "rambda";
import data from "./data.txt";
const splitIntoLines = R.split("\n");
R.pipe(
splitIntoLines,
splitLinesByComa,
translateRangeStringsToArrays,
filterCompletelyIncluded,
getLength,
console.log
)(data);
Since each of our lines has a format of {number}-{number},{number}-{number}
, we also need to split each line by the coma using our splitLinesByComa
function:
import R from "rambda";
import data from "./data.txt";
const splitIntoLines = R.split("\n");
const splitLinesByComa = R.map(R.split(","));
R.pipe(
splitIntoLines,
splitLinesByComa,
translateRangeStringsToArrays,
filterCompletelyIncluded,
getLength,
console.log
)(data);
The next one is going to be a bit more complicated. We need to split the current state of a line — which is [{number}-{number},{number}-{number}]
, into an array of ranges:
import R from "rambda";
import data from "./data.txt";
const splitIntoLines = R.split("\n");
const splitLinesByComa = R.map(R.split(","));
const translateRangeStringsToArrays = R.map(
R.map(
R.pipe(
R.split("-"),
R.map(parseInt),
R.over(R.lensIndex(1), R.add(1)),
R.apply(R.range)
)
)
);
R.pipe(
splitIntoLines,
splitLinesByComa,
translateRangeStringsToArrays,
filterCompletelyIncluded,
getLength,
console.log
)(data);
This is no rocket science. The only “gotcha” moment happens in the over
call — since range
creates a range that doesn’t include the number passed as the second argument, and we actually need that number to be in the range, we need to increment the second element of each array (so with the index of 1
) by 1
.
OK, so we now have an array of ranges. We need to check if any of them is entirely included in the second one. This happens, if the length of an intersection of both ranges is equal to the length of the shorter one:
import R from "rambda";
import data from "./data.txt";
const splitIntoLines = R.split("\n");
const splitLinesByComa = R.map(R.split(","));
const translateRangeStringsToArrays = R.map(
R.map(
R.pipe(
R.split("-"),
R.map(parseInt),
R.over(R.lensIndex(1), R.add(1)),
R.apply(R.range)
)
)
);
const filterCompletelyIncluded = R.filter(
R.pipe(
R.juxt([
R.pipe(R.map(R.prop("length")), R.apply(Math.min)),
R.pipe(R.apply(R.intersection), R.prop("length"))
]),
R.apply(R.equals)
)
);
R.pipe(
splitIntoLines,
splitLinesByComa,
translateRangeStringsToArrays,
filterCompletelyIncluded,
getLength,
console.log
)(data);
To compare the lengths, we need to use two values computed from a single data point. For that, we will use the juxt
function that we already used several times. The first function passed to juxt
extracts the length of both of the arrays and finds the shortest one. The other calculates the intersection and extracts its length. Next, we can check if the lengths are equal. Since we want to count the number of entirely overlapping assignments, we want to filter out the ones that don’t overlap completely.
Next, we only need to count the number of filtered rows:
import R from "rambda";
import data from "./data.txt";
const splitIntoLines = R.split("\n");
const splitLinesByComa = R.map(R.split(","));
const translateRangeStringsToArrays = R.map(
R.map(
R.pipe(
R.split("-"),
R.map(parseInt),
R.over(R.lensIndex(1), R.add(1)),
R.apply(R.range)
)
)
);
const filterCompletelyIncluded = R.filter(
R.pipe(
R.juxt([
R.pipe(R.map(R.prop("length")), R.apply(Math.min)),
R.pipe(R.apply(R.intersection), R.prop("length"))
]),
R.apply(R.equals)
)
);
const getLength = R.prop("length");
R.pipe(
splitIntoLines,
splitLinesByComa,
translateRangeStringsToArrays,
filterCompletelyIncluded,
getLength,
console.log
)(data);
And here it is, the code that reveals the answer to the Day 4 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.