Advent of Code 2023 - Day 01

By Eric Burden | December 1, 2023

It’s that time of year again! Just like last year, I’ll be posting my solutions to the Advent of Code puzzles. This year, I’ll be solving the puzzles in Kotlin. I’ll post my solutions and code to GitHub as well. If you haven’t given AoC a try, I encourage you to do so along with me!

Day 1 - Trebuchet?!

Find the problem description HERE.

The Input - Numbers Every Which Way

Well, well, well… Here we are again. Another year, another catastrophic threat to Christmas cheer. Thankfully, we’ve advanced to trebuchet technology to tackle this looming crisis. It looks like this year we’ll be working to restore snow production, which will hopefully result in a few snow days! What’s that? Remote work is a thing now? Ah well, at least snow is pretty…

Our input for today is the “calibration document” for the trebuchet we’re apparently about to be fired out of (no snow days doesn’t seem like such a big sacrifice now, huh?). We need to read in a list of strings from a text file, extract all the numbers, then use the first and last number to calculate the “calibration value” on each line. Because (spoiler alert!) the parsing strategy changes a bit from part 1 to part 2, I’ll address each one individually. Instead, I’ll use this section to share my “read the input file” code I cleverly prepared ahead of time this year, and that I hope will continue to be handy.

import java.io.File
import java.net.URI

internal object Resources {
  private fun String.toURI(): URI =
      Resources.javaClass.classLoader.getResource(this)?.toURI()
          ?: throw IllegalArgumentException("Cannot find Resource: $this")
          
  fun resourceAsText(fileName: String): String = File(fileName.toURI()).readText()

  fun resourceAsLines(fileName: String): List<String> = File(fileName.toURI()).readLines()

  fun resourceAsLineChunks(fileName: String, delimiter: String = "\n\n"): List<List<String>> =
      resourceAsText(fileName).split(delimiter).map { it.lines().takeWhile { !it.isEmpty() }.toList() }

  fun resourceAsString(fileName: String, delimiter: String = ""): String =
      resourceAsLines(fileName).reduce { a, b -> "$a$delimiter$b" }

  fun resourceAsListOfInt(fileName: String): List<Int> =
      resourceAsLines(fileName).map { it.toInt() }

  fun resourceAsListOfLong(fileName: String): List<Long> =
      resourceAsLines(fileName).map { it.toLong() }
}

These aren’t terribly complicated, but they’re general enough to be useful almost every day, I suspect. We can read the input file as a single string, as a list of lines (strings), as a list of line chunks (with a delimiter), as a list of strings from a single line (with optional delimiter), as a list of integers (one per line), and as a list of longs (again, one per line). There’s a good chance I’ll come up with others over the course of the month (I’m already thinking I’d like a resourceAsParsedLines that takes a parsing function as a second argument), but as long as I get to re-use some of these, it will have been worth it.

Part One - Artistic Expression

  • Bad news: You’re being hoisted into a trebuchet to be launched into the sky.
  • Good news: The elves can’t calibrate their siege weapon! We’re saved!

Wait, did we volunteer to decipher the calibration document to help the elves propel us into the stratosphere? Guess we’d better get to it, then.

/**
 * Extracts all digit characters from a string as numbers.
 *
 * @return A list of all digits in the input String as Ints.
 */
private fun String.extractDigitsPart1(): List<Int> {
    // Take the string, convert to a list of characters, filter to
    // only characters that are digits, then convert those digit
    // characters to integers.
    return this.toList().filter(Char::isDigit).map(Char::digitToInt)
}

/**
 * Derive the calibration value from a list of numbers
 *
 * This function "concatenates" the first digit in a list of numbers and the 
 * last digit in a list of
 * numbers into a single two-digit number.
 *
 * @return The calibration value extracted from a list of numbers.
 */
private fun List<Int>.toCalibrationValue(): Int {
    // This works because the 'numbers' are all single digits. So, it's
    // the ten's place * 10 plus the one's place (times 1, technically).
    return (this.first() * 10) + this.last()
}

