kennysliding

December 6, 2025 Transmission_ID: aoc-2025

Advent of Code 2025: Day 6

rust advent of code ai

Problem

Read it here

tl;dr

You have a math worksheet where problems are arranged vertically in columns:

123 328  51 64
 45 64  387 23
  6 98  215 314
*   +   *   +
  • Numbers in the same problem are stacked vertically
  • The operation (+ or *) is at the bottom
  • Problems are separated by columns of only spaces

Part 1: Read numbers horizontally (row by row). Solve each problem, then sum all answers.

ProblemCalculationAnswer
123, 45, 6123 × 45 × 633210
328, 64, 98328 + 64 + 98490
51, 387, 21551 × 387 × 2154243455
64, 23, 31464 + 23 + 314401

Part 1 total: 33210 + 490 + 4243455 + 401 = 4277556


Part 2: Plot twist — cephalopod math is read right-to-left, and each column is a digit (top = most significant, bottom = least significant).

Same input, completely different parsing:

123 328  51 64
 45 64  387 23
  6 98  215 314
*   +   *   +

Reading right-to-left, column by column:

Problem (R→L)NumbersCalculationAnswer
Rightmost4, 431, 6234 + 431 + 6231058
2nd from right175, 581, 32175 × 581 × 323253600
3rd from right8, 248, 3698 + 248 + 369625
Leftmost356, 24, 1356 × 24 × 18544

Part 2 total: 1058 + 3253600 + 625 + 8544 = 3263827

Key insight: Part 1 reads rows to get numbers. Part 2 reads columns to construct each digit of a number (top-to-bottom = MSD-to-LSD), then processes problems right-to-left.


My Solution (Handcrafted)

This felt like a data engineering problem — parsing columnar data with weird formatting. Right up my alley.

Part 1: Split and Slice

The trick is that split_whitespace does all the heavy lifting. Each row becomes a clean list of numbers/operators:

pub fn solve_1(filename: &str) -> u64 {
    let input = Self::read_input(filename);

    let parsed_input: Vec<Vec<String>> = input
        .iter()
        .map(|line| line.split_whitespace().map(|s| s.to_string()).collect())
        .collect();

    let mut total = 0;

    for i in 0..parsed_input[0].len() {
        let operator = parsed_input[parsed_input.len() - 1][i].clone();

        let mut sequence = vec![];
        for j in 0..parsed_input.len() - 1 {
            sequence.push(parsed_input[j][i].parse::<u64>().unwrap());
        }

        match operator.as_str() {
            "+" => total += sequence.iter().sum::<u64>(),
            "*" => total += sequence.iter().product::<u64>(),
            _ => panic!("Invalid operator: {}", operator),
        }
    }
    total
}

Each column index i gives us one problem. Grab the operator from the last row, collect numbers from all other rows at that index, apply the operation.

Part 2: Character-by-Character Parsing

Part 2 is trickier — we can’t use split_whitespace because spacing matters now. Each character position is a potential digit.

I use the operator row to find where each problem starts and ends:

pub fn solve_2(filename: &str) -> u64 {
    let input = Self::read_input(filename);
    let mut total = 0;
    let mut column_start: usize = 0;
    let mut column_end: usize = 1;

    loop {
        // use the operator row to find the width of a column
        loop {
            if column_end == input[0].len() {
                break;
            }
            match input[input.len() - 1].chars().nth(column_end).unwrap() {
                ' ' => column_end += 1,
                _ => break,
            }
        }

        let operator = input[input.len() - 1].chars().nth(column_start).unwrap();

        // build the sequence of numbers with digits
        let mut sequence = vec![];
        for i in column_start..column_end {
            let mut number_string = String::new();
            for j in 0..input.len() - 1 {
                let digit = input[j].chars().nth(i).unwrap();
                number_string.push(digit);
            }
            number_string = number_string.replace(" ", "");
            if number_string.is_empty() {
                continue;
            }
            let number = number_string.parse::<u64>().unwrap();
            sequence.push(number);
        }

        match operator {
            '+' => total += sequence.iter().sum::<u64>(),
            '*' => total += sequence.iter().product::<u64>(),
            _ => panic!("Invalid operator: {}", operator),
        }
        // ... advance to next problem
    }
    total
}

For each character column within a problem, read top-to-bottom to get digits, concatenate them into a number string, then parse.


Gemini’s Solution

Gemini took a more structured approach with helper functions.

Grid Preprocessing

First, pad all lines to the same length to make column access easier:

fn read_padded_grid(filename: &str) -> Vec<Vec<char>> {
    let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
    let max_len = lines.iter().map(|s| s.len()).max().unwrap_or(0);

    lines
        .iter()
        .map(|s| {
            let mut chars: Vec<char> = s.chars().collect();
            while chars.len() < max_len {
                chars.push(' ');
            }
            chars
        })
        .collect()
}

Problem Detection

Uses a helper to check if a column is all spaces (problem separator):

fn is_col_empty(grid: &Vec<Vec<char>>, col: usize) -> bool {
    for row in grid {
        if col < row.len() && !row[col].is_whitespace() {
            return false;
        }
    }
    true
}

Part 1: Block Processing

For each problem block, extract numbers from rows:

fn process_block_part1(grid: &Vec<Vec<char>>, start: usize, end: usize) -> u64 {
    let mut numbers: Vec<u64> = Vec::new();
    let mut operator = None;

    for row in grid {
        let slice = &row[start..std::cmp::min(end, row.len())];
        let s: String = slice.iter().collect();
        let trimmed = s.trim();

        if trimmed == "+" {
            operator = Some('+');
        } else if trimmed == "*" {
            operator = Some('*');
        } else if let Ok(n) = trimmed.parse::<u64>() {
            numbers.push(n);
        }
    }

    match operator {
        Some('+') => numbers.iter().sum(),
        Some('*') => numbers.iter().product(),
        _ => 0,
    }
}

Part 2: Vertical Column Reading

Same structure, but read columns instead of rows:

fn process_block_part2(grid: &Vec<Vec<char>>, start: usize, end: usize) -> u64 {
    let mut numbers: Vec<u64> = Vec::new();
    let mut operator = None;

    for col in start..end {
        let mut num_str = String::new();

        for row in grid {
            let c = row[col];
            if c.is_ascii_digit() {
                num_str.push(c);
            } else if c == '+' {
                operator = Some('+');
            } else if c == '*' {
                operator = Some('*');
            }
        }

        if !num_str.is_empty() {
            if let Ok(n) = num_str.parse::<u64>() {
                numbers.push(n);
            }
        }
    }

    match operator {
        Some('+') => numbers.iter().sum(),
        Some('*') => numbers.iter().product(),
        _ => 0,
    }
}

Comparison

AspectMy SolutionGemini’s Solution
Part 1split_whitespace — simpleSlice grid blocks — more explicit
Part 2Operator row to find boundariesis_col_empty helper
Grid handlingDirect string indexingPadded char grid
StructureInline logic in solve functionsHelper functions for each step
Code length~90 lines~170 lines

Reflection

Both solutions work, different styles.

My approach relies on split_whitespace doing the parsing work for Part 1, which keeps it compact. For Part 2, I use the operator row as a guide to find problem boundaries — simple but requires careful index tracking.

Gemini’s approach is more defensive — pad the grid first, use helper functions to detect empty columns, process blocks with dedicated functions. More lines of code but easier to debug if something goes wrong.

For a parsing problem like this, both approaches are valid. Mine is more “data engineer” style — trust the structure, slice and dice. Gemini’s is more “software engineer” style — abstract into helpers, handle edge cases explicitly.

Time spent: ~15 minutes