This post contains inline annotations/footnotes to help add context, helpful tips or expand upon a tangent in the text. Expand them by clicking or tapping on them Annotations might have their own annotationsSuch as this one! inside of them too.

Updated Dec 12, 2022 to remove shorthand argument names in the last reduce to help clarify what’s going on

Welcome back to Advent of Code 2022! I haven’t written about my experience using OCaml for last year’s puzzles yet, but I figured I’d try to get a head start on this year while it’s all fresh in my mind. This year I’ll be trying to solve the puzzles using small swift CLI apps and will be trying to do write-ups of my solutions as I go.

For the most part, I’m not going for speed or cleverness so much as expressiveness and maintainability. I like to try and focus on learning patterns that lend themselves to understanding the code quicker 7 months later and to think about what adding additional features or changing requirements on the code could mean. I’m also not going to be focusing on the superfluous and boilerplate code that wraps and integrates my solutions into a CLI and I’m assuming that you the reader has enough general knowledge to do tasks like reading and splitting an input file by newlines and familiarity with iterators and map/reduce functions.

Since this is the first day, however, let’s take a look at how I’ve got this all set up just for reference. Each day I’ll scaffold a new CLI app using the swift package manager:

swift package init --type executable

Then I’ll add in the Swift Argument Parser package. I’ll use a slightly modified Package.swift as well which I can copy-paste to the next day to hit the ground running without needing to worry about changing anything for each day’s different name:

// swift-tools-version: 5.7

import PackageDescription
import Foundation

// Turns the directory "AOC/2022/day04" into just "day04" for example
let dayName = (FileManager().currentDirectoryPath as NSString).lastPathComponent

let package = Package(
    name: dayName,
    platforms: [.macOS(.v13)],
    dependencies: [
        .package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.0")
    ],
    targets: [
        .executableTarget(
            name: dayName,
            dependencies: [.product(name: "ArgumentParser", package: "swift-argument-parser")]),
        .testTarget(
            name: "\(dayName)Tests",
            dependencies: [.target(name: dayName)]),
    ]
)

And finally I’ll use a boilerplate for the command under Sources/dayXX/dayXX.swift that looks a bit like this:

import Foundation
import OSLog
import ArgumentParser

let dayName = (FileManager().currentDirectoryPath as NSString).lastPathComponent
let dayNumber = Int(dayName.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()) ?? 0

extension Logger {
    static let parsing = Logger(subsystem: "com.aoc22.\(dayName)", category: "Parsing")
    static let solution = Logger(subsystem: "com.aoc22.\(dayName)", category: "Solution")
}

@main
public struct Challenge: ParsableCommand {
    public static var configuration = CommandConfiguration(
        abstract: "Day \(dayNumber)!",

        version: "1.0.0",

        subcommands: [PartOne.self, PartTwo.self]
    )

    public init() {}
}

struct BaseOptions: ParsableArguments {
    @Argument(help: "The file to work upon for the puzzle input")
    var inputFile: String = "input"
}

extension Challenge {
    struct PartOne: ParsableCommand {
        static var configuration = CommandConfiguration(
            abstract: ""
        )

        @OptionGroup var options: BaseOptions

        mutating func run() {
            let parsedInput = Parser().parseFile(options.inputFile)
            let solution = solve(parsedInput)

            print("\(solution)")
        }

        func solve(_ input: [ParsedLine]) -> Int {
            return 0
        }
    }

    struct PartTwo: ParsableCommand {
        static var configuration = CommandConfiguration(
            abstract: ""
        )

        @OptionGroup var options: BaseOptions

        mutating func run() {
            let parsedInput = Parser().parseFile(options.inputFile)
            let solution = solve(parsedInput)

            print("\(solution)")
        }

        func solve(_ input: [ParsedLine]) -> Int {
            return 0
        }
    }
}

typealias ParsedLine = String// TODO: Fill me in

struct Parser {
    fileprivate func parseLine(_ line: Substring) -> ParsedLine {
        // TODO: Fill me in
        return String(line)
    }

    public func parseString(_ input: String) -> [ParsedLine] {
        return input
            .split(separator: "\n")
            .map(parseLine)
    }

    public func parseFile(_ filename: String) -> [ParsedLine] {
        let url = URL(fileURLWithPath: filename)

        guard let contents = try? String(contentsOf: url, encoding: .utf8) else {
            fatalError("Couldn't parse the file!")
        }

        return parseString(contents)
    }
}

This gives me a quick and ready structure, leaving me with the details of how a line from the input is parsed and what that resulting container looks like, letting me focus on writing a solution by filling in parseLine() and the two solve()s without fussing with the setup. I’ve got a few other patterns setup in here too, such as the input always being under at ./input and some logging for debugging purposes, which I found to be extremely helpful last year. I’ve gone a bit further too and have a small CLI that downloads the days input automatically: aoc-utils download after a puzzle is released and ./input is set up and ready to go as well!

The last part to all of this is a set of tests. I like to start off with tests against the example input and the known solution to those samples as well as tests against my final solution so that I can refactor my code and ensure the solution stays the same.

import XCTest
@testable import dayXX

let testInput = """
"""

final class dayXXTests: XCTestCase {
    func testPartOneExample() throws {
        let parsedOutput = Parser().parseString(testInput)
        let solution = Challenge.PartOne().solve(parsedOutput)

        XCTAssertEqual(solution, 1)
    }

    func testPartTwoExample() throws {
        let parsedOutput = Parser().parseString(testInput)
        let solution = Challenge.PartTwo().solve(parsedOutput)

        XCTAssertEqual(solution, 2)
    }

