Yes I'll keep doing this until I cannot be bothered to do it in Bash ;-) (Well today is probably the last one ;-))
Part 1
To paraphrase the puzzle, given some input ("engine schematic") like this:
467..114.. ...*...... ..35..633. ......#... 617*...... .....+.58. ..592..... ......755. ...$.*.... .664.598..
The task is to compute the sum of integers that are adjacent to a non-digit horizontally, vertically, or diagonally. In the above example, only 114 and 58 are not adjacent to any symbol.
#!/usr/bin/env bash
#
# Solver for https://adventofcode.com/2023/day/3 part 1
# Redirect the input file to this script, for example day3part1.sh < path/to/input.txt
#
set -euo pipefail
solve_2023_day3_part1() {
local lines first_line sum i j line num segment_current segment_next segment_prev
# Read lines, padded with '.' characters at top, bottom, left, right.
lines=()
read -r first_line
lines+=(".${first_line//?/.}.")
lines+=(".$first_line.")
while read -r line; do
lines+=(".$line.")
done
lines+=(".${first_line//?/.}.")
sum=0
# For each row index except the padding row before and after.
for ((i = 1; i < ${#lines[@]} - 1; ++i)); do
line=${lines[i]}
# For each column index except the padding column at the start and end.
for ((j = 1; j < ${#line} - 1; )); do
# Try to match numeric prefix.
if [[ ${line:j} =~ ^([0-9]+) ]]; then
num=${BASH_REMATCH[1]}
# Check if the line segment on the current line, or above or below
# has any non-numeric non-dot value.
((segment_length = ${#num} + 2))
segment_current=${line:j-1:segment_length}
segment_prev=${lines[i-1]:j-1:segment_length}
segment_next=${lines[i+1]:j-1:segment_length}
if [[ "$segment_current$segment_prev$segment_next" =~ [^.0-9] ]]; then
((sum += num))
fi
((j += ${#num} + 1))
else
((++j))
fi
done
done
echo "$sum"
}
solve_2023_day3_part1
Part 2
To paraphrase the change in part 2, we're looking for *
symbols ("gears") that are adjacent to precisely two integers, and compute the sum of the product of adjacent integers. In the above example this would be 467 * 35 + 755 * 598 =わ 16345 +たす 451490 =わ 467835.
#!/usr/bin/env bash
#
# Solver for https://adventofcode.com/2023/day/3 part 2
# Redirect the input file to this script, for example day3part2.sh < path/to/input.txt
#
set -euo pipefail
solve_2023_day3_part2() {
local lines first_line gears_to_parts line
local num offset row col i j separator
local sum value g1 g2 rest
# Read lines, padded with '.' characters at top, bottom, left, right.
lines=()
read -r first_line
lines+=(".${first_line//?/.}.")
lines+=(".$first_line.")
while read -r line; do
lines+=(".$line.")
done
lines+=(".${first_line//?/.}.")
# Mapping of gear positions and list of adjacent numbers.
declare -A gears_to_parts
separator=' '
# For each row index except the padding row before and after.
for ((i = 1; i < ${#lines[@]} - 1; ++i)); do
line=${lines[i]}
# For each column index except the padding column at the start and end.
for ((j = 1; j < ${#line} - 1; )); do
# Try to match numeric prefix.
if [[ ${line:j} =~ ^([0-9]+) ]]; then
num=${BASH_REMATCH[1]}
# Find all "gears" around the number.
for offset in -1 0 1; do
((row = i + offset)) || true
for ((col = j-1; col < j + ${#num} + 1; ++col)); do
if [ "${lines[row]:col:1}" = '*' ]; then
# Gear found, add current number to list of numbers
# next to this gear position.
gears_to_parts[$row:$col]+="$num$separator"
fi
done
done
((j += ${#num} + 1))
else
((++j))
fi
done
done
sum=0
for value in "${gears_to_parts[@]}"; do
IFS="$separator" read g1 g2 rest <<< "$value"
[ "$g2" ] && ! [ "$rest" ] && ((sum += g1 * g2))
done
echo "$sum"
}
solve_2023_day3_part2
Review request
I do realize this is a bit whimsical, and Bash is a poor choice to solve algorithmic puzzles. Also, this code is intended as a one-off, and not for reuse. I'm solving in Bash because, and as long as, it gives me joy. I won't make promises about how far I'm willing to stick to pure Bash, it will depend on the day and my mood. The main principles I do intend to stick to are:
- The program must produce the correct solution to the full input within seconds.
- The program must use idiomatic Bash.
Do you see any 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?
What would you do differently?