Skip to Content

Advent of Code 2023 - Day 1, in Kotlin - Trebuchet?!

Kotlin solutions to parts 1 and 2 of Advent of Code 2023, Day 1: 'Trebuchet?!'

Posted on

Hello again! Another December 1st means the start of a new season of Advent of Code! I look forward to this every year because I have so much fun solving the puzzle and comparing my solutions to those of my friends, and a bunch of random strangers on the internet.

For the past six years I have made a serious effort to solve the puzzle and blog about it on the day it was revealed. However, because of the significant demand on my time that takes, I’ve decided to let myself relax that stance a bit this year. I will try my best to solve and blog each puzzle each day. However, given that I have just started a new job and have several personal obligations during the month of December, I am going to just give a best-effort approach. So we’ll see how well I do. I am going to try and embrace the concept of Eventual Consistency and get these blog posts done eventually, ideally not too far after the puzzle is revealed. Thanks for understanding.

As in past years, I will put my solutions in a GitHub Repository .

Let’s get started!

JetBrains Live Stream

I was fortunate enough to be invited to the JetBrains Live Stream today to go over my solution to today’s puzzle. Check it out, and be sure to check it out for the first 12 days of the Advent of Code. I’ve seen the secret guest list and they’ve got some great folks lined up to solve these puzzles on-air. I think you’ll really enjoy yourself.

Also, JetBrains are running an Advent of Code contest with some nice prizes. Details are in the video as well.

Puzzle Input

Each day we’ll have a class named after the day number (Day01 in our case today) which will have an input parameter that represents the puzzle input. In our case, this input comes from the unit test that runs the actual puzzle and tests the solution for correctness. The type of input may change from day to day. Today, for example, it is a List<String> representing the entire puzzle input. Other days it might be a String or List<Int>, depending on how we’ll use it when writing the solution. The work of loading the daily input file and possibly turning it into a List is done by a helper class class called Resources . which I’ve used in the past.

Let’s set up our Day01 class, taking the puzzle input as a List<String>, which we will make a private property that we can use througout our class. We can take this input as-is since we don’t need to do any additional processing on it.

class Day01(private val input: List<String>) {

}

⭐ Day 1, Part 1

The puzzle text can be found here.

Part 1 asks us to find some integers in each row of input, combine them, and sum them all together. For this, I’ve decided to use first and last from the Kotlin Standard Library which will take a lambda and (in our case) return the first or last digit in each row of input. Since these are Characters, we will need to concatenate them before turning them into an Int via toInt. To concatenate, I’ve chosen to make the whole expression a String. An alternative approach would have been to convert each Character to an Int, multiply the first one by 10, and add them together.

// In Day01

private fun calibrationValue(row: String): Int =
    "${row.first { it.isDigit() }}${row.last { it.isDigit() }}".toInt()

Next, we use sumOf over the input to add all of the calibration values together.

// In Day01

fun solvePart1(): Int =
    input.sumOf { calibrationValue(it) }

Star earned! Onward!

⭐ Day 1, Part 2

The puzzle text can be found here.

Part 2 might seem tricky on the surface, but we’ll work through it and hopefully come up with a good solution. The first thing we’ll need to do is make a Map<String, Int> to store the mapping from a word to its value. We could have made the map values Characters, but I kept them as Int because I liked the way it highlighted in my editor. :)

// In Day01
private val words: Map<String, Int> = mapOf(
    "one" to 1,
    "two" to 2,
    "three" to 3,
    "four" to 4,
    "five" to 5,
    "six" to 6,
    "seven" to 7,
    "eight" to 8,
    "nine" to 9
)

Before we use our map let’s stop and think. Given an input row, we really only care when an individual Character is a digit, or is the start of a word that could spell a digit. Any other letter of any kind can be ignored.

To accomplish this, we’ll use sumOf over each row of input after we calculate the calibrationValue just like in Part 1. This time we’ll need to modify the row so that the call to calibrationValue works properly. For this, we’ll look at each Character of the row using mapIndexedNotNull which gives us each Character of a String and its index value.

We’ll look at each Character and if it is a digit, we just accept it into the eventual String we end up creating. No more work to do in that case. The next case is more interesting! We will call our possibleWordsAt function (see below) in order to get all of the Strings starting at this position in the row (via the index) that could be a word representing a digit. Once we have the List<String> that represents possible number-words, we loop through them and see if any of them are in the words map. If they are, the call to words[candidate] will return a non-null value, which we’ll add to our String. If it returns a null (the value is not in the map), we can keep looping. This shows off my favorite Kotlin Standard Library function - firstNotNullOfOrNull, which roughly translates to “run this lambda over the input until you get a non-null value and then stop, and if you don’t ever find a value, return null”. So for example, if one word returns “two”, we’ll get a 2 from the words map and add it to our String.

Since we end that call with a List<String>, we will join them into a single String via joinToString.

// In Day01
fun solvePart2(): Int =
    input.sumOf { row ->
       calibrationValue(
            row.mapIndexedNotNull { index, c ->
                if (c.isDigit()) c
                else
                    row.possibleWordsAt(index).firstNotNullOfOrNull { candidate ->
                        words[candidate]
                    }
            }.joinToString()
        )
    }

All that’s left is to write our possibleWordsAt function. I decided to write this as an extension function, but it doesn’t really have to be if you prefer not to. Either way, we only need to consider the next 3 to 5 characters because the words are at least 3 characters long and at most 5. We loop over the range of 3 to 5 and take a substring starting at the startingAt point, and extending at most to the end of the String. To make sure we don’t ask for a substring larger than we can get, we call coerceAtMost from the Kotlin Standard Library. Note that there is a corresponding coerceAtLeast which does the opposite.

// In Day01

private fun String.possibleWordsAt(startingAt: Int): List<String> =
    (3..5).map { len ->
        substring(startingAt, (startingAt + len).coerceAtMost(length))
    }

Calling solvePart2() should now give us the correct answer.

Star earned! See you tomorrow (hopefully)!

Further Reading

  1. Index of All Solutions - All posts and solutions for 2023, in Kotlin.
  2. My Github repo - Solutions and tests for each day.
  3. Solution - Full code for day 1
  4. Advent of Code - Come join in and do these challenges yourself!