The 2023 Advent of Code advent calendar has started and I'm doing it in Racket again this year. I'll probably stick with it for a couple of weeks or until the puzzles start taking me more than 10-15 minutes to finish. I've also decided to write some short posts about my solutions, so here's the first one.
The first part of today's challenge is to take a list of strings, combine the first and last decimal digit in each string into a decimal number and sum them all up. The example input looks like:
1abc2
pqr3stu8vwx
a1b2c3d4e5f
treb7uchet
In Racket, we can open a file as an input port using
call-with-input-file
and iterate over its lines using
in-lines
.
(call-with-input-file "day01.txt"
(lambda (in)
(for/sum ([line (in-lines in)])
...)))
The for/sum
variant of for
returns the sum of all
intermediate iteration results. With the above skeleton in place, all
we have to do is extract the first and last digit in every line and
convert them to a number (the so-called "calibration value" in the
problem statement).
We're going to need to convert characters to digits, so let's first write a helper procedure to do that.
(define (get-digit s i)
(define c (string-ref s i))
(and (char-numeric? c)
(- (char->integer c)
(char->integer #\0))))
The get-digit
procedure takes a string and an index into that string,
grabs the character at the given index and converts it into a decimal
digit if it is numeric. We're only interested in the ASCII character set
for this particular problem, so there's no need to worry about other
unicode numerals.
Next, we can define the procedure to extract the calibration value from a line.
(define (calibration-value s)
(define-values (d0 d1)
(for/fold ([d0 #f]
[d1 #f])
([i (in-range 0 (string-length s))])
(define digit (get-decimal-digit s i))
(values (or d0 digit)
(or digit d1))))
(+ (* d0 10) d1))
It uses the for/fold
form to iterate over all the indices in a
given string and keep track of the first and last-seen digits. Finally,
it combines the two digits into a decimal number.
We can plug the two helpers into our skeleton to solve part one:
(call-with-input-file "day01.txt"
(lambda (in)
(for/sum ([line (in-lines in)])
(calibration-value line))))
For part two, we get a new example and the puzzle gets a little bit harder. We now need to account for digits that are spelled out on each line. The example input looks like:
two1nine
eightwothree
abcone2threexyz
xtwone3four
4nineeightseven2
zoneight234
7pqrstsixteen
As in the first part, let's first write a helper to extract digits out of a string.
(define digit-rxs
(for/list ([s (in-list '(zero one two three four five six seven eight nine))])
(regexp (format "^~a" s))))
(define (get-spelled-out-digit s i)
(or
(for/first ([(rx n) (in-indexed (in-list digit-rxs))]
#:when (regexp-match? rx s i))
n)
(get-decimal-digit s i)))
Like get-decimal-digit
, the get-spelled-out-digit
procedure takes
a string and a starting index into that string. It then iterates over
a list of regular expressions to return the index of the first match.
The regexp-match?
procedure takes an optional starting index that
tells it where in the given string it should start matching from1. If
none of the regular expressions match, the for/first
form produces
#f
and we fall back to the get-decimal-digit
procedure.
Now, we can update calibration-value
to take its digit-extracting
procedure as an argument instead of calling get-decimal-digit
directly.
(define (calibration-value s [get-digit get-decimal-digit])
(define-values (d0 d1)
(for/fold ([d0 #f]
[d1 #f])
([i (in-range 0 (string-length s))])
(define digit (get-digit s i))
(values (or d0 digit)
(or digit d1))))
(+ (* d0 10) d1))
Since the get-digit
argument to calibration-value
defaults to
get-decimal-digit
, we don't need to change our solution to part one
to account for this refactoring. The solution to part two is the same
as part one, but we pass the get-spelled-out-digit
procedure to
calibration-value
:
(call-with-input-file "day01.txt"
(lambda (in)
(for/sum ([line (in-lines in)])
(calibration-value line get-spelled-out-digit))))
That's it for day one!
Why not just pass in a substring? I prefer to avoid copying where possible and many Racket built-ins that work on sequences support this pattern of passing in beginning and end indices. ↩