See https://adventofcode.com/2024
I've been doing Java on and off for 25 years (!). My goal with this year's AoC is to write the solutions in "modern Java" as much as possible. This means using the newer APIs, streams, lambdas, functional programming idioms, records, etc.
The input files in src/main/resources
are encrypted. If you're not me, you'll need
to download the relevant input files from the AoC website and replace the encrypted ones
in that directory.
You can run the Part* classes within IntelliJ interactively.
To run using the command line, install maven, then run mvn package appassembler:assemble
to build the application.
Run the main program, supplying the day and part you want to run as arguments:
# runs day 2, part 1
target/appassembler/bin/aoc2024 2 1
# runs day 3, both parts
target/appassembler/bin/aoc2024 3
# run everything
target/appassembler/bin/aoc2024 all
So far I've been making good use of the functional programming features in Java. But as the problems get more complicated (I'm at day 6), it's starting to get difficult to achieve purity. Some pain points:
-
Records are great, but they are not as full-featured as Scala's case classes. I miss methods that help with derived record creation.
-
Performant FP requires not just streams and lambdas, but also functional data structures. e.g. efficient immutable lists that you can transform into another with an added or removed element. Using the standard structures like ArrayLists and HashMaps without mutating them ends up looking super awkward in the code and does a lot of copying in memory.
-
Some conveniences in streams are missing, like "foldLeft" and "mapWithIndex".
In the course of being a bit irritated by trying to do pure FP in Java, I discovered vavr, a library of functional data structures that seems largely inspired by Scala. People have strong opinions about it, both pro and con. As a concept, it looks pretty fantastic to me. I don't think I'll use it for AoC, at least not this time around, but it's a nice discovery.
I almost always have to rewrite/refactor the part 1 solution to answer part 2 of puzzles. For day 10, however, I had already written code to find all the unique trails for part 1, so I literally didn't need to do anything except count them for part 2. I'll take that win!
Day 11 part 2 is the first problem I had to sleep on. Initially I thought it was one of those where you don't actually need to iterate, you could pre-calculate the resulting number of stones at a given evolution. I'm still not sure if that could actually work. In the end, I simply figured out a more efficient representation for iteration. It worked nicely.
I've been dreading the type of problem that came up in Day 13, where the only viable solution isn't based on time- or spacing-saving algorithms, but on calculation. I won't spoil it here, but I'll just say it took me a little while to write out the equation.
I often declare records as members of the class when 1) they're only used by that class, 2) there's a bunch of them that are related and work together, 3) it's nice to see them all in one place instead of in separate files. Today I randomly discovered local classes and records. This allows you to scope, say, an intermediate record for a stream transformation, to a single method and avoid clutter at the class level. Pretty cool!