5
\$\begingroup\$

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

(Spoiler alert ahead.)

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() {
 while read line; do
 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?

asked Dec 1, 2023 at 19:35
\$\endgroup\$
0

1 Answer 1

3
\$\begingroup\$

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.

answered Dec 1, 2023 at 21:26
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Can anyone tell us why the proposed solution is superior to the original? \$\endgroup\$ Commented Dec 1, 2023 at 21:32
  • 1
    \$\begingroup\$ @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. \$\endgroup\$ Commented Dec 1, 2023 at 21:51

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.