2023-12-01 12:37:08 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"os"
|
2023-12-01 13:33:20 +00:00
|
|
|
"slices"
|
2023-12-01 12:37:08 +00:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
2023-12-01 13:44:05 +00:00
|
|
|
func main() {
|
|
|
|
if len(os.Args) != 2 {
|
|
|
|
fmt.Fprintf(os.Stderr, "Usage: %s inputfile\n", os.Args[0])
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
filename := os.Args[1]
|
|
|
|
inputFile, err := os.Open(filename)
|
|
|
|
if err != nil {
|
|
|
|
panic(fmt.Sprintf("failed to open input file: %s", err))
|
|
|
|
}
|
|
|
|
|
|
|
|
defer inputFile.Close()
|
|
|
|
|
|
|
|
inputFileBytes, err := io.ReadAll(inputFile)
|
|
|
|
if err != nil {
|
|
|
|
panic(fmt.Sprintf("failed to read input file: %s", err))
|
|
|
|
}
|
|
|
|
|
|
|
|
input := string(inputFileBytes)
|
|
|
|
inputLines := strings.Split(strings.TrimSpace(input), "\n")
|
|
|
|
|
|
|
|
fmt.Printf("Part 1: %d\n", part1(inputLines))
|
|
|
|
fmt.Printf("Part 2: %d\n", part2(inputLines))
|
|
|
|
}
|
|
|
|
|
|
|
|
func part1(input []string) int {
|
|
|
|
digits := []string{
|
|
|
|
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
|
|
|
|
}
|
|
|
|
|
|
|
|
result, err := solve(input, digits, strconv.Atoi)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
|
|
|
func part2(input []string) int {
|
|
|
|
digits := []string{
|
|
|
|
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
|
|
|
|
}
|
|
|
|
|
|
|
|
words := []string{
|
|
|
|
"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
|
|
|
|
}
|
|
|
|
|
|
|
|
allPossible := make([]string, len(digits), len(digits)+len(words))
|
|
|
|
copy(allPossible, digits)
|
|
|
|
allPossible = append(allPossible, words...)
|
|
|
|
|
|
|
|
result, err := solve(input, allPossible, func(s string) (int, error) {
|
|
|
|
n, err := strconv.Atoi(s)
|
|
|
|
if err == nil {
|
|
|
|
return n, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
idx := slices.Index(words, s)
|
|
|
|
if idx != -1 {
|
|
|
|
return idx, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0, errors.New("invalid digit")
|
|
|
|
})
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2023-12-01 13:33:20 +00:00
|
|
|
// filterToMatches find all overlapping matches of the stringset "valid" in "line"
|
|
|
|
func filterToMatches(line string, valid []string) []string {
|
|
|
|
type match struct {
|
|
|
|
idx int
|
|
|
|
value string
|
|
|
|
}
|
|
|
|
|
|
|
|
matches := []match{}
|
|
|
|
start := 0
|
|
|
|
// While we _could_ advance by the length of the string this
|
|
|
|
// challenge allows for overlapping strings. We can be a bit
|
|
|
|
// naive by just advancing one char and working that way
|
|
|
|
for i := range line {
|
|
|
|
for _, matchCandidate := range valid {
|
|
|
|
if strings.HasPrefix(line[i:], matchCandidate) {
|
|
|
|
matches = append(matches, match{idx: start, value: matchCandidate})
|
|
|
|
break
|
|
|
|
}
|
2023-12-01 12:37:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-01 13:33:20 +00:00
|
|
|
slices.SortFunc(matches, func(a, b match) int {
|
|
|
|
return a.idx - b.idx
|
|
|
|
})
|
|
|
|
|
|
|
|
output := []string{}
|
|
|
|
for _, item := range matches {
|
|
|
|
output = append(output, item.value)
|
|
|
|
}
|
|
|
|
|
|
|
|
return output
|
2023-12-01 12:37:08 +00:00
|
|
|
}
|
|
|
|
|
2023-12-01 13:33:20 +00:00
|
|
|
// smashToDigits will "smash" two digits together to form a two digit number
|
|
|
|
func smashDigits(digit1, digit2 int) int {
|
|
|
|
return digit1*10 + digit2
|
|
|
|
}
|
|
|
|
|
|
|
|
// mapToDigits will convert a list of strings to an int, and return the aggregated results
|
|
|
|
// or return an error immediately if one is encountered. In other words, it is assumed
|
|
|
|
// every item in this list is a valid number according to the toDigit function
|
|
|
|
func mapToDigits(items []string, toDigit func(string) (int, error)) ([]int, error) {
|
|
|
|
output := []int{}
|
|
|
|
for _, item := range items {
|
|
|
|
n, err := toDigit(item)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("convert %s: %w", item, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
output = append(output, n)
|
|
|
|
}
|
|
|
|
|
|
|
|
return output, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// getCoordinate gets a coordinate from the given calibration value
|
|
|
|
func getCoordinate(line string, validNumbers []string, convertToNumber func(string) (int, error)) (int, error) {
|
|
|
|
candidates := filterToMatches(line, validNumbers)
|
2023-12-01 12:37:08 +00:00
|
|
|
if len(candidates) == 0 {
|
|
|
|
return 0, errors.New("invalid calibration value")
|
|
|
|
}
|
|
|
|
|
2023-12-01 13:33:20 +00:00
|
|
|
digits, err := mapToDigits(candidates, convertToNumber)
|
|
|
|
if err != nil {
|
|
|
|
// Programmer error, given the above filter
|
|
|
|
panic(err)
|
|
|
|
}
|
2023-12-01 12:37:08 +00:00
|
|
|
|
2023-12-01 13:33:20 +00:00
|
|
|
if len(digits) == 1 {
|
|
|
|
return smashDigits(digits[0], digits[0]), nil
|
2023-12-01 12:37:08 +00:00
|
|
|
}
|
|
|
|
|
2023-12-01 13:33:20 +00:00
|
|
|
value1 := digits[0]
|
|
|
|
value2 := digits[len(candidates)-1]
|
|
|
|
return smashDigits(value1, value2), nil
|
2023-12-01 12:37:08 +00:00
|
|
|
}
|
|
|
|
|
2023-12-01 13:33:20 +00:00
|
|
|
func solve(input []string, validNumbers []string, convert func(string) (int, error)) (int, error) {
|
2023-12-01 12:37:08 +00:00
|
|
|
total := 0
|
|
|
|
for _, line := range input {
|
2023-12-01 13:33:20 +00:00
|
|
|
coordinate, err := getCoordinate(line, validNumbers, convert)
|
2023-12-01 12:37:08 +00:00
|
|
|
if err != nil {
|
2023-12-01 13:33:20 +00:00
|
|
|
return 0, fmt.Errorf("no valid coordinate on line '%s': %s", line, err)
|
2023-12-01 12:37:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
total += coordinate
|
|
|
|
}
|
|
|
|
|
2023-12-01 13:33:20 +00:00
|
|
|
return total, nil
|
|
|
|
}
|