Code Review Videos > Kotlin > Advent of Code 2023: Day 1 Part 1 – Calibrating Snow Operations

Advent of Code 2023: Day 1 Part 1 – Calibrating Snow Operations

I’ve never yet taken part in the Advent of Code, and judging by how much time it took me to solve both parts of Day 1, it’s debatable as to whether I will have completed all challenges by Christmas Eve.

But it’s an excuse to test out my rather rudimentary Kotlin skills, and share what I learn along the way.

Caution: This post contains spoilers.

Puzzle Description

The puzzle opens with a Christmas-y narrative about the malfunctioning global snow production and the Elves’ unconventional solution – a trebuchet. However, things take an unexpected turn when it’s revealed that a young Elf, in an attempt to showcase artistic skills, has amended the calibration document.

Our mission?

Recover the calibration values from each line and determine the sum of all these values.

Understanding the Problem

The newly-improved calibration document is a collection of lines, each containing a calibration value.

To extract this value, we need to combine the first and last digits of each line, forming a two-digit number.

The provided example illustrates this process:

1abc2
pqr3stu8vwx
a1b2c3d4e5f
treb7uchet

The calibration values for these lines are 12, 38, 15, and 77 respectively, resulting in a sum of 142.

Our goal is to calculate the sum of all calibration values for the entire document.

You can find the instructions in full by clicking here.

Writing Test Cases

There are no formal unit tests provided for Advent of Code challenges, as you can complete the tasks in any programming language… or I guess, even manually, if you are so inclined.

As such I had to set up my own Kotlin project, and then create my own unit tests.

Setting up a new Kotlin project is pretty easy – just follow the instructions provided by JetBrains.

After that, I created the following unit tests:

// src/test/kotlin/com/codereviewvideos/aoc23/day1a/CalibratorTest.kt

package com.codereviewvideos.aoc23.day1a

import org.junit.jupiter.api.Test
import java.io.File
import kotlin.test.assertEquals

class CalibratorTest {
  @Test
  fun `1abc2 returns 12`() {
    assertEquals(12, Calibrator.calibrate("1abc2"))
  }

  @Test
  fun `pqr3stu8vwx returns 38`() {
    assertEquals(38, Calibrator.calibrate("pqr3stu8vwx"))
  }

  @Test
  fun `a1b2c3d4e5f returns 15`() {
    assertEquals(15, Calibrator.calibrate("a1b2c3d4e5f"))
  }

  @Test
  fun `treb7uchet returns 77`() {
    assertEquals(77, Calibrator.calibrate("treb7uchet"))
  }
}Code language: Kotlin (kotlin)

With those tests in place, I went to work.

The Initial Implementation

I have to admit, I didn’t have an initial idea of an immediate solution in mind.

My first thought went to some kind of iteration. Maybe regex, if I hit a wall.

Typically with these code puzzles I find whatever monstrosity I come up with, some uber boffin will have found a saucy little one liner that neatly solves the puzzle, and simultaneously makes me feel like an abject failure.

Anyway, by sheer good fortune, it turns out that Kotlin provides some methods that work on strings that very conveniently give us an immediate solution:

package com.codereviewvideos.aoc23.day1a

class Calibrator {
  companion object {
    fun calibrate(s: String): Int {
      val first = s.first { it.isDigit() }
      val last = s.last { it.isDigit() }
      return (first.toString() + last).toInt()
    }
  }
}Code language: Kotlin (kotlin)

I actually didn’t come up with the idea of using a companion object.

This was what IntelliJ provided when I alt + clicked on the test method – Calibrator.calibrate("1abc2") – which, at the time, didn’t exist.

The companion object was the auto-generated code it created.

In hindsight that is probably because I didn’t tell the tests to create a new instance of my class at any point. Knowing a bit more about Java now than I did when I first tried Kotlin, I now know this is akin to a static class.

OK, onwards.

kotlin last docs

Originally I’d gone with first – just looking at the very first character in the string, and was looking for a way to find if it was a number.

Very fortunately, Kotlin provides two variations of first and its friend, last.

If you take the simple method called of yourString.first() or "a string".last(), you get the first / last character. There’s a conversion here. From a string, to a character. That’s kinda unsettling coming from a language like JavaScript, but it’s happening and you need to be aware of it.

However, there’s a second form of first / last. A more interesting form.

<code>val first = s.first { it.isDigit() }</code>Code language: Kotlin (kotlin)

This expression finds the first character in the string s that is a digit. The it.isDigit() is a predicate that checks if a character is a digit.

You could actually re-write this to be:

val first = s.first({ it -> it.isDigit() })Code language: Kotlin (kotlin)

Which is valid code, and perhaps easier to understand. But it isn’t idiomatic Kotlin. If at all interested in this syntax, I covered it in more detail in this post, under the Trailing Lambda section.

The really nice thing about this function is it finds the first digit without us needing to write any further code. It checks character by character, starting from the first (or last) character, and works its way through the whole string until it finds a match.

This really neatly solved the problem.

