On this page:
<day04>
4.1 What is the sum of the sectors of the non-fake rooms?
<day04-setup>
<day04-parsing>
<day04-checksum>
<day04-q1>
4.2 What’s the sector ID of the room where north pole objects are stored?
<day04-q2-setup>
<day04-q2>
4.3 Testing Day 4
<day04-test>
6.7

4 Day 4: Decrypting the Room List

Read the description of today’s puzzle. Here is my input.

4.1 What is the sum of the sectors of the non-fake rooms?

(require racket rackunit)
 
(define test-room "not-a-real-room-404[oarel]")
 
(define input-string (file->string "day04input.txt"))

Each room is described in a string containing three bits of information. So our first job is to make sure that we have a quick way of chopping up these strings to get those three bits.

(define (nominal-checksum str)
  (substring str (- (string-length str) 6) (sub1 (string-length str))))
 
(define (name str)
  (substring str 0 (- (string-length str) 11)))
 
(define (sector str)
  (string->number (substring str (- (string-length str) 10) (- (string-length str) 7))))

Computing a room’s checksum is a fiddly process. I’ve tried to make the code for this readable, but in summary here’s what each step does:

  1. Convert name to a list of characters, omitting hyphens

  2. Create a list of the unique characters: '(#\o #\r #\s #\e)

  3. Create a list of pairs, each pair is a character and its count: '((#\o 3) (#\r 1) (#\s 2) (#\e 3))

  4. Sort these pairs by the count numbers: '((#\o 3) (#\e 3) (#\s 2) (#\r 1))

  5. Group the pairs by the count numbers and sort each group alphabetically: '(((#\e 3) (#\o 3)) ((#\s 2)) ((#\r 1)))

  6. Collapse the list back down to pairs, and take the first member of the first 5 pairs.

(define (actual-checksum str)
  (define name-chars
    (filter-not (curry char=? #\-) (string->list (name str))))
 
  (define unique-chars
    (remove-duplicates name-chars))
 
  (define char-counts
    (for/list ([char (in-list unique-chars)])
              (list char (count (curry char=? char) name-chars))))
 
  (define sorted-by-count
    (sort char-counts (lambda (a b) (> (second a) (second b)))))
 
  (define alphasort
    (map (lambda (x) (sort x (lambda (a b) (char<? (first a) (first b)))))
         (group-by (curry second) sorted-by-count)))
 
  (list->string (take (map first (apply append alphasort)) 5)))

We can tell if a room/string is fake by checking to see if its nominal-checksum is different than its actual-checksum. If we wrap this check in a function and map it onto a list of room strings, we can get the sector numbers of all the valid rooms and total them up.

(define (real-room? str)
  (cond [(string=? (nominal-checksum str) (actual-checksum str))
         (sector str)]
        [else #f]))
 
(define (sum-of-real-room-sectors room-list)
  (apply + (filter-not false? (map real-room? room-list))))
 
(define (q1 str)
  (sum-of-real-room-sectors (string-split str "\n")))

4.2 What’s the sector ID of the room where north pole objects are stored?

Kind of a weird question, but a fun one to figure out. I figured I’d start by decrypting all the room names as described in the puzzle: by rotating each character forward in the alphabet by the number used as the sector ID. The code-point for character #\a is 96, so by subtracting that value from the char->integer for a character I can get its position in the alphabet.

I’m also making use of Racket’s curry function, which is a very nice quick way of building a function on the spot when there’s already a function that does almost what you want, if only you could pre-set one (or more) of the parameters. So, for example, since rotate-char takes two arguments (a character and a number), (curry rotate-char #\c) returns a function that is just rotate-char with the first argument already baked-in. If you need to bake in arguments starting from the last argument, you can use curryr, as I do below.

(define (rotate-char char n)
  (cond
    [(char=? #\- char) #\space]
    [else
     (let* [(char-number (- (char->integer char) 96))
            (rotated-number (+ 1 (modulo (+ char-number -1 n) 26)))]
       (integer->char (+ rotated-number 96)))]))
 
(define (rotate-string str n)
  (list->string (map (curryr rotate-char n) (string->list str))))
 
(define (decrypt-room str)
  (list (rotate-string (name str) (sector str)) (sector str)))

Spoiler alert! (somewhat more than usual)

I wasn’t sure where to go from here, so I just decrypted all the rooms in the REPL, and ran a few searches. After a few false tries the answer popped up on my screen, which was a fun kind of "aha" moment.

(define (q2 str)
  (define real-rooms
    (filter (lambda (r) (real-room? r)) (string-split str "\n")))
 
  (define northpole-room
    (first
     (filter (lambda (r) (string-contains? (first r) "northpole object storage"))
             (map decrypt-room real-rooms))))
 
  (last northpole-room))

4.3 Testing Day 4

(module+ test
  (check-equal? (actual-checksum test-room) "oarel")
  (check-equal? (q1 input-string) 361724)
  (check-equal? (rotate-string "qzmt-zixmtkozy-ivhz" 343) "very encrypted name")
  (check-equal? (q2 input-string) 482))