class Day01(input: List<String>) {

    private val input = input

    // In part one, the valid numbers in the string are all digits.
    // Take the input, extract all the digits from each line, then 
    // sum the calibration values of each line.
    fun solvePart1(): Int = input
        .map(String::extractDigitsPart1)
        .sumOf { it.toCalibrationValue() }
}

Part Two - Burning Out My Fuse Up Here Alone

I feel like we’re being a bit too accommodating when it comes to helping the elves in this endeavor. I can’t help but think that, if the trebuchet weren’t an option, we could borrow some sort of flying vehicle (perhaps reindeer-propelled) to make this trip. Probably with fewer life-threatening injuries, which would be a big plus. Must just be me. Anyway, part two asks us to parse words that represent numbers (like ‘one’ or ‘fiveight’) as well as digits from the lines of the input.

/**
 * Extract all digits from a string as numbers
 *
 * This function also extracts digits in "word" form, such as "one" or "two". 
 * Note, this function will also extract number-words whose letters overlap, 
 * such as "twoone" or "fiveight".
 *
 * @return A list of all digits in the input String as Ints.
 */
private fun String.extractDigitsPart2(): List<Int> {
    val pattern = """(?=(one|two|three|four|five|six|seven|eight|nine|\d))"""

    // Because we have to account for overlaps, I'm using the "non-consuming" regular
    // expression above. This does weird things (IMO) to the match values, causing
    // `findAll()` to return a list of values for each match where the first value
    // is an empty string. Probably just means I need to learn more about regex.
    // Regardless, the result is that I need to flatten the results and remove empty
    // strings before converting the matches to numbers.
    return Regex(pattern)
            .findAll(this)
            .flatMap { it.groupValues }
            .filter(String::isNotBlank)
            .map(String::toNumberUnchecked)
            .toList()
}

/**
 * Convert a String to an Int
 *
 * This function will numeric strings _and_ words that represent digits into
 * integers.
 *
 * @return The Int representation of a String
 */
private fun String.toNumberUnchecked(): Int {
    // I wouldn't recommend using this in production anywhere, since we're
    // not actually checking to see whether any numeric strings are
    // (a) valid numbers or (b) single digits. Just gotta count on the
    // puzzle input for this one.
    return when (this) {
        "one" -> 1
        "two" -> 2
        "three" -> 3
        "four" -> 4
        "five" -> 5
        "six" -> 6
        "seven" -> 7
        "eight" -> 8
        "nine" -> 9
        else -> toInt()
    }
}

class Day01(input: List<String>) {

    private val input = input

    // In part one, the valid numbers in the string are all digits
    fun solvePart1(): Int = input
        .map(String::extractDigitsPart1)
        .sumOf { it.toCalibrationValue() }

    // In part two, we also have to account for "number-words" like "one" 
    // and "twone"
    fun solvePart2(): Int = input
        .map(String::extractDigitsPart2)
        .sumOf { it.toCalibrationValue() }
}

I may or may not have submitted an incorrect answer for part two, re-read the example input (suspiciously a different example than part one), called Eric Wastl an unkind name under my breath, then proceeded to figure out how to parse ’twone’ into [2, 1].

Wrap Up

Another year, another set of coding puzzles! From a historical perspective, Day 1 this represents a bit of a bump in difficulty level from previous years. I don’t know if this means we’re in for a more challenging year overall or if this is a deliberate attempt to keep the LLMs at bay. Either way, I know I spent more time on a Day 1 puzzle today than I have in the past.

This year I’m picking up a complete new (to me) language in Kotlin. So far, the biggest challenge has been figuring out enough about how Maven works to get my project setup in place, but that’s probably a me issue. I’m really excited to learn a lot this year and looking forward to giving Kotlin a workout.

Finally, if you enjoy Advent of Code as much as I do, consider helping out with this completely free (to use, not to run) educational and community event. And, if you’re not in a position to support financially (or even if you are), you can support the community over on the r/adventofcode with code reviews, advice, visualizations, or hilarious memes. Happy Holidays!

comments powered by Disqus