# Advent of code 2023 day 1: count trebuchet calibration numbers - mostly in Bash

It's that time of the year again!

Although there's no more Winter Bash, we can still have all the Bash we want in winter ;-)

### Part 1

To paraphrase the description:

1. For each line in the input:
• Find the first digit.
• Find the last digit.
• Form a 2-digit number by concatenating the first and the last digits.
2. Compute the sum.

For example, given the input:

1abc2
pqr3stu8vwx
a1b2c3d4e5f
treb7uchet


The numbers are 12, 38, 15, and 77, and the sum is 142.

Finding the digits is simple enough using sed and regex replacements, and computing the sum is easy using awk.

{ sed -e 's/^[^0-9]\+//' -e 's/[^0-9]\+$//' -e 's/^$$.$$.*$$.$$$/\1\2/' -e 's/^$$.$$$/\1\1/' | awk '{ sum +=$1 } END { print sum }'; } < path/to/input.txt

### Part 2

Part 2 throws a wrench in the gears by changing the specifications: English spelling of digits must also be considered as digits. For example from two1nine we should parse 29 instead of 11.

My first idea was to replace the English words with their corresponding digits, and then simply reuse the implementation for part 1. There, instead of spelling out all the s/// commands for sed manually, I used a loop to produce the commands like any lazy programmer would:

digits=(dummy one two three four five six seven eight nine)
replace_digits=()
for ((i = 1; i < ${#digits[@]}; ++i)); do replace_digits+=(-e "s/${digits[i]}/$i/g") done { sed "${replace_digits[@]}" -e 's/^[^0-9]\+//' -e 's/[^0-9]\+$//' -e 's/^$$.$$.*$$.$$$/\1\2/' -e 's/^$$.$$$/\1\1/' | awk '{ sum +=$1 } END { print sum }'; } < path/to/input.txt


Keen readers and advent of code veterans probably have already spotted how this approach falls apart on inputs such as:

eightwo1
2oneight


Incorrectly parses eightwo1 as 21 instead of 81.
Incorrectly parses 2oneight as 21 instead of 28.

I considered adding additional replacement rules to work around this, such as s/oneight/18/g and so on, but it seems there would be quite a lot of those to write, and starting to look harder to do then to simply implement the algorithm straight as instructed by the description.

And so it was time to open a text editor and write something more than a one-liner:

#!/usr/bin/env bash
#
# Solver for https://adventofcode.com/2023/day/1 part 2
# Redirect the input file to this script, for example day1part2.sh < path/to/input.txt
#

set -euo pipefail

# Prints the digit in $line at$index if exists, without a trailing newline, or fail.
printf_digit_in_line_at() {
local line index
line=$1 index=$2
case ${line:index} in [0-9]*) printf "%s" "${line:index:1}" ;;
one*) printf 1 ;;
two*) printf 2 ;;
three*) printf 3 ;;
four*) printf 4 ;;
five*) printf 5 ;;
six*) printf 6 ;;
seven*) printf 7 ;;
eight*) printf 8 ;;
nine*) printf 9 ;;
*) return 1
esac
}

# Prints the first digit found in $line, without trailing newline. printf_first_digit() { local line index line=$1
for ((index = 0; index < ${#line}; ++index)); do printf_digit_in_line_at "$line" "$index" && return || true done } # Prints the last digit found in$line, without trailing newline.
printf_last_digit() {
local line index
line=$1 for ((index =${#line} - 1; index >= 0; index--)); do
printf_digit_in_line_at "$line" "$index" && return || true
done
}

# Solve part2 of the puzzle and print the output.
solve_part2() {
printf_first_digit "$line" printf_last_digit "$line"
echo
done | awk '{ sum += $1 } END { print sum }' } solve_part2  ### Review request I do realize this is a bit whimsical, and Bash is not a great choice to solve algorithmic puzzles. Do you some patterns here that you would replace with better patterns? Do you see a simpler way to solve any part of the puzzle with Bash and common shell tools? Anything else to do better? ## 1 Answer For me, bash solution would rather be something like this: ## Part 1 #!/bin/bash set -eu shopt -s extglob sum=0 while read line ; do line=${line##+([^0-9])}
line=${line%%+([^0-9])} left=${line:0:1}
right=${line: -1} add=$left$right (( sum += add )) done echo$sum


## Part 2

#!/bin/bash
set -eu
shopt -s extglob

declare -A english_digits=([one]=1 [two]=2 [three]=3 [four]=4 [five]=5
[six]=6 [seven]=7 [eight]=8 [nine]=9)
digits=({1..9})
digits_pattern=$(IFS='|' ; echo "${!english_digits[*]}|${digits[*]}") sum=0 while read line ; do if [[$line =~ ($digits_pattern) ]] ; then left=${BASH_REMATCH[1]}
fi
if [[ $line =~ .*($digits_pattern) ]] ; then
right=${BASH_REMATCH[1]} fi if [[${english_digits[$left]:-} ]] ; then left=${english_digits[$left]} fi if [[${english_digits[$right]:-} ]] ; then right=${english_digits[$right]} fi add=$left$right (( sum += add )) done echo$sum


I.e. using bash and only bash instead of external tools like sed and awk.

• Can anyone tell us why the proposed solution is superior to the original?
– Mast
Dec 1, 2023 at 21:32
• @Mast: I like my solution for the reason stated at the bottom. But beauty is in the eyes of the beholder, and I wouldn't dare to call it superior. Dec 1, 2023 at 21:51