6 votes

Day 6: Trash Compactor

Today's problem description: https://adventofcode.com/2025/day/6

Please post your solutions in your own top-level comment. Here's a template you can copy-paste into your comment to format it nicely, with the code collapsed by default inside an expandable section with syntax highlighting (you can replace python with any of the "short names" listed in this page of supported languages):

<details>
<summary>Part 1</summary>

```python
Your code here.
```

</details>

19 comments

  1. mawelborn
    Link
    "It's no use, Mr. James--it's generators all the way down." Much easier today than yesterday, I think. Though the values not being left- or right-aligned in the columns was a wrinkle for Part 02....

    "It's no use, Mr. James--it's generators all the way down."

    Much easier today than yesterday, I think. Though the values not being left- or right-aligned in the columns was a wrinkle for Part 02. As others have said, the actual problem is solved quickly and in only a few lines. The rest is parsing an input format that's more annoying than interesting.

    Part 1
    def solve(input: str) -> int:
        return sum(calculations(input))
    
    
    def calculations(input: str) -> Iterator[int]:
        operators = {"+": add, "*": mul}
        for operator, operands in operators_and_operands(input):
            yield reduce(operators[operator], operands)
    
    
    def operators_and_operands(input: str) -> Iterator[tuple[str, Iterable[int]]]:
        for column in columns_of_rows(input):
            *operands, operator = column
            yield operator, map(int, operands)
    
    
    def columns_of_rows(input: str) -> Iterator[Iterable[str]]:
        rows = input.strip().split("\n")
        rows_of_columns = map(str.split, rows)
        yield from zip(*rows_of_columns)
    
    Part 2
    def solve(input: str) -> int:
        return sum(calculations(input))
    
    
    def calculations(input: str) -> Iterator[int]:
        operators = {"+": add, "*": mul}
        for operator, operands in operators_and_operands(input):
            yield reduce(operators[operator], map(int, operands))
    
    
    def operators_and_operands(input: str) -> Iterator[tuple[str, Iterable[str]]]:
        *operand_rows, operator_row = input.strip().split("\n")
        operand_cols = cephalpod_notation(operand_rows)
        return zip(operator_row.split(), operand_cols)
    
    
    def cephalpod_notation(operand_rows: Iterable[str]) -> Iterator[Iterable[str]]:
        operands = map("".join, zip_longest(*operand_rows, fillvalue=""))
        operands = map(str.strip, operands)
        yield from split_at(operands, lambda operand: operand == "")
    
    3 votes
  2. [5]
    lily
    (edited )
    Link
    Well, I figured we'd get to the parsing puzzle eventually. I got stuck for a while after I realized the columns weren't all the same size in the real input. I'm not especially happy with my...

    Well, I figured we'd get to the parsing puzzle eventually. I got stuck for a while after I realized the columns weren't all the same size in the real input. I'm not especially happy with my parsing logic, but it appears to do the job, and at this point I think I'm better off not messing with it any further.

    I will admit I tend to dislike these kinds of puzzles somewhat, since parsing text formats (especially column-based ones like this) is pretty finicky and not all that enjoyable to me. This one was at least interesting, though - I'll give it that.

    Solution (Lily)
    define add(sum: Integer, number: Integer): Integer {
        return sum + number
    }
    
    define multiply(product: Integer, number: Integer): Integer {
        return product * number
    }
    
    var lines = File.read_to_string("inputs/day_06.txt").split("\n")
    var number_rows = lines.size() - 1
    
    var numbers_horizontal: List[Integer] = List.repeat(number_rows, 0)
    var factors_horizontal: List[Integer] = List.repeat(number_rows, 1)
    var numbers_vertical: List[Integer] = []
    
    var total_horizontal = 0
    var total_vertical = 0
    
    var skip_column = false
    for column in lines[0].size() - 1...0 by -1: {
        if skip_column: {
            skip_column = false
            continue
        }
    
        var number_vertical = 0
        for i, number_horizontal in numbers_horizontal: {
            var char = lines[i][column]
            if char != ' ': {
                var digit = char - '0'
                numbers_horizontal[i] += digit * factors_horizontal[i]
                factors_horizontal[i] *= 10
                number_vertical = number_vertical * 10 + digit
            }
        }
    
        numbers_vertical.push(number_vertical)
    
        var char = lines[number_rows][column]
        if char == '+': {
            total_horizontal += numbers_horizontal.fold(0, add)
            total_vertical += numbers_vertical.fold(0, add)
        elif char == '*':
            total_horizontal += numbers_horizontal.fold(1, multiply)
            total_vertical += numbers_vertical.fold(1, multiply)
        else:
            continue
        }
    
        for i in 0...number_rows - 1: {
            numbers_horizontal[i] = 0
            factors_horizontal[i] = 1
        }
    
        numbers_vertical.clear()
        skip_column = true
    }
    
    print("Part 1: {}\nPart 2: {}".format(total_horizontal, total_vertical))
    
    2 votes
    1. [4]
      imperialismus
      Link Parent
      Yeah, it's supposed to be fun after all. I do wonder though, would it be cleaner to separate the parsing for part 1 and part 2? At least that's the approach I took.

      I'm not especially happy with my parsing logic, but it appears to do the job, and at this point I think I'm better off not messing with it any further.

      Yeah, it's supposed to be fun after all. I do wonder though, would it be cleaner to separate the parsing for part 1 and part 2? At least that's the approach I took.

      1. lily
        Link Parent
        Possibly that would be cleaner, yeah, though maybe less efficient. I did update my code today to parse from right-to-left rather than left-to-right, which simplified things a little bit.

        Possibly that would be cleaner, yeah, though maybe less efficient. I did update my code today to parse from right-to-left rather than left-to-right, which simplified things a little bit.

      2. [2]
        TaylorSwiftsPickles
        Link Parent
        Pulling things out of my posterior orifice here, I didn't even log in to see the actual inputs, but wouldn't: this Doing a text operation replacing all `" "` (double spaces) with `" "`, stripping...

        Pulling things out of my posterior orifice here, I didn't even log in to see the actual inputs, but wouldn't:

        this Doing a text operation replacing all `" "` (double spaces) with `" "`, stripping leftover spaces before the first and after the last digit per row, then splitting rows by `"\n"` and columns by `" "`
        solve the problem fast?
        1. balooga
          Link Parent
          That’s basically what my first attempt did, and it worked for Part 1. The problem is that in Part 2, the number and positions of those extras spaces actually matter quite a bit. My solution that...

          That’s basically what my first attempt did, and it worked for Part 1. The problem is that in Part 2, the number and positions of those extras spaces actually matter quite a bit. My solution that accounts for that was to infer the width of each column by measuring the distance between its operator and the next one in the bottom row, then extract all the numbers from that column as strings (with space characters intact) using the right-sized window.

          2 votes
  3. balooga
    Link
    Ack, I solved Part 1 pretty quickly but Part 2 is throwing me for a loop. Need to reverse course on most of my assumptions that held Part 1 together, namely the way my input formatting collapses...

    Ack, I solved Part 1 pretty quickly but Part 2 is throwing me for a loop. Need to reverse course on most of my assumptions that held Part 1 together, namely the way my input formatting collapses adjacent spaces to make parsing easier. It's getting too late for me now, I'll need to sleep on it and refactor this thing in the morning.

    Probably a good opportunity to rethink my string -> number -> string -> number conversion strategy too, let's just say TypeScript is not happy with some of the stuff I've been doing in my current state of sleepy delirium lol...

    2 votes
  4. imperialismus
    (edited )
    Link
    Yesterday was rough. After trying a long time, I gave up on solving the range merging and resorted to copying some code off Google (not specifically AoC code, so not technically cheating, but it...

    Yesterday was rough. After trying a long time, I gave up on solving the range merging and resorted to copying some code off Google (not specifically AoC code, so not technically cheating, but it felt like a moral loss). Today on the other hand was a big morale boost. I actually bought a physical notepad with grid paper to sketch things out, but in the end I didn't need it for this problem, although it might come in handy later!

    Part 2 solution (Python)
    from functools import reduce
    import operator
    
    def gen_problems(s):
        lines = s.split("\n")
        grid = [list(line) for line in lines]
        operands = []
        buf = []
        for col in range(len(grid[0])):
            digits = "".join([grid[row][col] for row in range(len(grid)-1)])
            if digits.isspace():
                operands.append(buf)
                buf = []
            else:
                buf.append(int(digits))
        operands.append(buf)
        operations = lines[-1].split()
        return operands, operations
    
    def compute_problems(operands, operations):
        total = 0
        for index, op in enumerate(operations):
            nums = operands[index]
            if op == "+":
                total += sum(nums)
            elif op == "*":
                total += reduce(operator.mul, nums)
        return total
    
    input = open("./input.txt").read()[0:-2]
    print(compute_problems(*gen_problems(input)))
    
    2 votes
  5. [4]
    hpr
    Link
    Elixir Both Parts def parse() do parse(AocElixir.read_lines(6)) end def parse_op(op) when op === "+", do: &Kernel.+/2 def parse_op(op) when op === "*", do: &Kernel.*/2 def parse_ops(raw_ops_row),...

    Elixir

    Both Parts
      def parse() do
        parse(AocElixir.read_lines(6))
      end
    
      def parse_op(op) when op === "+", do: &Kernel.+/2
      def parse_op(op) when op === "*", do: &Kernel.*/2
    
      def parse_ops(raw_ops_row), do: raw_ops_row |> String.split() |> Enum.map(&parse_op/1)
    
      def parse(lines) do
        row_num = Enum.count(lines)
        {raw_num_rows, [raw_ops_row]} = Enum.split(lines, row_num - 1)
        {raw_num_rows, parse_ops(raw_ops_row)}
      end
    
      ### PART 1 ###
    
      def parse_str_num(str_num), do: str_num |> String.trim() |> String.to_integer()
    
      def combine(row1, row2, fns) do
        Enum.zip_with([row1, row2, fns], fn [a, b, op] -> op.(a, b) end)
      end
    
      def part1({raw_num_rows, ops}) do
        num_rows =
          Enum.map(
            raw_num_rows,
            fn row -> String.split(row) |> Enum.map(&parse_str_num/1) end
          )
    
        num_rows
        # associate each column with its operation
        |> Enum.reduce(&combine(&1, &2, ops))
        |> Enum.sum()
      end
    
      ### PART 2 ###
    
      def split_on_condition(enum, condition) do
        enum
        |> Enum.chunk_while(
          [],
          fn char, acc ->
            if condition.(char) do
              # emit a chunk with all previous values once condition is hit
              # reverse because cons would otherwise reverse the input
              {:cont, Enum.reverse(acc), []}
            else
              # accumulate non-matching values
              {:cont, [char | acc]}
            end
          end,
          # emit the remaining accumulator
          fn acc -> {:cont, Enum.reverse(acc), []} end
        )
      end
    
      def part2({raw_num_rows, ops}) do
        column_charlists =
          raw_num_rows
          |> Enum.map(&String.to_charlist/1)
          |> Enum.zip()
          |> Enum.map(&Tuple.to_list(&1))
    
        grouped_num_columns =
          column_charlists
          |> split_on_condition(&Enum.all?(&1, fn char -> char === ?\s end))
          |> Enum.map(
            &Enum.map(&1, fn col ->
              col
              |> to_string()
              |> String.trim()
              |> String.to_integer()
            end)
          )
    
        Enum.zip(ops, grouped_num_columns)
        |> Enum.map(fn {op, cols} -> Enum.reduce(cols, op) end)
        |> Enum.sum()
      end
    
    Benchmarks
    Name               ips        average  deviation         median         99th %
    parse          17.70 K       56.49 μs     ±8.84%       55.51 μs       72.43 μs
    part one        1.44 K      696.27 μs    ±12.45%      668.96 μs     1012.20 μs
    part two        0.62 K     1609.27 μs    ±11.51%     1605.36 μs     2125.26 μs
    

    Notes:

    Blegh, this was an odyssey for all the wrong reasons for me.
    I breezed through part one and even though I basically had to rewrite for part two, I quickly had a solution that satisfied the demo.

    BUT IT WOULD NOT PASS.

    Why, why, I lamented. Well it turned out after painstaking searches and debugging, that my program's core logic had no fault! It worked! However, there was a little detail:

    Enum.zip(), which I used to transform the rows of numbers into columns, stops once any of the Enumerables are empty. Which should not be a problem, right? The lines all have the same length! The text file was even properly formatted after all, all nice and tidy!

    Except, in my infinite wisdom, I had given my file-reading-helper a little convenience: It trimmed the input string. Which deleted the trailing newline. Which made the last line shorter by one char. Which meant the last column wasn't being processed.

    Blegh.

    2 votes
    1. [3]
      balooga
      Link Parent
      Yeah, I got bit with something similar. Nothing I had coded myself, but I always get my input into place by pasting the AoC input content into a local file in my VS Code project. This method has...

      Except, in my infinite wisdom, I had given my file-reading-helper a little convenience: It trimmed the input string.

      Yeah, I got bit with something similar. Nothing I had coded myself, but I always get my input into place by pasting the AoC input content into a local file in my VS Code project. This method has always worked fine for me but thanks to today’s puzzle I just realized that the IDE’s auto-format rules “helpfully” trim all whitespace from the end of every line, even in unknown file types. Which completely breaks this puzzle input. That was annoying.

      1 vote
      1. [2]
        lily
        Link Parent
        I had to copy the input into Notepad++ instead of Sublime Text, since my Sublime config automatically trims trailing whitespace. It pays to have more than one editor around, I suppose. (I think if...

        I had to copy the input into Notepad++ instead of Sublime Text, since my Sublime config automatically trims trailing whitespace. It pays to have more than one editor around, I suppose. (I think if you put a cursor at the end of every line Sublime won't do it, though - maybe that's also the case in VS Code?)

        1. balooga
          Link Parent
          Yeah, I just pulled up vim and pasted it in there. Thankfully it was an easy problem to solve, just a tricky one to identify in the first place.

          Yeah, I just pulled up vim and pasted it in there. Thankfully it was an easy problem to solve, just a tricky one to identify in the first place.

  6. tomf
    Link
    my head is not in the game this year. Anyway, here's P1 Google Sheets =SORT( SUM( BYCOL( SPLIT(A1:A5," "), LAMBDA( x, IF(OR(x="+"), SUM(x), PRODUCT(x))))))

    my head is not in the game this year. Anyway, here's P1

    Google Sheets
    =SORT(
      SUM(
       BYCOL(
        SPLIT(A1:A5," "),
        LAMBDA(
         x,
         IF(OR(x="+"),
          SUM(x),
          PRODUCT(x))))))
    
    2 votes
  7. [3]
    thecakeisalime
    Link
    Today was a weird one. Numpy does not like how big these numbers get (leading to overflows), so there's a lot of ugly typecasting. If there's any numpy experts around here, I'd love to know how I...

    Today was a weird one. Numpy does not like how big these numbers get (leading to overflows), so there's a lot of ugly typecasting. If there's any numpy experts around here, I'd love to know how I could improve this (other than just writing the rotation function myself, which I considered but decided against).

    Parts 1 and 2 (Python)
    import numpy
    import math
    
    def operate(params, operation):
      match operation:
        case '*':
          # numpy.prod() overflows
          return math.prod(params)
        case '+':
          return sum(params)
    
    def part_one(lines):
      inputs = [[numpy.uint64(num) for num in l.split()] for l in lines[:-1]]
      operations = lines[-1].split()
      problems = numpy.rot90(inputs, k=3)
      return int(sum(operate(p, o) for p, o in zip(problems, operations)))
      
    
    def part_two(lines):
      inputs = [list(l) for l in lines[:-1]]
      grid = numpy.rot90(inputs, k=1)
      numbers = [''.join(num).strip() for num in grid]
      problem = []
      problems = []
      for n in numbers:
        if (n == ''):
          problems.append(problem)
          problem = []
        else:
          problem.append(int(n))
      problems.append(problem)
      operations = lines[-1].split()[::-1]
      
      return int(sum(operate(p, o) for p, o in zip(problems, operations)))
    
    
    with open('06.in') as file:
      lines = [line.rstrip('\n') for line in file]
    
    print('Part 1:', part_one(lines))
    print('Part 2:', part_two(lines))
    
    2 votes
    1. [2]
      scarecrw
      Link Parent
      The problem is that when you pass params to operate in part 2, it isn't a numpy array, so when numpy.prod() auto-converts it to one, it defaults the datatype to int32 and overflows. You can fix it...

      The problem is that when you pass params to operate in part 2, it isn't a numpy array, so when numpy.prod() auto-converts it to one, it defaults the datatype to int32 and overflows. You can fix it with numpy.prod(params, dtype='uint64').

      As for the rotation, using list(zip(*inputs))[::-1] is a neat trick if you want to do the rotations yourself.

      1 vote
      1. thecakeisalime
        Link Parent
        Thanks! In Part 1 I found that numpy.rot90() output a int32 numpy array, and I couldn't figure out how to set the dtype on it (still don't know how), so I didn't even think about that when working...

        Thanks! In Part 1 I found that numpy.rot90() output a int32 numpy array, and I couldn't figure out how to set the dtype on it (still don't know how), so I didn't even think about that when working on Part 2. It looks like passing the proper dtype to numpy.prod() makes it all work properly and I can get rid of a few casts.

        That rotation is pretty neat. I probably wouldn't have figured that out on my own, but I'm still trying to figure out all the tricks python (and some of its common libraries) have to offer. I see that someone else used itertools.zip_longest which seems to do something similar, but doesn't truncate if the inputs are different lengths.

  8. scarecrw
    (edited )
    Link
    Another day down, though this was mostly just a bit annoying. I did add a few useful tools to my saved utilities: transpose/2, split_list/3, and prod_list/2. Not sure if they'll be useful in...

    Another day down, though this was mostly just a bit annoying. I did add a few useful tools to my saved utilities: transpose/2, split_list/3, and prod_list/2. Not sure if they'll be useful in future days, but I thought they were general enough to keep.

    Prolog Solution
    :- ['src/util.pl'].
    :- initialization(main).
    
    main:-
        read_file_to_strings('inputs/day06.txt', Lines),
    
        parse_input_1(Lines, Vals, Ops),
        maplist(solve, Ops, Vals, Nums),
        sumlist(Nums, Result1),
        write("Part 1: "), write(Result1), nl,
    
        parse_input_2(Lines, Vals2, Ops2),
        maplist(solve, Ops2, Vals2, Nums2),
        sumlist(Nums2, Result2),
        write("Part 2: "), write(Result2), nl,
    
        halt.
    
    solve("+", Vals, Result):- sum_list(Vals, Result).
    solve("*", Vals, Result):- prod_list(Vals, Result).
    
    parse_input_1(Lines, Vals, Ops):-
        maplist([S]>>split_string(S, " ", " "), Lines, SplitLines),
        last(SplitLines, Ops),
        append(ValStrings, [Ops], SplitLines),
        maplist(maplist(string_number), ValStrings, ValRows),
        transpose(ValRows, Vals).
    
    parse_input_2(Lines, Vals, Ops):-
        last(Lines, OpsString),
        append(OrigValStrings, [OpsString], Lines),
        split_string(OpsString, " ", " ", Ops),
        maplist(string_chars, OrigValStrings, Chars),
        transpose(Chars, FlippedChars),
        maplist(exclude(=(' ')), FlippedChars, FilteredChars),
        maplist([C, S]>>string_chars(S, C), FilteredChars, FlippedStrings),
        split_list(FlippedStrings, "", ValStrings),
        maplist(maplist(string_number), ValStrings, Vals).
    
    Utilities
    transpose([], []).
    transpose([L], R):-
        maplist([X, V]>>(V = [X]), L, R).
    transpose([H|T], R):-
        transpose(T, RecResult),
        maplist([X,Y,Z]>>(Z = [X|Y]), H, RecResult, R).
    
    split_list(List, Delimiter, Result):-
        split_list(List, Delimiter, [], Result).
    split_list([], _, Acc, [Acc]).
    split_list([Delimiter], Delimiter, Acc, [Acc, []]).
    split_list([Delimiter|T], Delimiter, Acc, Result):-
        split_list(T, Delimiter, [], Result1),
        Result = [Acc|Result1].
    split_list([H|T], Delimiter, Acc, Result):-
        Acc1 = [H|Acc],
        split_list(T, Delimiter, Acc1, Result).
    
    prod_list(List, Result):-
        foldl([A,B,C]>>(C is A * B), List, 1, Result).
    
    1 vote
  9. Berdes
    Link
    Not the most interesting day for me since this was only about parsing the input the right way, without any shred of thinking to be had on the algorithm side. What's funny is that it would have...

    Not the most interesting day for me since this was only about parsing the input the right way, without any shred of thinking to be had on the algorithm side.

    What's funny is that it would have been much faster for me to first tackle the 2nd part, followed by the first part since the first part is just the second part with the two loops parsing numbers needing to be swapped.

    Solution (Rust)
    use std::io;
    use std::time::Instant;
    use std::vec::Vec;
    
    enum Op {
      Add,
      Mul,
    }
    
    impl Op {
      fn from_str(s: &str) -> Op {
        assert_eq!(s.len(), 1);
        match s {
          "+" => Op::Add,
          "*" => Op::Mul,
          _ => panic!("Bad op {}", s),
        }
      }
    
      fn neutral_element(&self) -> u64 {
        match self {
          Op::Add => 0,
          Op::Mul => 1,
        }
      }
    
      fn apply(&self, lhs: u64, rhs: u64) -> u64 {
        match self {
          Op::Add => lhs + rhs,
          Op::Mul => lhs * rhs,
        }
      }
    }
    
    struct Input {
      len: usize,
      numbers: Vec<Vec<u64>>,
      operators: Vec<Op>,
    
      raw_len: usize,
      raw_numbers: Vec<String>,
      raw_ops: String,
    }
    
    impl Input {
      fn read_from_stdin() -> Input {
        let raw_lines: Vec<String> = io::stdin().lines().map(|l| l.unwrap()).collect();
        assert!(raw_lines.len() > 0);
        let raw_len = raw_lines[0].len();
        for i in 1..raw_lines.len() {
          assert_eq!(raw_len, raw_lines[i].len());
        }
        let raw_numbers = raw_lines[..raw_lines.len() - 1].to_vec();
        let raw_ops = raw_lines.last().expect("").clone();
    
        let mut lines: Vec<Vec<String>> = raw_lines
          .into_iter()
          .map(|l| l.split_ascii_whitespace().map(|s| s.to_string()).collect())
          .collect();
        let len = lines[0].len();
        for i in 1..lines.len() {
          assert_eq!(len, lines[i].len());
        }
        let op_line = lines.pop().unwrap();
        Input {
          len,
          numbers: lines
            .into_iter()
            .map(|l| l.into_iter().map(|n| n.parse::<u64>().expect("")).collect())
            .collect(),
          operators: op_line
            .into_iter()
            .map(|op| Op::from_str(op.as_str()))
            .collect(),
          raw_len,
          raw_numbers,
          raw_ops,
        }
      }
    }
    
    fn part1(input: &Input) -> u64 {
      let mut ret = 0;
      for i in 0..input.len {
        let op = &input.operators[i];
        let mut tmp = op.neutral_element();
        for ns in &input.numbers {
          tmp = op.apply(tmp, ns[i]);
        }
        ret += tmp;
      }
      ret
    }
    
    fn part2(input: &Input) -> u64 {
      let mut ret = 0;
      let mut i = 0;
      while i < input.raw_len {
        let op = Op::from_str(str::from_utf8(&input.raw_ops.as_bytes()[i..i + 1]).expect(""));
        let mut number_end = input.raw_len;
        for j in i + 1..input.raw_len {
          if input.raw_ops.as_bytes()[j] != b' ' {
            number_end = j - 1;
            break;
          }
        }
        let mut tmp = op.neutral_element();
        for j in i..number_end {
          let mut n: u64 = 0;
          for l in &input.raw_numbers {
            let c = l.as_bytes()[j];
            if c != b' ' {
              n = n * 10 + (c - b'0') as u64;
            }
          }
          tmp = op.apply(tmp, n);
        }
        ret += tmp;
        i = number_end + 1;
      }
      ret
    }
    
    fn main() {
      let input = Input::read_from_stdin();
      timed_run("Part 1", || part1(&input));
      timed_run("Part 2", || part2(&input));
    }
    
    1 vote
  10. balooga
    Link
    TypeScript Okay, got my actual solution figured out now. The breakthrough was realizing that the operators (in the last row) always appear in the same position within each column, even though the...

    TypeScript

    Okay, got my actual solution figured out now. The breakthrough was realizing that the operators (in the last row) always appear in the same position within each column, even though the numbers above them shift around. So I begin by walking that bottom row, using each operator as a delimiter which indicates the size (number of characters) in the previous column. Only caveat was that since there's no delimiter to mark the end of the last column, I needed a bit of special kludge to handle that case correctly.

    Once I have the information for a given column — still within that bottom-row walk loop — I can assemble an array of "numbers" (but I'm keeping them as strings to preserve digit placement) for the math problem represented by that column. I store the operator for that problem in my data structure too, so the final formatted data is an array of MathProblem objects that look like this:

    {
        "operation": "*",
        "numbers": ["123", " 45", "  6"]
    }
    

    Then I leave the logic for handling that formatted data up to the Parts 1 and 2 logic. Part 1 is easy, I just call numbers.join(operation) to merge them into a string that looks like this: "123* 45* 6" and then I pass that to eval(). It handles the number parsing and math and doesn't care about the irregular whitespace. Part 2 is a little trickier but just boils down to assembling different "number" strings by getting the length of the existing ones and iterating that many times, and concatenating all of the characters from each existing one that share the same index.

    It's harder to write this in plain English than it was to just code it, lol...

    Parts 1 and 2
    import { RunFunction } from '../../../types';
    
    type Operation = '+' | '*';
    interface MathProblem {
      operation: Operation;
      numbers: string[];
    }
    type InputData = MathProblem[];
    
    function formatInput(input: string): InputData {
      const makeProblem = (operation: Operation) => ({
        operation,
        numbers: [],
      });
    
      const inputArr = input.split('\n');
      inputArr.length && inputArr[inputArr.length - 1] === '' && inputArr.pop();
      const problems: InputData = [];
      const lastRow = inputArr[inputArr.length - 1];
      let currentProblem = makeProblem(lastRow[0] as Operation);
      let currentSize = 0;
      for (let charIndex = 1; charIndex < lastRow.length; charIndex++) {
        const isLastChar = charIndex === lastRow.length - 1;
        if (lastRow[charIndex] === ' ') {
          currentSize++;
          if (!isLastChar) {
            continue;
          }
        }
        if (isLastChar) {
          currentSize++;
        }
        for (let row = 0; row < inputArr.length - 1; row++) {
          // offset accounts for 1-space padding between cols + next operator
          // (neither is present when processing last number in row, so fudge that one)
          const offset = isLastChar ? 2 : 0;
          currentProblem.numbers.push(
            inputArr[row].substring(charIndex - currentSize - 1 + offset, charIndex - 1 + offset)
          );
        }
        problems.push(currentProblem);
        if (!isLastChar) {
          currentProblem = makeProblem(lastRow[charIndex] as Operation);
          currentSize = 0;
        }
      }
      return problems;
    }
    
    export const run: RunFunction<InputData> = () => {
      const totalAnswersHuman = (problems: InputData): number => {
        let total = 0;
        for (const { operation, numbers } of problems) {
          total += eval(numbers.join(operation));
        }
        return total;
      };
    
      const totalAnswersCephalopod = (problems: InputData): number => {
        let total = 0;
        for (const { operation, numbers } of problems) {
          const cephalopodNumbers: string[] = [];
          for (let charIndex = numbers[0].length - 1; charIndex >= 0; charIndex--) {
            cephalopodNumbers.push(
              numbers
                .map(number => number[charIndex])
                .join('')
                .trim()
            );
          }
          total += eval(cephalopodNumbers.join(operation));
        }
        return total;
      };
    
      return [formatInput, totalAnswersHuman, totalAnswersCephalopod];
    };
    

    Benchmarks: The median formatting time on my machine over 50 iterations was 97µs, Part 1 was 242µs, and Part 2 was 482µs.

    1 vote