Advent of Code 2024, insights from the first week
It’s been 10 years since I started enjoying Advent of Code, a series of daily coding exercises in December. Over this time, I’ve learned a lot, and I firmly believe that practice makes you a better developer. Since the time required to solve these exercises is still manageable, I’d like to share a wrap-up of the first week—mostly because I probably won’t have time to do it later.
In this post, we will explore practical coding insights and demonstrate how Scala’s powerful features can simplify problem-solving. We’ll also highlight how reusable code can save time. The post will showcase examples and tools that you can incorporate into your own work.
Day1
This was a perfect opportunity to use zip
and unzip
.
scala> List((1,2), (3,4), (5,6)).unzip
val res0: (List[Int], List[Int]) = (List(1, 3, 5),List(2, 4, 6))
scala> pair._1.zip(pair._2)
val res2: List[(Int, Int)] = List((1,2), (3,4), (5,6))
With these built-in methods, there’s no need to loop explicitly over collections, and the intent is clearer.
Similarly, to count occurrences in a list, verbose code like this:
scala> List(1,2,1,1,2).groupBy(identity).view.mapValues(_.length).toMap
val res1: scala.collection.immutable.Map[Int,Int] = Map(1 -> 3, 2 -> 2)
can be simplified using groupMapReduce
:
scala> List(1,2,1,1,2).groupMapReduce(identity)(_=>1)(_+_)
val res4: scala.collection.immutable.Map[Int,Int] = Map(1 -> 3, 2 -> 2)
This simple exercise reveals how much the Scala collection library shines !
Day2
Once again, sliding windows proved useful for grouping elements in pairs, making it easy to handle iteration logic involving the current element and its predecessor.
scala> List(1,2,3,4).sliding(2).foreach(println)
List(1, 2)
List(2, 3)
List(3, 4)
Another handy method was patch
, which, despite its somewhat unintuitive name, is effective for removing elements from a collection.
scala> List(1,2,3,4).patch(2, Nil, 1)
val res6: List[Int] = List(1, 2, 4)
Day3
This day was a tough reminder that I’m still too slow with regular expressions
(I never seem to remember how escaping works with triple-quote syntax in Scala).
I also had to refresh my memory on the non-greedy syntax .*?
.
Day5
String interpolation continues to be a convenient way to destructure strings:
case s"${fst}|${snd}" => (fst.toInt, snd.toInt)
I wasted a lot of time due to a silly mistake: using i
instead of arr(i)
. Since both are integers, the compiler didn’t catch it.
For part two, I initially reimplemented a bubble sort before realizing I could just use the built-in sortWith
.
Ultimately, the task boiled down to writing code to compare two elements:
private def lt(orderingRules: Map[Int, Set[Int]]): (Int, Int) => Boolean = { (a, b) =>
orderingRules.get(a).exists(_.contains(b))
}
Day 6
Over the years, I’ve extracted reusable code for Advent of Code. This time, I reused a grid implementation, which was helpful. However, the design wasn’t great, and I lost some time figuring out how it worked.
The upside? It worked on the first try, avoiding common coding mistakes like off-by-one errors.
Day7
This was the day where a 32-bit integer overflow occurred. Fortunately, it happened during parsing, and the error message made it clear:
[error] java.lang.NumberFormatException: For input string: "634495747544"
I also reused a combinatorics method that I had extracted in previous years. Honestly, it’s satisfying to see how clear and straightforward the code can be in Scala:
object Day7 extends MultiPuzzle[Long, Long] {
val opsPart1: List[(Long, Long) => Long] = List(_ + _, _ * _)
val opsPart2: List[(Long, Long) => Long] = List(_ + _, _ * _, (a, b) => s"$a$b".toLong)
override def part1(input: Iterator[String]): Long = common(input, opsPart1)
override def part2(input: Iterator[String]): Long = common(input, opsPart2)
private def common(input: Iterator[String], ops: List[(Long, Long) => Long]) =
input.map { s =>
val numbers = s.split("[: ]+").map(_.toLong)
val candidates = Combinatorics.permutationsWithReplacement(ops, numbers.length - 2)
val equationCouldBeTrue = candidates.exists { candidate =>
val (expected :: fst :: tail) = numbers.toList
val actual = tail.zip(candidate).foldLeft(fst) { case (acc, (cur, op)) => op(acc, cur) }
actual == expected
}
if (equationCouldBeTrue) numbers.head else 0
}.sum
}
If you’re interested, you can find all my solutions on Github.