    func testPartOneRealInput() throws {
        let parsedOutput = Parser().parseFile("input")
        let solution = Challenge.PartOne().solve(parsedOutput)

        XCTAssertEqual(solution, 3)
    }

    func testPartTwoRealInput() throws {
        let parsedOutput = Parser().parseFile("input")
        let solution = Challenge.PartTwo().solve(parsedOutput)

        XCTAssertEqual(solution, 4)
    }
}

Now, once everything is in place and I’ve got a solution setup I can run swift test and make sure my solution is correct, and swift run dayXX part-one / swift run dayXX part-two to get my solution to put in on the site.

With that all in place, let’s dive into the first puzzle which’ll give us a nice warm up for the season!

Part One

Find the Elf carrying the most Calories. How many total Calories are that Elf carrying?

We’ll take a fairly common and slightly naive approach today. There is a way to solve this without building an array of every elf’s calories, instead you just keep track of the highest seen calorie count and the calorie count for the current elf, but that “clever” solution isn’t what I’m after and doesn’t lend itself to showing off some of the language features around iterators.

Looking at our input, we can see each elf’s calorie list is separated by a blank line, and each elf has one or more lines of integer numbers that’ll we will need to sum up in order to find that elf’s total number of calories carried. We can start by splitting the input into an array of strings In Swift’s type annotations: [String], although in reality what we’ll get is actually a [Substring] where Substring is actually a reference to the slice of our original string which contains our split out components. A Substring is intended to be short-lived, so we normally wouldn’t want to pass it around without copying the contents to a separate String however today we’ll be fairly quickly converting the Substring into an Int so it shouldn’t matter too much. . From there we should be able to iterate over this array, and split each string into even smaller strings representing each item’s calories. From there it’ll be a matter of converting the stringified numbers to Swift Ints and summing them up.

Assuming input is our string representing our puzzle’s … well, input, we can use the split(separator:) function to split on two newlines (\n\n):

input.split(separator: "\n\n")

Now that we’ve got each elf’s calories separated out we can focus on a single elf at a time. Again we’ll need to split, but only on a single new line this time, and then we’ll convert each element in that split to an integer and sum them up. Swift’s iterators make this fairly easy to do in one go using reduce(_:_:):

func parseElf(_ rawLinesOfCalories: Substring) -> Int {
    return rawLinesOfCalories
        .split(separator: "\n")
        .reduce(0) { memo, caloriesLine in memo + (Int(caloriesLine) ?? 0) }
}

The biggest thing with this process is that parsing our string into an integer with Int(_: String) -> Int? returns an Optional. We could, for the sake of this challenge since we know the input is well formed, force unwrap it with Int(caloriesLine)! but we could also provide a fallback just in case, using the nil-coalescing operator ??. Note that we’re putting this parsing into a separate function as it’ll make the next bit a little easier.

Next, we can map over our split up input, calling this new paseElf(_:) function and have an array that’ll give us the answer to both parts. I’m going to go a step further and shove them into a small container to make debugging easier and to get our first taste of typealias:

typealias ElfToCalories = (elf: Int, calories: Int)

func parseString(_ input: String) -> [ElfToCalories] {
    return input.split(separator: "\n\n")
        .map(parseElf)
        .enumerated()
        .map { (index, calories) in
            let elfsTuple = (elf: index + 1, calories: calories)

            Logger.parsing.debug("Elf \(elfsTuple.elf): \(elfsTuple.calories)")

            return elfsTuple
        }
}

Breaking this down a little bit, we can see that we’re defining a typealias for a named-element tuple containing the elf’s number in our input (we’ll 1-index this to make it easier to debug against the problems example) and their calories. Next we do our existing split on the double new lines and we map each element into an integer. At the point that we call .enumerated(), we’ll have an array of integers ([Int]). .enumerated() is the magic that lets us get the elf’s index within the array, it transforms our array of integers into an array of tuples, where the first element of each tuple is the index and the second is the original value:

let elfCalories = [50, 42, 38, 9]
elfCalories.enumerated() // Equivalent to: [(0, 50), (1, 42), (2, 38), (3, 9)]

Finally we map over this one last time to convert our 0-indexed into 1-indexing and our ElfToCalories tuple and to log out some helpful messages. Finally we’ve gotten our input parsed into a helpful data structure and we’re able to get on with the challenge. We need to find the elf that is carrying the most amount of calories, which we can do using max(by:). This returns an optional but we know that we’ll have a max so we’ll take the shortcut and force unwrap it. Since we’re working with our named-element tuple, we can pull out the calories to get our solution:

func solve(_ input: [ElfToCalories]) -> Int {
    return input
        .max { a, b in a.calories < b.calories }!
        .calories
}

Part Two

Find the top three Elves carrying the most Calories. How many Calories are those Elves carrying in total?

We can stick with the same parsing for part one but instead of using max(by:), we’ll have to find three elves and add their calories together. Thankfully Swift has sorted(by:) and prefix(_:) which we can use to first sort our array so that the highest calorie elves are at the front, and then we can take the first 3 elves. From there we can use reduce(_:_:) again to sum up their combined calories for our solution!

func solve(_ input: [ElfToCalories]) -> Int {
    return input
        .sorted { a, b in a.calories > b.calories }
        .prefix(3)
        .reduce(0) { memo, elf in memo + elf.calories }
}

And that’s it, we’ve survived day 1, see you tomorrow!