The only quirk I had to address is how to return both digits, concatenated, without adding them:

(first.toString() + last).toInt()Code language: Kotlin (kotlin)

Quirky, but after a bunch of attempts using template strings, this was the final suggested representation by IntelliJ.

OK, so problem solved. At least for a single line.

Handling Multiple Lines

The next part of this challenge is that we need to handle multiple lines of input.

The initial documentation gives us four example lines.

I decided to craft my own test case.


  @Test
  fun `calibrate multiple lines returns a sum of all values`() {
    val input =
        """
      1ten0
      te10n
      """
            .trimIndent()
    assertEquals(20, Calibrator.calibrate(input))
  }Code language: Kotlin (kotlin)

The challenge here is that we need to process more than one line. We already have the logic for one line.

How to solve for multiple?

package com.codereviewvideos.aoc23.day1a

class Calibrator {
  companion object {
    fun calibrate(s: String): Int {
      val split = s.split("\n")
      return split.map { calibrateLine(it) }.reduce { acc, value -> acc + value }
    }

    private fun calibrateLine(s: String): Int {
      val first = s.first { it.isDigit() }
      val last = s.last { it.isDigit() }
      return (first.toString() + last).toInt()
    }
  }
}
Code language: Kotlin (kotlin)

There may be a better way to do this. I wouldn’t be surprised.

What I ended up doing was keeping the calibrate method, but moving the logic I had for one line to be a private function. You can’t directly call calibrateLine, but you can call calibrate with one or more lines. An easier API for the caller, whilst hiding away the complexity.

After this I did the usual thing I do with working on multiple lines of input.

Split the string, run a map over the resulting array of individual strings, and then in this case, reduce the resulting list of Int values down to a single number.

That works fine, but I figured there must be a sum method on a list of numbers. And there is:

return split.map { calibrateLine(it) }.sum()Code language: Kotlin (kotlin)

Which also works, and is much neater.

IntelliJ then prompted me to go one step further:

kotlin sumof list of ints

Such that the final code I came up with was:

package com.codereviewvideos.aoc23.day1a

class Calibrator {
  companion object {
    fun calibrate(s: String): Int {
      val split = s.split("\n")
      return split.sumOf { calibrateLine(it) }
    }

    private fun calibrateLine(s: String): Int {
      val first = s.first { it.isDigit() }
      val last = s.last { it.isDigit() }
      return (first.toString() + last).toInt()
    }
  }
}Code language: Kotlin (kotlin)

With a working implementation, I could now get the answer to the first part of the Advent of Code Day 1 puzzle.

Getting The Answer

In order to get the answer, I needed a way to run the full input through my implementation.

I wondered if I ought to create some kind of HTTP GET against the text document. But then I figured why not just copy / paste the data to my project?

What I ended up doing was pasting the data in to a file at src/main/resources/input.txt

Then I needed to figure out how to load a file from the resources directory in Kotlin. That took a bit of Stack Overflowing:

  @Test
  fun `calibrate entire input file contents returns the puzzle answer`() {
    // Get the file path within the resources directory
    val filePath = object {}.javaClass.classLoader.getResource("input.txt")?.file

    // Check if the file path is not null
    if (filePath != null) {
      val file = File(filePath)

      // Read the contents of the file
      val content = file.readText()

      // Print or use the file content as needed
      assertEquals(53386, Calibrator.calibrate(content))
    }
  }
Code language: Kotlin (kotlin)

The most curious line is surely the one highlighted.

For your and my benefit, here’s what ChatGPT had to say about what the heck is happening here:

  1. object {}: This is an anonymous object declaration in Kotlin. The empty curly braces {} define the body of the object. In this case, it’s an empty object used for accessing its class.
  2. .javaClass: The javaClass property returns the runtime class of the object. It is equivalent to calling this.getClass() in Java.
  3. .classLoader: The classLoader property retrieves the class loader for the class. The class loader is responsible for loading classes during runtime.
  4. .getResource("input.txt"): This method is used to retrieve a URL representing the specified resource. In this case, it’s looking for a resource named “input.txt” in the classpath.
  5. ?.file: The safe call operator ?. is used to safely invoke the file property on the URL obtained from getResource("input.txt"). If the URL is null (e.g., if the resource is not found), the entire expression evaluates to null. If the URL is not null, it retrieves the file component of the URL.
  6. val filePath = ...: The result of the entire expression is assigned to the variable filePath. This variable will either hold the file path as a string or be null if the resource is not found.

Frankly I think this is way more complex than it ought to be for what must surely be a fairly common task.

I didn’t actually know the answer would be 53386.

I ran the test with a random number in there, then updated the test when it failed, kindly spitting out that number as the “failing” value.

Anyway that gained me my first ever Gold Star on an Advent of Code challenge. Hoorah.

Pasting the answer in immediately gave me access to Part Two of the puzzle… which was quite a bit harder, and proved my initial implementation wasn’t sufficient to cover the next set of requirements.

But that’s for another post.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.