(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:
Convert name to a list of characters, omitting hyphens
Create a list of the unique characters: '(#\o #\r #\s #\e)
Create a list of pairs, each pair is a character and its count: '((#\o 3) (#\r 1) (#\s 2) (#\e 3))
Sort these pairs by the count numbers: '((#\o 3) (#\e 3) (#\s 2) (#\r 1))
Group the pairs by the count numbers and sort each group alphabetically: '(((#\e 3) (#\o 3)) ((#\s 2)) ((#\r 1)))
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.
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.