1.1. Prologue - Setup
Standard setup for AOC solutions.
;;;; Day06.lisp
;;;; 2025 AOC Day 6 solution
;;;; Common Lisp solutions by Leo Laporte (with lots of help)
;;;; Started: 06 Dec 2025 at 11:15
;;;; Finished:
;; —————————————————————————-
;; Prologue code for setup - same every day
;; —————————————————————————-
(defpackage :aoc.2025.day06
(:use :cl :alexandria :iterate) ; no prefix for these libraries
(:local-nicknames ; short prefixes for these
(:re :cl-ppcre) ; regex
(:5a :fiveam) ; test framework
(:sr :serapeum) ; CL extensions
(:tr :trivia))) ; pattern matching
(in-package :aoc.2025.day06)
(setf 5a:run-test-when-defined t) ; test as we go
(setf 5a:verbose-failures t) ; show failing expression
(sr:toggle-pretty-print-hash-table) ; automatic pretty print for hashes
(declaim (optimize (debug 3))) ; max debugging info
;; (declaim (optimize (speed 3)) ; max speed if needed
(defparameter data-file “~/cl/AOC/2025/Day06/input.txt”
“Downloaded from the AoC problem set”)
1.2. Part One
Hmmm. Seems easy. Too easy. I’ve got a bad feeling about this.
I’ll PARSE-INPUT into two values, an array of lists, each list will contain
all the integers from the columns, and then a list of operand strings. The
problem is practically done by then. I just apply each operand to its
respective column and sum the results. The list will be very long with the
provided data but I don’t think 1000 digits lists are particularly
problematic. Let’s see.
1.2.1. Example Data
(defparameter *example* (list "123 328 51 64 "
" 45 64 387 23 "
" 6 98 215 314"
"* + * + "))
1.2.2. Parser
(sr:-> parse-input (list) (values array list))
(defun parse-input (input)
"given a list of strings, return two values, an array of integers by column
and a list of strings representing operators"
(let* ((rows (length input))
(cols (length (sr:words (first input)))) ; actual numbers not spaces
;; an array of lists - each list contains all the integers from a column
(digits (make-array cols :element-type 'list :initial-element nil)))
(iter (for row below (1- rows))
(for row-of-nums = (sr:words (nth row input))) ; chop it up first
(iter (for col below cols)
(setf (aref digits col)
(push (parse-integer (nth col row-of-nums)) ; the number
(aref digits col))))) ; that column's list
;; return array of lists of integers by column and list of operators
(values digits (sr:tokens (lastcar input)))))
1.3. Test Parse
Let’s see how the parser works with the example…
(multiple-value-bind (digits operators) (parse-input *example*)
(format t "Digits: ~S~%Operators: ~S~%" digits operators))
Digits: #((6 45 123) (98 64 328) (215 387 51) (314 23 64))
Operators: ("*" "+" "*" "+")
1.3.1. Solution
(sr:-> day06-1 (list) number)
(defun day06-1 (input)
"given a list of strings of digits and a final string of operators, produce
the sum of the results of using each operator on the column above - most of the
work is done in the parsing"
(multiple-value-bind (digits operators) (parse-input input)
;; now go through each column
(iter (for col below (length digits))
(summing
;; apply the apropriate operand
(ecase (char (nth col operators) 0)
(#\+ (apply #'+ (aref digits col)))
(#\* (apply #'* (aref digits col))))))))
(5a:test day06-1-test
(5a:is (= 4277556 (day06-1 *example*))))
Running test DAY06-1-TEST .
Did 1 check.
Pass: 1 (100%)
Skip: 0 ( 0%)
Fail: 0 ( 0%)
1.4. Part Two
Frickin’ cephalpods. They’re as bad as the lanternfish. So it turns out that the columns are digit by digit. And spaces are significant. This is going to require a completely different parser. I think the final function can remain mostly the same as long as the parsing is done correctly.
In fact, I can pretty much use the same parser with slight
modifications. Instead of using WORDS to extract the digits I’ll step through
the string a column at a time, replacing spaces with zeroes and putting in the
single digits.
This is simple. Right? Right? Well not exactly. The integers to process are combinations of all the digits in the column. So…
64 23 314 +
ends up being: 623 + 431 + 4. So I will have to chunk up the columns and then index into them. A bit more complicated.
The real question is how can I determine when to begin a new column? Is the width consistent? Alas no. Examining the input file shows that operands are usually separated by four columns but not always (unlike the example - tricky!)
Is a new column always begun by an operator? Can I use them as an anchor? First, a test to see if the operators do really match the start of the columns…
(defun opstarts (input)
(let* ((rows (1- (length input)))
(op-string (nth rows input))
(column-starts '()))
;; use op-string to determine column start and end
(iter (for i below (length op-string))
(when (not (char= #\space (char op-string i)))
(push i column-starts))) ; push column start
(reverse column-starts)))
;; print the operator start list for example
(format t “Ops starts for example: ~a~%~%” (opstarts example))
;; compare the input to the operators (well just the first 70 characters)
(format t “First 70 chars of input: ~%~a~%~a~%~a~%~a~%~a~%”
(subseq (first (uiop:read-file-lines data-file)) 0 70)
(subseq (second (uiop:read-file-lines data-file)) 0 70)
(subseq (third (uiop:read-file-lines data-file)) 0 70) (subseq (fourth (uiop:read-file-lines data-file)) 0 70)
(subseq (nth 4 (uiop:read-file-lines data-file)) 0 70))
Ops starts for *example*: (0 4 8 12) First 70 chars of input: 886 63 27 258 98 318 99 975 7 6393 947 87 23 765 35 1 6415 4 652 49 14 97 72 335 28 269 23 1648 698 46 68 814 3193 89 421 13 65 143 36 8 42 429 55 327 44 775 558 76 85 171 6136 9139 23 97 8 686 695 9 4 689 2 373 96 131 7 41 96 28 7328 8919 66 68 * + + * + + * + * + + * * + + + + *
Yep that seems to work for the example and at least for the first 70 of 1000 characters in the input data. I think it’s worth going forward with that assumption, so I think the easiest way to do this is to create a new parser, CEPHLAPOD-PARSE, that creates a 2D array of digits grouped by column, instead of WORDS, and I might as well keep the list of operators in the array.
1.4.1. Cephalopod Parser
(sr:-> cephalapod-parse (list) array)
(defun cephalapod-parse (input)
"given a list of strings, return a 2D array with number strings grouped by column"
(let ((rows (length input)) ; number of lines will be rows in arr
(column-starts '()) ; indices into line for each column
(operators (lastcar input))) ; the operators are the last line
;; use the operators to determine column starts
(iter (for i below (length operators))
(when (not (char= #\space (char operators i))) ; it's an operand
(push i column-starts))) ; save the location of the operand
(setf column-starts (reverse column-starts))
;; now fill the 2d array GRID with numbers by column groups
(let ((grid (make-array (list rows (length column-starts))
:element-type 'list
:initial-element nil)))
(iter (for line in input)
(for row :from 0)
(iter (for (start . rest) on column-starts)
(for group :from 0)
(setf (aref grid row group)
(push (subseq line start (first rest))
(aref grid row group)))))
grid)))
Let’s see what that produces… BTW I’ve changed my source template in the Emacs ORG-MODE.EL configuration to provide an automatic name for every source block using the current time. Just to keep things straight.
(format t "Example: ~%~a" (cephalapod-parse *example*))
Example:
#2A(((123 ) (328 ) ( 51 ) (64 ))
(( 45 ) (64 ) (387 ) (23 ))
(( 6 ) (98 ) (215 ) (314))
((* ) (+ ) (* ) (+ )))
I see I am capturing the space at the end of each column except the last. I can deal witth that later. Let’s see if I can get the columns.
(let ((arr (cephalapod-parse *example*)))
(iter (for col below (array-dimension arr 1))
(collect
(iter (for row below (array-dimension arr 0))
(collect (car (aref arr row col)))))))
| 123 | 45 | 6 | * |
| 328 | 64 | 98 | + |
| 51 | 387 | 215 | * |
| 64 | 23 | 314 | + |
OK good. So each row contains the proper strings followed by the operator. I need to take those strings and process them character by character.
(let ((arr (cephalapod-parse *example*)))
(iter (for col below (array-dimension arr 1))
(print (car (aref arr col 0)))))
"123 " " 45 " " 6 " "* "
Yeah they line up nicely. I’m looking to turn that into (* 1 24 356) - so I’d take char 0 of the first three, turn that into 1. char 1 of lines 0-2 turns into 24, char 2 of lines 0-2 is 356 and then char 0 of line 4 is *, the operand. I’m looking to write a function, PROCESS-COL, that does that for a single row. I’ll start with the tests.
(sr:-> process-cols (list) number)
(defun process-cols (row)
"Given a list of strings representing a row, with each number aligned with
spaces, and the last item representing an operator, return the result of
applying the operator to the digits formed by going from top to bottom column by
column."
(let* ((operator-string (lastcar row)) ; last element of row is the op
(digit-list (butlast row)) ; the list of number strings
(cols (length (first digit-list))) ; length of each number string
(numbers ; list of the operands created by concatenating by col
(iter (for c below cols)
;; collect the numbers by going down the columns
(for digits =
(iter (for str in digit-list)
(collect (subseq str c (1+ c)) into pieces)
(finally (return (apply #'concatenate 'string pieces)))))
(collect (parse-integer digits :junk-allowed t)))))
(setf numbers (remove nil numbers)) ; the line of spaces at the end is nil
(ecase (char operator-string 0)
(#\+ (apply #'+ numbers))
(#\* (apply #'* numbers)))))
(5a:test process-cols-test
(5a:is (= (process-cols ‘(“123 "
" 45 "
" 6 "
“* “)) (* 1 24 356)))
(5a:is (= (process-cols ‘(“328 "
“64 "
“98 "
“+ “)) (+ 369 248 8)))
(5a:is (= (process-cols ‘(” 51 "
“387 "
“215 "
“* “)) (* 32 581 175)))
(5a:is (= (process-cols ‘(“64 "
“23 "
“314”
“+ “)) (+ 623 431 4))))
Running test PROCESS-COLS-TEST ....
Did 4 checks.
Pass: 4 (100%)
Skip: 0 ( 0%)
Fail: 0 ( 0%)
Looking good. Now I just have to put it all together.
1.4.2. Solution
(sr:-> day06-2 (list) number)
(defun day06-2 (input)
(let* ((grid (cephalapod-parse input)) ; make 2D array of input
(column-list ; transpose it into a list of lists of strings
(iter (for col below (array-dimension grid 1))
(collect
(iter (for row below (array-dimension grid 0))
(collect (car (aref grid row col))))))))
;; process each number string by adding digits column by column
(iter (for column in column-list)
(summing (process-cols column)))))
(5a:test day06-2-test
(5a:is (= 3263827 (day06-2 example))))
Running test DAY06-2-TEST .
Did 1 check.
Pass: 1 (100%)
Skip: 0 ( 0%)
Fail: 0 ( 0%)
Very nice the test passes. But I get an out-of-bounds error on the data. Ah I see the problem. When I transpose the array I’m using row col instead of col row - that makes it out of bounds. That worked because I accidentally swapped rows and cols in the iter clauses (which only worked in the example because it’s a square - the data is most definitely not square!). Swapped the iter clauses and used the proper (aref arr row col) and it works again. All done. And it only took two days! At least the solution to part 2 took 0 seconds!
1.5. Solutions
;; now solve the puzzle!
(time (format t "The answer to AOC 2025 Day 6 Part 1 is ~a~%"
(day06-1 (uiop:read-file-lines *data-file*))))
(time (format t “The answer to AOC 2025 Day 6 Part 2 is ~a~%”
(day06-2 (uiop:read-file-lines data-file))))
The answer to AOC 2025 Day 6 Part 1 is 5873191732773 The answer to AOC 2025 Day 6 Part 2 is 11386445308378
1.6. Performance
Timings with SBCL on a 2023 MacBook Pro M3 Max with 64GB RAM and Tahoe 26.1
Evaluation took: 0.002 seconds of real time 0.002017 seconds of total run time (0.001854 user, 0.000163 system) 100.00% CPU 641,616 bytes consed
Evaluation took: 0.000 seconds of real time 0.000915 seconds of total run time (0.000859 user, 0.000056 system) 100.00% CPU 1,505,808 bytes consed