It is December again and that means it is time for the Advent of Code. Due to my workload and family obligations I will probably not be able to get very far this year, but still I wanted to write a post about it.
This year I am using Java, together with my students. My goal is to write as modern as possible Java, which means using streams and new language constructs where possible.
Day 1
In day 1 we are parsing 2 lists of numbers, with the lists printed vertically. This means each line has 2 numbers, one for list one and the other for list two. To parse these data structures I used a very nice stream where I map
each line onto a String[]
using split
.
To be sure that the input is valid, the peek
method allows you to check if the result is what you intended, and otherwise an exception will terminate everything. From here I map
the String[]
into a Pair
record which holds the 2 numbers. Streaming over the resulting pairs
the left and right lists can be extracted quite easily.
I loved this approach, it is very straightforward and does not have a lot of control flow.
@Override
public List<List<Integer>> parseInput(List<String> input) {
var pairs = input.stream()
.map(s -> s.split("\\s+"))
.peek(parts -> {
if (parts.length != 2)
throw new IllegalArgumentException("Invalid input format");
})
.map(parts -> new Pair<>(Integer.parseInt(parts[0]), Integer.parseInt(parts[1])))
.collect(Collectors.toList());
var left = pairs.stream()
.map(Pair::left)
.collect(Collectors.toList());
var right = pairs.stream()
.map(Pair::right)
.collect(Collectors.toList());
return List.of(left, right);
}
Solving the problem with these lists was quite easy. In part 2 there was a need for a frequency table of a list. I also found a very nice solution to that problem using the groupingBy
method from Collectors
.
input.get(1).stream()
.collect(Collectors.groupingBy(n -> n, Collectors.counting()));
Day 2
I really liked day 2, the first part was quite straightforward. You have to identify increment or decrement only lists and apply some conditions to them.
Part 2 was much more interesting, here you have to account for fault tolerance. In the Python implementations that were posted the common solution is to concatenate 2 parts of the array and then rerun the validation logic.
Using streams we can something very similar. First we use an IntStream
to iterate over every int[]
(report). Then for every int
in that report, we construct a new array by filtering
out the index of the current item. After that it is a simple case of determining increment or decrement and applying the conditional logic.
Suppose you have a list of
[1,2,4,7]
, while iterating it will first hit index0
, thefilter
will prevent that entry from continuing. Next1
through3
will continue and as a result oftoArray()
a new array will be constructed with only those items.
x == 0
| map ------.
v ------- v
[1, 2, 4, 7] [2,4,7]
input.stream()
// Loop over the list
.filter(in -> IntStream.range(0, in.length) // take a report
// for every entry in that int[]
.anyMatch(x -> {
// create a new list, excluding the one we are on now
int[] c = IntStream.range(0, in.length)
.filter(i -> i != x)
.map(i -> in[i])
.toArray();
boolean allInc = IntStream.range(0, c.length - 1)
.allMatch(i -> c[i] <= c[i + 1]);
boolean allDec = IntStream.range(0, c.length - 1)
.allMatch(i -> c[i] >= c[i + 1]);
boolean good = IntStream.range(0, c.length - 1)
.allMatch(i -> Math.abs(c[i] - c[i + 1]) >= 1 &&
Math.abs(c[i] - c[i + 1]) <= 3);
// matching the condition
return (allInc || allDec) && good;
})
)
.count();
My first solution was nothing like this, but after refining it I am very happy with how clean it came out.
Day 3
This was the traditional easy puzzle after a more complicated one. Basically simple parsing for which I used regular expressions. Nothing special, on to day 4.
Day 4
For day 4 I solved the first part with an over engineered path finding solution, which turned out to be quite the overkill, but extremely fun to program.
I really like the pattern in use, below is some of the code of it. First you create a Deque
that holds the work, then you load it up with the initial starting points. In the case of the puzzle these are the location of the X
characters.
From there you just loop over the work
, taking a partial solution and seeing if any cells around it will lead to another partial solution, so from XM
to XMA
and on the next iteration to XMAS
. The dx
is a collection of Coord
that indicate valid movements across the board.
Deque<Path> work = new ArrayDeque<>();
// Load initial points
for (int y = 0; y < input.length; y++) {
for (int x = 0; x < input[y].length; x++) {
if (input[y][x] == 'X') {
work.add( new Path(List.of(new Coord(x,y)), "X", null) );
}
}
}
// Process each outstanding point..
while (!work.isEmpty()) {
var path = work.pop();
for (Coord d : dx) {
if (path.dir() != null && path.dir != d) {
continue;
}
var newCoord = lastStep.add(d);
// Ensure this is a valid point on the grid
if (newCoord.x() >= 0 && newCoord.x() < input[0].length &&
newCoord.y() >= 0 && newCoord.y() < input.length) {
// ... create new paths and string based on location
// Check if we have an end case, else add it to the work
if (target.equals(xmas)) {
matches.add(newPath);
} else if (target.startsWith(xmas)) {
work.add(newPath);
}
}
}
}
A more straightforward approach, which was actually needed for part 2, is to just try to solve it in one step. First you iterate over both y
and x
coordinates looking for an X
, just as above. When you find one, iterate over [-1, 0, 1]
on both the x
and y
axis-es, using dy
and dx
for the direction. If both direction
are 0
, we continue as it would give the current position. The beauty of this approach is that you can move outward in steps, x + 3 * dx
will give you a value 3 cells in the give direction
. From there it is a simple matter of checking if we are in bounds and if the letters spell MAS.
for (int y = 0; y < input.length; y++) {
for (int x = 0; x < input[y].length; x++) {
if (input[y][x] != 'X') continue;
for (int dy = -1; dy <= 1; dy++) {
for (int dx = -1; dx <= 1; dx++) {
if (dy == dx && dx == 0) continue;
if (!(0 <= y + 3 * dy && y + 3 * dy < input.length &&
0 <= x + 3 * dx && x + 3 * dx < input[y].length)) continue;
if (input[y+1*dy][x+1*dx] == 'M' &&
input[y+2*dy][x+2*dx] == 'A' &&
input[y+3*dy][x+3*dx] == 'S') {
matches++;
}
}
}
}
}
For part 2 a similar approach can be used, however the order is not important. So I chose to create a List
and then check against a target list with the containsAll
method, it does not care about order.
var x1 = List.of(input[y-1][x-1], input[y][x], input[y+1][x+1]);
var x2 = List.of(input[y-1][x+1], input[y][x], input[y+1][x-1]);
if (x1.containsAll( target ) && x2.containsAll( target )){
matches++;
}
Another reminder to not over engineer at the start.
[This article will be update with more days]