Solve day 2
parent
1d10dbf1b6
commit
608aa5d4cf
|
@ -0,0 +1,186 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CubeCounts = map[string]int
|
||||||
|
|
||||||
|
type Game struct {
|
||||||
|
id int
|
||||||
|
rounds []CubeCounts
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
inputBytes, err := io.ReadAll(inputFile)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("failed to read input file: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
input := string(inputBytes)
|
||||||
|
inputLines := strings.Split(strings.TrimSpace(input), "\n")
|
||||||
|
games, err := parseGames(inputLines)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("invalid input: %s", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Part 1: %d\n", part1(games))
|
||||||
|
fmt.Printf("Part 2: %d\n", part2(games))
|
||||||
|
}
|
||||||
|
|
||||||
|
func part1(games []Game) int {
|
||||||
|
invalidIDTotal := 0
|
||||||
|
for _, game := range games {
|
||||||
|
if isGameValid(game) {
|
||||||
|
invalidIDTotal += game.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return invalidIDTotal
|
||||||
|
}
|
||||||
|
|
||||||
|
func part2(games []Game) int {
|
||||||
|
totalPower := 0
|
||||||
|
for _, game := range games {
|
||||||
|
minPossibleCubes := maxCubesByColor(game)
|
||||||
|
totalPower += cubePower(minPossibleCubes)
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalPower
|
||||||
|
}
|
||||||
|
|
||||||
|
// isGameValid will check if the given game is valid by the number of cubes in the bag
|
||||||
|
func isGameValid(game Game) bool {
|
||||||
|
cubeCounts := map[string]int{
|
||||||
|
"red": 12,
|
||||||
|
"green": 13,
|
||||||
|
"blue": 14,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, round := range game.rounds {
|
||||||
|
for color, count := range round {
|
||||||
|
if count > cubeCounts[color] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// maxCubesByColor will get the maximum quantity of cubes for each color in the game's rounds
|
||||||
|
func maxCubesByColor(game Game) CubeCounts {
|
||||||
|
maxCounts := CubeCounts{}
|
||||||
|
for _, round := range game.rounds {
|
||||||
|
for color, count := range round {
|
||||||
|
currentMax := maxCounts[color]
|
||||||
|
if count > currentMax {
|
||||||
|
maxCounts[color] = count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxCounts
|
||||||
|
}
|
||||||
|
|
||||||
|
// cubePower calculates the "power" of the cubes selected in a round
|
||||||
|
func cubePower(cubes CubeCounts) int {
|
||||||
|
return cubes["red"] * cubes["green"] * cubes["blue"]
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseGames(inputLines []string) ([]Game, error) {
|
||||||
|
return tryParse[Game](inputLines, parseGame)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseGame(line string) (Game, error) {
|
||||||
|
gameID, chosenCubes, err := splitGameLine(line)
|
||||||
|
if err != nil {
|
||||||
|
return Game{}, fmt.Errorf("invalid game %q: %w", line, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rounds, err := parseRounds(chosenCubes)
|
||||||
|
if err != nil {
|
||||||
|
return Game{}, fmt.Errorf("invalid rounds %q: %w", chosenCubes, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Game{
|
||||||
|
id: gameID,
|
||||||
|
rounds: rounds,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitGameLine(line string) (int, string, error) {
|
||||||
|
gamesPattern := regexp.MustCompile(`^Game (\d+): (.*)$`)
|
||||||
|
matches := gamesPattern.FindStringSubmatch(line)
|
||||||
|
if matches == nil {
|
||||||
|
return 0, "", errors.New("malformed game line")
|
||||||
|
}
|
||||||
|
|
||||||
|
rawGameID := matches[1]
|
||||||
|
gameID, err := strconv.Atoi(rawGameID)
|
||||||
|
if err != nil {
|
||||||
|
// we already know this isn't going to happen from the regexp above
|
||||||
|
panic("game id was non-numeric")
|
||||||
|
}
|
||||||
|
|
||||||
|
return gameID, matches[2], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRounds(roundsSpec string) ([]CubeCounts, error) {
|
||||||
|
roundSpecs := strings.Split(roundsSpec, ";")
|
||||||
|
return tryParse[CubeCounts](roundSpecs, parseRound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRound(round string) (CubeCounts, error) {
|
||||||
|
roundPattern := regexp.MustCompile(`(\d+) (\w+)`)
|
||||||
|
matches := roundPattern.FindAllStringSubmatch(round, -1)
|
||||||
|
if matches == nil {
|
||||||
|
return nil, errors.New("malformed round spec")
|
||||||
|
}
|
||||||
|
|
||||||
|
cubes := CubeCounts{}
|
||||||
|
for _, match := range matches {
|
||||||
|
color := match[2]
|
||||||
|
rawCubeCount := match[1]
|
||||||
|
count, err := strconv.Atoi(rawCubeCount)
|
||||||
|
if err != nil {
|
||||||
|
// we already know this isn't going to happen from the regexp above
|
||||||
|
panic("cube count was non-numeric")
|
||||||
|
}
|
||||||
|
|
||||||
|
cubes[color] = count
|
||||||
|
}
|
||||||
|
|
||||||
|
return cubes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tryParse[T any](items []string, parse func(string) (T, error)) ([]T, error) {
|
||||||
|
res := make([]T, 0, len(items))
|
||||||
|
for i, item := range items {
|
||||||
|
parsed, err := parse(item)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid item #%d: %w", i+1, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res = append(res, parsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
Loading…
Reference in New Issue