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?

\$\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.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Can anyone tell us why the proposed solution is superior to the original? \$\endgroup\$
    – Mast
    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\$
    – choroba
    Dec 1, 2023 at 21:51

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.