441 lines
10 KiB
Go
441 lines
10 KiB
Go
// TOO HIGH 288057819955899
|
|
|
|
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type PartRatingType rune
|
|
|
|
const (
|
|
RatingTypeX PartRatingType = 'x'
|
|
RatingTypeM PartRatingType = 'm'
|
|
RatingTypeA PartRatingType = 'a'
|
|
RatingTypeS PartRatingType = 's'
|
|
)
|
|
|
|
type ComparisonOperator rune
|
|
|
|
const (
|
|
OperatorGreater ComparisonOperator = '>'
|
|
OperatorLess ComparisonOperator = '<'
|
|
)
|
|
|
|
type Part struct {
|
|
XtremelyCoolRating int
|
|
MusicalRating int
|
|
AerodynamicRating int
|
|
ShinyRating int
|
|
}
|
|
|
|
type Rule struct {
|
|
Conditions []RuleCondition
|
|
FallbackDestination string
|
|
}
|
|
|
|
type RuleCondition struct {
|
|
PartRatingType PartRatingType
|
|
Operator ComparisonOperator
|
|
Operand int
|
|
SuccessDestination string
|
|
}
|
|
|
|
type Range struct {
|
|
// both are inclusive
|
|
min int
|
|
max int
|
|
}
|
|
|
|
func (part Part) Rating(ratingType PartRatingType) int {
|
|
switch ratingType {
|
|
case RatingTypeX:
|
|
return part.XtremelyCoolRating
|
|
case RatingTypeM:
|
|
return part.MusicalRating
|
|
case RatingTypeA:
|
|
return part.AerodynamicRating
|
|
case RatingTypeS:
|
|
return part.ShinyRating
|
|
default:
|
|
panic(fmt.Sprintf("invalid rating type %c", ratingType))
|
|
}
|
|
}
|
|
|
|
func (operator ComparisonOperator) Compare(a, b int) bool {
|
|
switch operator {
|
|
case OperatorGreater:
|
|
return a > b
|
|
case OperatorLess:
|
|
return a < b
|
|
default:
|
|
panic(fmt.Sprintf("invalid operator %c", operator))
|
|
}
|
|
}
|
|
|
|
func (r Range) Min() int {
|
|
return r.min
|
|
}
|
|
|
|
func (r Range) Max() int {
|
|
return r.max
|
|
}
|
|
|
|
func (r Range) Empty() bool {
|
|
return r.min > r.max
|
|
}
|
|
|
|
func (r Range) EliminateLessThan(n int) Range {
|
|
if r.Empty() {
|
|
return r
|
|
}
|
|
|
|
if n > r.min {
|
|
r.min = n
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
func (r Range) EliminateLessThanEq(n int) Range {
|
|
return r.EliminateLessThan(n + 1)
|
|
}
|
|
|
|
func (r Range) EliminateGreaterThan(n int) Range {
|
|
if r.Empty() {
|
|
return r
|
|
}
|
|
|
|
if n < r.max {
|
|
r.max = n
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
func (r Range) EliminateGreaterThanEq(n int) Range {
|
|
return r.EliminateGreaterThan(n - 1)
|
|
}
|
|
|
|
func (r Range) Spread() int {
|
|
if r.Empty() {
|
|
return 0
|
|
}
|
|
|
|
return r.max - r.min + 1
|
|
}
|
|
|
|
func (r Range) Intersection(other Range) Range {
|
|
return Range{
|
|
min: max(r.min, other.min),
|
|
max: min(r.max, other.max),
|
|
}
|
|
}
|
|
|
|
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))
|
|
sections := strings.Split(input, "\n\n")
|
|
if len(sections) != 2 {
|
|
panic(fmt.Sprintf("input file did not have expected number of sections (got %d, expected 2)", len(sections)))
|
|
}
|
|
|
|
rawRules := strings.Split(strings.TrimSpace(sections[0]), "\n")
|
|
rules, err := parseRules(rawRules)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("could not parse rules: %s", err))
|
|
}
|
|
|
|
rawParts := strings.Split(strings.TrimSpace(sections[1]), "\n")
|
|
parts, err := parseParts(rawParts)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("could not parse parts: %s", err))
|
|
}
|
|
|
|
fmt.Printf("Part 1: %d\n", part1(rules, parts))
|
|
fmt.Printf("Part 2: %d\n", part2(rules))
|
|
}
|
|
|
|
func part1(rules map[string]Rule, parts []Part) int {
|
|
ruleFuncs := make(map[string]func(Part) string, len(rules))
|
|
for ruleName, rule := range rules {
|
|
ruleFuncs[ruleName] = buildRuleFunc(rule)
|
|
}
|
|
|
|
acceptedParts := []Part{}
|
|
for _, part := range parts {
|
|
accepted, err := isPartAccepted(ruleFuncs, part)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("could not process part %v: %s", part, err))
|
|
}
|
|
|
|
if accepted {
|
|
acceptedParts = append(acceptedParts, part)
|
|
}
|
|
}
|
|
|
|
acceptedRatings := 0
|
|
for _, part := range acceptedParts {
|
|
acceptedRatings += part.XtremelyCoolRating
|
|
acceptedRatings += part.MusicalRating
|
|
acceptedRatings += part.AerodynamicRating
|
|
acceptedRatings += part.ShinyRating
|
|
}
|
|
|
|
return acceptedRatings
|
|
}
|
|
|
|
func part2(rules map[string]Rule) int {
|
|
ranges := map[PartRatingType]Range{
|
|
RatingTypeX: {min: 1, max: 4000},
|
|
RatingTypeM: {min: 1, max: 4000},
|
|
RatingTypeA: {min: 1, max: 4000},
|
|
RatingTypeS: {min: 1, max: 4000},
|
|
}
|
|
|
|
return combinationsSatisfyingRules(rules, "in", ranges)
|
|
}
|
|
|
|
func isPartAccepted(rules map[string]func(Part) string, part Part) (bool, error) {
|
|
startRule, ok := rules["in"]
|
|
if !ok {
|
|
return false, errors.New("could not locate start rule")
|
|
}
|
|
|
|
rule := startRule
|
|
for {
|
|
nextRuleName := rule(part)
|
|
if nextRuleName == "A" {
|
|
return true, nil
|
|
} else if nextRuleName == "R" {
|
|
return false, nil
|
|
}
|
|
|
|
rule, ok = rules[nextRuleName]
|
|
if !ok {
|
|
return false, fmt.Errorf("could not locate rule %q", nextRuleName)
|
|
}
|
|
}
|
|
}
|
|
|
|
func combinationsSatisfyingRules(rules map[string]Rule, currentRule string, ranges map[PartRatingType]Range) int {
|
|
if currentRule == "R" {
|
|
return 0
|
|
} else if currentRule == "A" {
|
|
combos := 1
|
|
for _, r := range ranges {
|
|
combos *= r.Spread()
|
|
}
|
|
|
|
return combos
|
|
}
|
|
|
|
rule, ok := rules[currentRule]
|
|
if !ok {
|
|
panic(fmt.Sprintf("invalid rule %s", currentRule))
|
|
}
|
|
|
|
combos := 0
|
|
culledRanges := cloneMap(ranges)
|
|
for _, condition := range rule.Conditions {
|
|
affectedRange, ok := culledRanges[condition.PartRatingType]
|
|
if !ok {
|
|
panic(fmt.Sprintf("invalid part rating type %d", condition.PartRatingType))
|
|
}
|
|
|
|
if condition.Operator == OperatorGreater {
|
|
updRanges := cloneMap(culledRanges)
|
|
updRanges[condition.PartRatingType] = affectedRange.EliminateLessThanEq(condition.Operand)
|
|
combos += combinationsSatisfyingRules(rules, condition.SuccessDestination, updRanges)
|
|
|
|
culledRanges[condition.PartRatingType] = affectedRange.EliminateGreaterThan(condition.Operand)
|
|
} else if condition.Operator == OperatorLess {
|
|
updRanges := cloneMap(culledRanges)
|
|
updRanges[condition.PartRatingType] = affectedRange.EliminateGreaterThanEq(condition.Operand)
|
|
combos += combinationsSatisfyingRules(rules, condition.SuccessDestination, updRanges)
|
|
|
|
culledRanges[condition.PartRatingType] = affectedRange.EliminateLessThan(condition.Operand)
|
|
} else {
|
|
panic(fmt.Sprintf("invalid operator %c", condition.Operand))
|
|
}
|
|
}
|
|
|
|
return combos + combinationsSatisfyingRules(rules, rule.FallbackDestination, culledRanges)
|
|
}
|
|
|
|
func parseParts(inputLines []string) ([]Part, error) {
|
|
return tryParse(inputLines, parsePart)
|
|
}
|
|
|
|
func parsePart(input string) (Part, error) {
|
|
partPattern := regexp.MustCompile(`^\{x=(\d+),m=(\d+),a=(\d+),s=(\d+)\}$`)
|
|
matches := partPattern.FindStringSubmatch(input)
|
|
if matches == nil {
|
|
return Part{}, errors.New("malformed part")
|
|
}
|
|
|
|
ratings, err := tryParse(matches[1:], strconv.Atoi)
|
|
if err != nil {
|
|
// can't happen because the pattern only has integers
|
|
panic(fmt.Sprintf("could not parse ratings: %s", err))
|
|
}
|
|
|
|
return Part{
|
|
XtremelyCoolRating: ratings[0],
|
|
MusicalRating: ratings[1],
|
|
AerodynamicRating: ratings[2],
|
|
ShinyRating: ratings[3],
|
|
}, nil
|
|
}
|
|
|
|
func parseRules(inputLines []string) (map[string]Rule, error) {
|
|
rules := make(map[string]Rule, len(inputLines))
|
|
for i, rawRule := range inputLines {
|
|
ruleName, rule, err := parseRule(rawRule)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid rule #%d: %w", i, err)
|
|
}
|
|
|
|
rules[ruleName] = rule
|
|
}
|
|
|
|
return rules, nil
|
|
}
|
|
|
|
func parseRule(rawRule string) (string, Rule, error) {
|
|
declarationsPattern := regexp.MustCompile(`^([a-z]+)\{((?:[xmas][<>]\d+:[a-zAR]+,)+)([a-zAR]+)\}$`)
|
|
declarationMatches := declarationsPattern.FindStringSubmatch(rawRule)
|
|
if declarationMatches == nil {
|
|
fmt.Println(rawRule)
|
|
return "", Rule{}, errors.New("malformed declarations")
|
|
}
|
|
|
|
name := declarationMatches[1]
|
|
rawConditions := declarationMatches[2]
|
|
fallbackDestination := declarationMatches[3]
|
|
|
|
conditions, err := parseRuleConditions(rawConditions)
|
|
if err != nil {
|
|
return "", Rule{}, fmt.Errorf("parse conditions: %w", err)
|
|
}
|
|
|
|
rule := Rule{
|
|
Conditions: conditions,
|
|
FallbackDestination: fallbackDestination,
|
|
}
|
|
|
|
return name, rule, nil
|
|
}
|
|
|
|
func buildRuleFunc(rule Rule) func(Part) string {
|
|
baseFunc := func(Part) string {
|
|
return rule.FallbackDestination
|
|
}
|
|
|
|
// We must store all of the destination functions, otherwise we will
|
|
// be binding to old names of functions when wrapping :(
|
|
destFuncs := []func(Part) string{baseFunc}
|
|
destFunc := func(part Part) string {
|
|
return destFuncs[0](part)
|
|
}
|
|
|
|
for i := len(rule.Conditions) - 1; i >= 0; i-- {
|
|
condition := rule.Conditions[i]
|
|
lastFunc := destFuncs[len(destFuncs)-1]
|
|
ruleDestFunc := func(part Part) string {
|
|
value := part.Rating(condition.PartRatingType)
|
|
if condition.Operator.Compare(value, condition.Operand) {
|
|
return condition.SuccessDestination
|
|
} else {
|
|
return lastFunc(part)
|
|
}
|
|
}
|
|
|
|
destFuncs = append(destFuncs, ruleDestFunc)
|
|
destFunc = ruleDestFunc
|
|
}
|
|
|
|
return destFunc
|
|
}
|
|
|
|
func parseRuleConditions(rawConditions string) ([]RuleCondition, error) {
|
|
conditionPattern := regexp.MustCompile(`^([xmas])([<>])(\d+):([a-zAR]+)$`)
|
|
|
|
splitRawConditions := strings.Split(strings.TrimRight(rawConditions, ","), ",")
|
|
conditions := make([]RuleCondition, len(splitRawConditions))
|
|
for i, rawCondition := range splitRawConditions {
|
|
conditionMatches := conditionPattern.FindStringSubmatch(rawCondition)
|
|
if conditionMatches == nil {
|
|
return nil, fmt.Errorf("malformed condition %q", rawCondition)
|
|
}
|
|
|
|
// These first two are definitely safe, because the pattern restricts
|
|
// these values to single-chars that are available in their types
|
|
ratingType := PartRatingType(conditionMatches[1][0])
|
|
operator := ComparisonOperator(conditionMatches[2][0])
|
|
rawOperand := conditionMatches[3]
|
|
destination := conditionMatches[4]
|
|
|
|
operand, err := strconv.Atoi(rawOperand)
|
|
if err != nil {
|
|
// cannot happen, we guarantee this is an integer from the pattern
|
|
panic(fmt.Sprintf("could not parse condition: %s", err))
|
|
}
|
|
|
|
conditions[i] = RuleCondition{
|
|
PartRatingType: ratingType,
|
|
Operator: operator,
|
|
Operand: operand,
|
|
SuccessDestination: destination,
|
|
}
|
|
}
|
|
|
|
return conditions, 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
|
|
}
|
|
|
|
func cloneMap[T comparable, U any, M ~map[T]U](m M) M {
|
|
copy := make(M, len(m))
|
|
for key, value := range m {
|
|
copy[key] = value
|
|
}
|
|
|
|
return copy
|
|
}
|