Advent of Code 2025

Racket notes and solutions

12/7/2025

Ongoing log

Newest entries up top; currently compacting cephalopod homework while dodging magnetically sealed trash doors.

Day 6: Trash Compactor (cephalopod math)

Columns are the operands, an all-space column is the delimiter, and the operator sits at the bottom. Part 1 reads the columns left-to-right; part 2 reads them right-to-left by evaluating the transposed columns in order.

#lang racket
(require racket advent-of-code)

(define homework (fetch-aoc-input (find-session) 2025 6))

;; Part 1 approach: Split each row into whitespace-separated tokens (numbers or operators),
;; transpose rows into columns so the operator (bottom row) becomes the head, then eval/sum.
(define rows
  (for/list ([ln (in-list (string-split homework "\n" #:repeat? #t))])
    (for/list ([tok (in-list (string-split ln #px"\s+"))])
      (if (regexp-match? #px"^\d+$" tok)
          (string->number tok)
          (string->symbol tok)))))

;; Group by column, then reverse so the operator (last row) leads each column.
(define columns
  (for/list ([col (in-range (length (first rows)))])
    (reverse (for/list ([row (in-list rows)])
               (list-ref row col)))))

;; part 1
(define ns (make-base-namespace))
(for/sum ([col columns]) (eval col ns))

;; Part 2 approach: Assume a rectangular char grid, transpose to columns, split on
;; all-space columns to find expression groups, strip spaces, collapse digit runs
;; into numbers, pick the operator, and eval each expression.
(define (char->digit char) (- (char->integer char) (char->integer #)))
(define (char->symbol ch) (string->symbol (string ch)))
(define (digits->number ds) (for/fold ([n 0]) ([d ds]) (+ (* n 10) d)))
(define (column->number col) (digits->number (filter number? col)))
(define (space-col? col) (andmap (curry eq? #space) col))

(define (expressions-from s)
  (define lines (string-split s "\n" #:repeat? #t))
  (define columns (apply map list (map string->list lines)))
  (define-values (acc cur)
    (for/fold ([acc '()] [cur '()]) ([col (in-list columns)])
      (if (space-col? col)
          (values (if (null? cur) acc (cons (reverse cur) acc)) '())
          (values acc (cons col cur)))))
  (define column-groups
    (reverse (if (null? cur) acc (cons (reverse cur) acc))))
  (for/list ([grp (in-list column-groups)])
    (define token-cols
      (for/list ([col (in-list grp)])
        (for/list ([ch (in-list col)] #:unless (char=? ch #space))
          (if (char-numeric? ch) (char->digit ch) (char->symbol ch)))))
    (cons (findf symbol? (apply append token-cols))
          (map column->number token-cols))))

(for/sum ([expr (expressions-from homework)]) (eval expr ns))

Notes:

  • apply map list handles the transpose once we assume rectangular input; no explicit grid structs needed.
  • for/fold accumulates column groups in one pass, treating all-space columns as delimiters.
  • Tokenizing stays declarative: strip spaces, digits -> numbers, operators via string->symbol.
  • digits->number folds whole columns of digits without concatenating strings; eval reuses the base namespace for the operators.

Day 5: fresh ingredient IDs

Split the input into ranges and available IDs. Merge overlapping/touching ranges into a normalized range-set, then use range-set-range-containing-or-absent for membership and in-range-set to sum total coverage.

#lang racket
(require racket
         advent-of-code
         (prefix-in reb: rebellion/base/range)
         (prefix-in opt: rebellion/base/option)
         (prefix-in rs:  rebellion/collection/range-set))

(define ingredient-ids (fetch-aoc-input (find-session) 2025 5))

(match-define (list range-section ingredient-section) (string-split ingredient-ids "\n\n"))

(define id-ranges
  (for/list ([m (in-list (regexp-match* #px"(\d+)-(\d+)"
                                        range-section
                                        #:match-select cdr))])
    (apply reb:closed-range (map string->number m))))

(define available-ingredients
  (map string->number (string-split ingredient-section "\n" #:repeat? #t)))

(define (merge-bounds bounds)
  (for/fold ([acc '()]) ([b bounds])
    (match-let ([(list lo hi) b])
      (match acc
        ['() (list b)]
        [(cons (list mlo mhi) rest)
         (if (<= lo (add1 mhi)) (cons (list mlo (max mhi hi)) rest)
             (cons b acc))]))))

(define merged-ranges
  (apply rs:range-set
         (let ([sorted (sort (map (λ (r)
                                    (list (reb:range-lower-endpoint r)
                                          (reb:range-upper-endpoint r)))
                                  id-ranges)
                             < #:key first)])
           (map (λ (b) (apply reb:closed-range b))
                (merge-bounds sorted)))))

(define (fresh? id)
  (opt:present?
   (rs:range-set-range-containing-or-absent merged-ranges id)))

(for/sum ([id (in-list available-ingredients)]
          #:when (fresh? id))
  1)

(for/sum ([r (rs:in-range-set merged-ranges)])
  (add1 (- (reb:range-upper-endpoint r)
           (reb:range-lower-endpoint r))))

Notes:

  • Leaning on the Rebellion package: rebellion/base/range for closed ranges and rebellion/collection/range-set for normalized, non-overlapping sets.
  • range-set requires disjoint/touching ranges, so a small merge pass coalesces overlaps before building it; comparator defaults handle numeric ordering.
  • Freshness check uses range-set-range-containing-or-absent, returning an option; present? is the clean predicate.
  • in-range-set walks the merged ranges; widths are inclusive (add1 on hi - lo) to sum total coverage.

Day 4: forklift-accessible rolls

Count @ cells whose 8-neighbor count of @ is < 4. For part two, repeatedly remove the accessible rolls and re-count until none remain.

#lang racket
(require racket advent-of-code)

(define paper (fetch-aoc-input (find-session) 2025 4))

;; Convert a newline-delimited string into a vector-of-vector grid of characters.
(define (string->grid s)
  (list->vector
   (for/list ([line (in-list (string-split s "\n" #:trim? #t))])
     (list->vector (string->list line)))))

(define (grid-height grid) (vector-length grid))
(define (grid-width grid) (vector-length (vector-ref grid 0)))
(define (grid-cell grid x y) (vector-ref (vector-ref grid y) x))

;; Return the values of the 8 surrounding cells around (x, y) that stay in bounds.
(define (adjacent-8 grid x y)
  (let* ([height (grid-height grid)] [width (grid-width grid)]
         [directions '((0 -1) (1 -1) (1 0) (1 1) (0 1) (-1 1) (-1 0) (-1 -1))])
    (for*/list ([dir (in-list directions)]
                #:do [(match-define (list dx dy) dir)
                      (define nx (+ x dx))
                      (define ny (+ y dy))]
                #:when (and (<= 0 nx) (< nx width) (<= 0 ny) (< ny height)))
      (grid-cell grid nx ny))))

(define grid (string->grid paper))

(define (accessible? grid x y)
  (and (eq? (grid-cell grid x y) #@)
       (< (count (curry eq? #@) (adjacent-8 grid x y)) 4)))

(define (get-accessible-rolls grid)
  (let ([height (grid-height grid)] [width (grid-width grid)])
    (for*/list ([y (in-range height)] [x (in-range width)]
                #:when (accessible? grid x y))
      (list x y))))

;; part 1
(for*/sum ([y (in-range (grid-height grid))]
           [x (in-range (grid-width grid))]
           #:when (accessible? grid x y))
  1)

;; part 2
(define (update-grid coord grid)
  (match-let ([(list x y) coord])
    (let* ([row (vector-ref grid y)]
           [row* (vector-copy row)]
           [_ (vector-set! row* x #.)]
           [grid* (vector-copy grid)])
      (vector-set! grid* y row*)
      grid*)))

(let loop ([total-rolls 0] [g grid])
  (let* ([accessible-rolls (get-accessible-rolls g)]
         [num-acc (length accessible-rolls)])
    (if (zero? num-acc)
        total-rolls
        (loop (+ total-rolls num-acc)
              (foldl update-grid g accessible-rolls)))))

Notes:

  • Uses vectors instead of lists to avoid list-set rebuilding rows while iterating removals.
  • Helpers (grid-cell, grid-width, grid-height) keep adjacency code short; assumes non-empty grid from AoC input.
  • match-define destructures direction tuples inline; count + curry reads like a predicate.
  • Coord-first update-grid pairs with foldl for the peel-off step without mutation of the previous grid.

Day 3: emergency battery banks

Greedy scan: try digits 9→0, pick the leftmost one that leaves room for the remaining picks, then recurse on the suffix. Do it once for length 2 and once for length 12.

#lang racket
(require racket advent-of-code threading)

;; Helpers
(define (char->digit char) (- (char->integer char) (char->integer #)))
(define (digits->number ds) (for/fold ([n 0]) ([d ds]) (+ (* n 10) d)))
(define full-digits '(9 8 7 6 5 4 3 2 1 0))

;; "12345\n67890" -> '((1 2 3 4 5) (6 7 8 9 0))
(define jolts
  (~>> (fetch-aoc-input (find-session) 2025 3)
       (string-split _ "\n")
       (map (compose (curry map char->digit) string->list))))

;; Return #t if there is enough room after idx to place remaining digits.
(define (space? digit-num idx ls-len total-digits)
  (>= (- ls-len (sub1 idx)) (- total-digits digit-num)))

;; Pick the lexicographically largest subsequence of length k from ls.
(define (max-subseq ls k)
  (let loop ([digits full-digits] [digit-num 1] [ls ls])
    (if (> digit-num k) '()
        (match-let ([(list* d more) digits])
          (let* ([len (length ls)] [idx (index-of ls d)]
                 [space-available? (and idx (space? digit-num idx len k))])
            (if space-available?
                (cons d (loop full-digits (add1 digit-num) (list-tail ls (add1 idx))))
                (loop more digit-num ls)))))))

;; part 1 + 2
(for/list ([k (list 2 12)])
  (for/sum ([battery jolts]) (~> battery (max-subseq _ k) digits->number)))

Notes:

  • Greedy search resets the digit scan after each pick; space? blocks choices that strand you short of k.
  • index-of returns #f when a digit is absent, which cleanly skips to the next candidate.
  • Both parts share the same picker; the outer for/list runs it for k=2 and k=12.
  • Threading macros (~>>, ~>) keep the parsing/pipeline readable; match-let/list* tidy destructuring without mutation.
  • compose + curry builds the mapper inline, highlighting the functional style.

Day 2: tandem repeats and periodicity

Looking for numbers that are mirror-tandems (two equal halves) and, separately, numbers whose digit strings are fully tiled by a shorter period.

#lang racket
(require racket advent-of-code threading)

;; Input: fetch puzzle ranges and expand them into inclusive streams.
(define ranges (fetch-aoc-input (find-session) 2025 2))

(define i-ranges
  (~> ranges
      (regexp-match* #px"(\d+)-(\d+)" _ #:match-select cdr)
      (map (λ~>> (map string->number)
                 (apply in-inclusive-range)) _)))

;; part 1: check if a number’s digits split evenly into k equal chunks.
(define (tandem? num k)
  (let* ([str (if (string? num) num (number->string num))]
         [len (string-length str)]
         [chunk (quotient len k)])
    (and (= len (* chunk k))
         (for/and ([i (in-range 1 k)])
           (string=? (substring str 0 chunk)
                     (substring str (* i chunk) (* (+ i 1) chunk)))))))

(for*/sum ([rng (in-list i-ranges)]
           [val (in-stream rng)]
           #:when (tandem? val 2))
  val)

;; part 2: return #t if the string is tiled by a shorter period (exact repetition).
(define (periodic? s)
  (let* ([n (string-length s)])
    (for/or ([k (in-range 2 (add1 n))]
             #:when (zero? (remainder n k))
             #:when (tandem? s k))
      #t)))

(for*/sum ([rng (in-list i-ranges)]
           [val (in-stream rng)]
           #:when (periodic? (number->string val)))
  val)

Notes:

  • Parsing stays regex-based; inclusive ranges expand the input spans.
  • tandem? is generic over k chunks; using 2 for the mirror test.
  • periodic? delegates to tandem? over divisors to detect full tiling.
  • Threading (~>) and λ~>> keep transformations point-free; for*/sum with #:when clauses filters in-line.
  • in-stream + in-list comprehensions keep iteration lazy-ish and declarative.

Day 1: counting dial clicks

The dial starts at 50. Negative turns rotate left, positive turns rotate right, and the first part asks for how many times we land exactly on 0. The twist in part two counts every pass over 0 while spinning.

#lang racket
(require racket advent-of-code)

(define input (fetch-aoc-input (find-session) 2025 1))

(define rotation-doc
  (for/list ([dial (in-list (regexp-match* #px"([LR])(\d+)" input #:match-select values))])
    (match-let* ([(list _ dir amt) dial] [amt (string->number amt)])
      (if (string=? dir "L") (- amt) amt))))

;; part 1: count landings on zero
(for/fold ([num 50] [zeros 0])
          ([turn rotation-doc])
  (let ([curr (modulo (+ num turn) 100)])
    (values curr (+ zeros (if (zero? curr) 1 0)))))

;; part 2: also count passes over zero mid-rotation
(define (count-zeros num turn)
  (let ([step (if (negative? turn) -1 1)])
    (for/sum ([offset (in-inclusive-range step turn step)]
              #:when (zero? (modulo (+ num offset) 100)))
      1)))

(for/fold ([num 50] [zeros 0])
          ([turn rotation-doc])
  (let ([curr (modulo (+ num turn) 100)])
    (values curr (+ zeros (count-zeros num turn)))))

Notes to revisit later:

  • Consider a more direct arithmetic expression for part two to avoid per-click iteration on large turns.
  • Regex captures with regexp-match* + #:match-select values to split direction/magnitude pairs.
  • for/fold and for/sum accumulators with multi-value returns to carry state cleanly.
  • in-inclusive-range to cover both the path and landing step when counting clicks.
  • #:when/#:do clauses inside comprehensions to gate work without extra conditionals.
  • Pattern matching via match-let* avoids manual indexing; pure arithmetic with modulo keeps the state immutable between folds.