189 lines
4.2 KiB
Go
189 lines
4.2 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type Race struct {
|
|
time int
|
|
recordDistance int
|
|
}
|
|
|
|
func main() {
|
|
if len(os.Args) != 2 && len(os.Args) != 3 {
|
|
fmt.Fprintf(os.Stderr, "Usage: %s inputfile\n", os.Args[0])
|
|
os.Exit(1)
|
|
}
|
|
|
|
inputFilename := os.Args[1]
|
|
inputFile, err := os.Open(inputFilename)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("could not open input file: %s", err))
|
|
}
|
|
|
|
defer inputFile.Close()
|
|
|
|
inputBytes, err := io.ReadAll(inputFile)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("could not read input file: %s", err))
|
|
}
|
|
|
|
input := strings.TrimSpace(string(inputBytes))
|
|
inputLines := strings.Split(input, "\n")
|
|
races, err := parseRaces(inputLines)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("failed to parse races: %s", err))
|
|
}
|
|
|
|
fmt.Printf("Part 1: %d\n", part1(races))
|
|
fmt.Printf("Part 2: %d\n", part2(races))
|
|
}
|
|
|
|
func part1(races []Race) int {
|
|
res := 1
|
|
for _, race := range races {
|
|
res *= numberOfWaysToWinRace(race)
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
func part2(races []Race) int {
|
|
bigRace, err := combineRaces(races)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return numberOfWaysToWinRace(bigRace)
|
|
}
|
|
|
|
func numberOfWaysToWinRace(race Race) int {
|
|
possibleWins := 0
|
|
for timeHeld := 0; timeHeld <= race.time; timeHeld++ {
|
|
distance := distanceForTimeHeld(timeHeld, race.time)
|
|
if distance > race.recordDistance {
|
|
possibleWins++
|
|
}
|
|
}
|
|
|
|
return possibleWins
|
|
}
|
|
|
|
func distanceForTimeHeld(buttonHeld int, raceTime int) int {
|
|
timeLeft := raceTime - buttonHeld
|
|
speed := buttonHeld
|
|
|
|
return speed * timeLeft
|
|
}
|
|
|
|
func parseRaces(lines []string) ([]Race, error) {
|
|
if len(lines) != 2 {
|
|
return nil, errors.New("input must be exactly two lines")
|
|
}
|
|
|
|
timeLine := removeRowPrefix(lines[0], "Time")
|
|
distanceLine := removeRowPrefix(lines[1], "Distance")
|
|
|
|
if timeLine == lines[0] {
|
|
return nil, errors.New("malformed 'time' line")
|
|
} else if distanceLine == lines[1] {
|
|
return nil, errors.New("malformed 'distance' line")
|
|
}
|
|
|
|
spacePattern := regexp.MustCompile(`\s+`)
|
|
rawTimeLineComponents := spacePattern.Split(timeLine, -1)
|
|
rawDistanceLineComponents := spacePattern.Split(distanceLine, -1)
|
|
if len(rawTimeLineComponents) != len(rawDistanceLineComponents) {
|
|
return nil, errors.New("'time' and 'distance' lines have a different number of elements")
|
|
}
|
|
|
|
timeLineComponents, err := tryParse(rawTimeLineComponents, strconv.Atoi)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid element in 'time' line: %w", err)
|
|
}
|
|
|
|
distanceLineComponents, err := tryParse(rawDistanceLineComponents, strconv.Atoi)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid element in 'distance' line: %w", err)
|
|
}
|
|
|
|
races := make([]Race, len(timeLineComponents))
|
|
for i := 0; i < len(timeLineComponents); i++ {
|
|
timeComponent := timeLineComponents[i]
|
|
distanceComponent := distanceLineComponents[i]
|
|
races[i] = Race{
|
|
time: timeComponent,
|
|
recordDistance: distanceComponent,
|
|
}
|
|
}
|
|
|
|
return races, nil
|
|
}
|
|
|
|
func removeRowPrefix(s string, prefix string) string {
|
|
pattern := regexp.MustCompile("^" + regexp.QuoteMeta(prefix) + `:\s*`)
|
|
|
|
return pattern.ReplaceAllLiteralString(s, "")
|
|
}
|
|
|
|
func combineRaces(races []Race) (Race, error) {
|
|
if len(races) == 0 {
|
|
return Race{}, errors.New("cannot combine zero races into one")
|
|
}
|
|
|
|
raceTimes := make([]int, len(races))
|
|
raceRecords := make([]int, len(races))
|
|
for i, race := range races {
|
|
raceTimes[i] = race.time
|
|
raceRecords[i] = race.recordDistance
|
|
}
|
|
|
|
bigRaceTime := smashNumbers(raceTimes)
|
|
bigRaceRecord := smashNumbers(raceRecords)
|
|
|
|
return Race{
|
|
time: bigRaceTime,
|
|
recordDistance: bigRaceRecord,
|
|
}, nil
|
|
}
|
|
|
|
func smashNumbers(nums []int) int {
|
|
if len(nums) == 0 {
|
|
// programmer error
|
|
panic("cannot combine zero numbers into a big one")
|
|
}
|
|
|
|
s := ""
|
|
for _, n := range nums {
|
|
s += strconv.Itoa(n)
|
|
}
|
|
|
|
bigNum, err := strconv.Atoi(s)
|
|
if err != nil {
|
|
// should never fail, given we only use numbers as is
|
|
panic(fmt.Sprintf("converting %s to a number failed: %w", s, err))
|
|
}
|
|
|
|
return bigNum
|
|
}
|
|
|
|
func tryParse[T any](items []string, doParse func(s string) (T, error)) ([]T, error) {
|
|
res := []T{}
|
|
for i, line := range items {
|
|
parsedItem, err := doParse(line)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("malformed item at index %d: %w", i, err)
|
|
}
|
|
|
|
res = append(res, parsedItem)
|
|
}
|
|
|
|
return res, nil
|
|
}
|