Advent of Code 2023 - Day 1: Trebuchet

:: programming, puzzle, racket

Day 1

For part 1, we’re given data as follows (Note: in both data examples, intraline spaces are only for emphasis):

1 abc 2
pqr 3 stu 8 vwx
a 1 b2c3d4e 5 f
treb 7 uchet

The task is to find the first and last numeric digit in each line, e.g. 1 and 2, concatenate them together, e.g. 12, and then sum all of those values.

For part 2, we’re given data as follows:

two 1 nine
eight wo three
abc one 2 three xyz
x two ne3 four
4 nineeightseven 2
z one ight23 4
7 pqrst six teen

The task is identical, but now instead of only numeric digits, we may need to convert a word, such as two, to a numeric digit.

1
2
#lang iracket/lang #:require racket
(require "../advent.rkt" threading)

Let’s parse the input into one string per line:

1
(define in (parse-aoc 1))
----------------------------------------------------------------------------------------------------
day01.txt -> 21295 chars, 1000 lines; first 3 lines; last 2 lines:
----------------------------------------------------------------------------------------------------
two934seven1
8825eightknfv
sevenoneqbfzntsix55
...
sixgtxr2fourrdkjg
fivebxsevensixone872dlx
----------------------------------------------------------------------------------------------------
(parse 1) -> 1000 entries:
----------------------------------------------------------------------------------------------------
("two934seven1" "8825eightknfv" "sevenoneqbfzntsix55" "foursqpqvv192rd ... fivebxsevensixone872dlx")
----------------------------------------------------------------------------------------------------

Next, we’ll define two lists. One to convert from characters of numeric digits to integers, and a second one to convert from words to integers:

1
2
3
4
5
(define digits '(("1" . 1) ("2" . 2) ("3" . 3) ("4" . 4) ("5" . 5)
                           ("6" . 6) ("7" . 7) ("8" . 8) ("9" . 9)))

(define words '(("one" . 1) ("two" . 2) ("three" . 3) ("four" . 4) ("five" . 5)
                            ("six" . 6) ("seven" . 7) ("eight" . 8) ("nine" . 9)))

Most of the work will be done in the function, find-digit, which will scan through a string, either forward or backward, looking for a string that matches a digit or word from the above two lists. Here’s the code with a couple example invocations:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
(define (find-digit s pairs fix? beg end)
  (let loop ([ p pairs ][ s s ])
    (if (null? p)
        (loop pairs (substring s (beg 0) (end (string-length s))))
        (let ([ pair (car p) ])
          (if (fix? s (car pair))
              (cdr pair)
              (loop (cdr p) s))))))

(find-digit "xyz7abc9d" digits string-prefix? add1 identity)

7

1
(find-digit "xyz7abc9d" digits string-suffix? identity sub1)

9

1
(find-digit "xysevenz7abc9d" words string-prefix? add1 identity)

7

The first parameter s is the string to search. The second parameter pairs is a list of conversion pairs, where each pair contains the string to match and the integer value for that match. The last three parameters configure the function to either search forward, or backward. To search forward, we’ll set fix? to Racket’s string-prefix? function to look at successive prefixes of the string. To search backward, we’ll use string-suffix?. The beg and end parameters configure the beginning and end of the substring we’ll use for the next iteration. When searching forward, we’ll increment the beginning of the string, and leave the end identical. Conversely, when searching backward, we’ll leave the beginning identical, and decrement the end of the string.

Let’s define some convenience functions for finding the first or last digit:

1
2
3
4
5
6
7
(define (find-first-digit s pairs)
  (find-digit s pairs string-prefix? add1 identity))

(define (find-last-digit s pairs)
  (find-digit s pairs string-suffix? identity sub1))

(find-first-digit "xyeightz7abc9d" digits)

7

1
(find-first-digit "xyeightz7abc9d" words)

8

1
(find-last-digit "xyeightz7abc9d" digits)

9

1
(find-last-digit "xyeightz7abc9d" words)

8

1
(find-last-digit "xyeightz7abc9d" (append digits words))

9

Next we’ll compute the “calibration value” for a line by finding the first and last digits, concatenate them, and compute their integer value:

1
2
3
4
5
(define (calibration-value pairs s)
  (+ (* 10 (find-first-digit s pairs))
     (find-last-digit s pairs)))

(calibration-value digits "xyeightz7abc9d")

79

1
(calibration-value (append digits words) "xyeightz7abc9d")

89

The solve function will compute the calibration value for each line of the input and sum the results:

1
2
(define (solve pairs)
  (list-sum (map (curry calibration-value pairs) in)))

So, we can now solve both parts. They only differ in whether they consider only digits, or both digits and words:

1
2
3
4
5
(define (part1) (solve digits))

(define (part2) (solve (append digits words)))

(part1)

52974

1
(part2)

53340