Day 2/2021 - Dive!

Day two of AoC 2021 (available at https://adventofcode.com/2021/day/2) has us piloting the submarine. Time to dive deeper into the ocean to find those sleigh keys.

The input is a series of commands: forward 5, down 3, up 2. We need to track position and depth.

Parsing the Input

My first instinct when I see structured text: regex. But my rule is never write regexes in code — always develop them in a tool like regex101, then paste them in.

type Direction = "forward" | "down" | "up";
type Instruction = [Direction, number];

const processInput = (day: number): Instruction[] => {
    const lines = readInputLines(day);
    const regex = /^(forward|down|up) (\d+)$/;
    
    return lines.map(line => {
        const match = line.match(regex);
        return [match![1] as Direction, Number(match![2])];
    });
};

The Direction type is a literal union — TypeScript knows a direction can only be one of three specific strings. The Instruction tuple pairs a direction with a number. This modeling pays off later.

Part One: Simple Navigation

  • forward X increases horizontal position by X
  • down X increases depth by X (we're underwater, positive is down)
  • up X decreases depth by X
const partOne = (input: Instruction[], debug: boolean) => {
    let position = 0;
    let depth = 0;
    
    for (const [direction, value] of input) {
        if (direction === "forward") {
            position += value;
        } else if (direction === "down") {
            depth += value;
        } else if (direction === "up") {
            depth -= value;
        }
    }
    
    if (depth < 0) {
        throw new Error("The submarine is flying!");
    }
    
    return position * depth;
};

The flying submarine check is a sanity assertion. Submarines aren't equipped for aviation — if we go above water, something's wrong with our logic.

GitHub Copilot was helpful here. Once I typed if (direction === "forward"), it inferred the pattern and suggested the depth increases/decreases. Good type modeling feeds good autocomplete.

Part Two: Aim Mechanics

Part two changes the interpretation. Now we track "aim" as a third value:

  • down X increases aim by X
  • up X decreases aim by X
  • forward X increases position by X AND increases depth by (aim × X)
const partTwo = (input: Instruction[], debug: boolean) => {
    let position = 0;
    let depth = 0;
    let aim = 0;
    
    for (const [direction, value] of input) {
        if (direction === "forward") {
            position += value;
            depth += aim * value;
        } else if (direction === "down") {
            aim += value;
        } else if (direction === "up") {
            aim -= value;
        }
    }
    
    if (depth < 0) {
        throw new Error("The submarine is flying!");
    }
    
    return position * depth;
};

Same structure, different semantics. Down/up now affect aim instead of depth directly, and forward has a side effect on depth.

Why Tuples Matter

The Instruction type as [Direction, number] lets us destructure cleanly:

for (const [direction, value] of input) {

TypeScript knows direction is a Direction and value is a number. The inference flows through, Copilot makes better suggestions, and typos get caught at compile time.

Compare to using any[] or {command: string, value: number} — you lose the precision and the tooling benefits.

Performance

Both parts run in about a millisecond. The input parsing takes longer (~22ms) than the actual computation.

Could I optimize? Sure. Should I? No. It runs once, it's fast enough. Optimizing from 1ms to 0.5ms when you run it once per month saves you... nothing meaningful.

Save optimization energy for the later puzzles where naive solutions run until the heat death of the universe.

Takeaways

  1. Develop regexes outside your code: Tools like regex101 let you test and debug interactively. Paste the working regex into your code.

  2. Literal types are powerful: "forward" | "down" | "up" is more useful than string. TypeScript can verify exhaustiveness and enable better autocomplete.

  3. Tuples for fixed structures: When you have "exactly two things, first is X, second is Y," use a tuple type, not an array.

  4. Sanity assertions catch logic bugs: The "flying submarine" check would immediately reveal if my up/down logic was inverted.

  5. Don't optimize prematurely: A millisecond is fast enough for a one-shot computation.

The full solution is available in the repository.