400 lines
7.3 KiB
Go
400 lines
7.3 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
func processLines(path string, f func(string) (stop bool, err error)) error {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
stop, err := f(scanner.Text())
|
|
if stop || err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func processGroups(path string, f func([]string) (stop bool, err error)) error {
|
|
group := []string{}
|
|
|
|
if err := processLines(path, func(line string) (stop bool, err error) {
|
|
if line == "" {
|
|
stop, err = f(group)
|
|
if stop || err != nil {
|
|
return
|
|
}
|
|
group = []string{}
|
|
} else {
|
|
group = append(group, line)
|
|
}
|
|
|
|
return
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err := f(group)
|
|
|
|
return err
|
|
}
|
|
|
|
// StringSet is a set of strings.
|
|
type StringSet map[string]bool
|
|
|
|
// Add an item to a StringSet.
|
|
func (s *StringSet) Add(v string) {
|
|
(*s)[v] = true
|
|
}
|
|
|
|
// AddAll adds all items in a list to the set.
|
|
func (s *StringSet) AddAll(l []string) {
|
|
for _, v := range l {
|
|
s.Add(v)
|
|
}
|
|
}
|
|
|
|
// Values returns all values in the set as a list.
|
|
func (s StringSet) Values() []string {
|
|
values := []string{}
|
|
for k := range s {
|
|
values = append(values, k)
|
|
}
|
|
|
|
return values
|
|
}
|
|
|
|
// NewStringSet creates an empty string set.
|
|
func NewStringSet() StringSet {
|
|
return StringSet{}
|
|
}
|
|
|
|
// Union returns the union of two SringSets.
|
|
func Union(s1, s2 StringSet) StringSet {
|
|
union := NewStringSet()
|
|
|
|
for v := range s1 {
|
|
union.Add(v)
|
|
}
|
|
|
|
for v := range s2 {
|
|
union.Add(v)
|
|
}
|
|
|
|
return union
|
|
}
|
|
|
|
// Intersection returns the union of two SringSets.
|
|
func Intersection(s1, s2 StringSet) StringSet {
|
|
intersect := NewStringSet()
|
|
|
|
for v := range s1 {
|
|
if _, ok := s2[v]; ok {
|
|
intersect.Add(v)
|
|
}
|
|
}
|
|
|
|
return intersect
|
|
}
|
|
|
|
// Difference returns the value of s1 with values of s2 removed.
|
|
func Difference(s1, s2 StringSet) StringSet {
|
|
difference := NewStringSet()
|
|
|
|
for v := range s1 {
|
|
if _, ok := s2[v]; !ok {
|
|
difference.Add(v)
|
|
}
|
|
}
|
|
|
|
return difference
|
|
}
|
|
|
|
type numRange struct {
|
|
lower, upper int
|
|
}
|
|
|
|
func (r numRange) validate(n int) bool {
|
|
return r.lower <= n && n <= r.upper
|
|
}
|
|
|
|
type fieldRule struct {
|
|
name string
|
|
ranges []numRange
|
|
}
|
|
|
|
func (f fieldRule) validate(n int) bool {
|
|
for _, r := range f.ranges {
|
|
if r.validate(n) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
var ruleMatcher = regexp.MustCompile(`([a-z ]+): ([0-9]+)-([0-9]+) or ([0-9]+)-([0-9]+)`)
|
|
|
|
func parseFieldRule(line string) (fieldRule, error) {
|
|
res := ruleMatcher.FindStringSubmatch(line)
|
|
if res == nil {
|
|
return fieldRule{}, fmt.Errorf("failed parsing rule line: %s", line)
|
|
}
|
|
|
|
name := res[1]
|
|
|
|
l1, err := strconv.Atoi(res[2])
|
|
if err != nil {
|
|
return fieldRule{}, fmt.Errorf("failed parsing int in rule %w", err)
|
|
}
|
|
|
|
u1, err := strconv.Atoi(res[3])
|
|
if err != nil {
|
|
return fieldRule{}, fmt.Errorf("failed parsing int in rule %w", err)
|
|
}
|
|
|
|
l2, err := strconv.Atoi(res[4])
|
|
if err != nil {
|
|
return fieldRule{}, fmt.Errorf("failed parsing int in rule %w", err)
|
|
}
|
|
|
|
u2, err := strconv.Atoi(res[5])
|
|
if err != nil {
|
|
return fieldRule{}, fmt.Errorf("failed parsing int in rule %w", err)
|
|
}
|
|
|
|
return fieldRule{
|
|
name: name,
|
|
ranges: []numRange{
|
|
numRange{
|
|
lower: l1,
|
|
upper: u1,
|
|
},
|
|
numRange{
|
|
lower: l2,
|
|
upper: u2,
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
type fieldRules []fieldRule
|
|
|
|
func (f fieldRules) validFields(n int) []string {
|
|
validFields := []string{}
|
|
for _, r := range f {
|
|
if r.validate(n) {
|
|
validFields = append(validFields, r.name)
|
|
}
|
|
}
|
|
|
|
return validFields
|
|
}
|
|
|
|
type ticket []int
|
|
|
|
func parseTicket(line string) (ticket, error) {
|
|
t := ticket{}
|
|
|
|
for _, n := range strings.Split(line, ",") {
|
|
i, err := strconv.Atoi(n)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
t = append(t, i)
|
|
}
|
|
|
|
return t, nil
|
|
}
|
|
|
|
func parseInput() (fieldRules, ticket, []ticket) {
|
|
rules := fieldRules{}
|
|
nearby := []ticket{}
|
|
|
|
var t ticket
|
|
|
|
if err := processGroups("input.txt", func(group []string) (bool, error) {
|
|
if len(rules) == 0 {
|
|
for _, line := range group {
|
|
rule, err := parseFieldRule(line)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
rules = append(rules, rule)
|
|
}
|
|
} else if group[0] == "your ticket:" {
|
|
var err error
|
|
t, err = parseTicket(group[1])
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
} else if group[0] == "nearby tickets:" {
|
|
for _, line := range group[1:] {
|
|
nt, err := parseTicket(line)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
nearby = append(nearby, nt)
|
|
}
|
|
} else {
|
|
log.Fatal(fmt.Errorf("unknown section %v", group))
|
|
}
|
|
|
|
return false, nil
|
|
}); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
return rules, t, nearby
|
|
}
|
|
|
|
func part1() {
|
|
rules, _, nearby := parseInput()
|
|
|
|
invalid := 0
|
|
|
|
for _, t := range nearby {
|
|
for _, v := range t {
|
|
if len(rules.validFields(v)) == 0 {
|
|
invalid += v
|
|
}
|
|
}
|
|
}
|
|
|
|
fmt.Printf("Total scanning error rate is %d\n", invalid)
|
|
}
|
|
|
|
func reduceFields(fieldOptions []StringSet) ([]string, error) {
|
|
type fieldOption struct {
|
|
options StringSet
|
|
position int
|
|
}
|
|
|
|
workingFields := []fieldOption{}
|
|
for i, o := range fieldOptions {
|
|
workingFields = append(
|
|
workingFields,
|
|
fieldOption{o, i},
|
|
)
|
|
}
|
|
|
|
// Sort slice by number of options
|
|
sort.Slice(workingFields, func(i, j int) bool {
|
|
return len(workingFields[i].options) < len(workingFields[j].options)
|
|
})
|
|
|
|
for i, o1 := range workingFields {
|
|
if len(o1.options) == 1 {
|
|
// Remove this option from everywhere else
|
|
for j, o2 := range workingFields {
|
|
if i == j {
|
|
// Skip current outer index
|
|
continue
|
|
}
|
|
|
|
workingFields[j].options = Difference(o2.options, o1.options)
|
|
}
|
|
}
|
|
}
|
|
|
|
// fmt.Printf("All options? %+v", workingFields)
|
|
|
|
// Sort slice by number of options
|
|
sort.Slice(workingFields, func(i, j int) bool {
|
|
return workingFields[i].position < workingFields[j].position
|
|
})
|
|
|
|
results := []string{}
|
|
for _, o := range workingFields {
|
|
if len(o.options) != 1 {
|
|
return []string{}, fmt.Errorf("some options have multiple values")
|
|
}
|
|
|
|
results = append(results, o.options.Values()[0])
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
func part2() {
|
|
rules, myTicket, nearby := parseInput()
|
|
fieldOptions := []StringSet{}
|
|
|
|
// Get all possible field names
|
|
allFields := []string{}
|
|
for _, rule := range rules {
|
|
allFields = append(allFields, rule.name)
|
|
}
|
|
|
|
// Add them to every field in ticket
|
|
for range myTicket {
|
|
s := StringSet{}
|
|
s.AddAll(allFields)
|
|
fieldOptions = append(fieldOptions, s)
|
|
}
|
|
|
|
var ticketOptions []StringSet
|
|
|
|
for _, nearbyTicket := range nearby {
|
|
ticketOptions = []StringSet{}
|
|
for _, ticketValue := range nearbyTicket {
|
|
validFields := StringSet{}
|
|
validFields.AddAll(rules.validFields(ticketValue))
|
|
if len(validFields) == 0 {
|
|
// This ticket is invalid
|
|
ticketOptions = nil
|
|
break
|
|
}
|
|
ticketOptions = append(ticketOptions, validFields)
|
|
}
|
|
// Done checking ticket
|
|
if ticketOptions != nil {
|
|
// Ticket is valid
|
|
for i := range fieldOptions {
|
|
fieldOptions[i] = Intersection(fieldOptions[i], ticketOptions[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
// fmt.Printf("Final fieldOptions %+v\n", fieldOptions)
|
|
|
|
fieldLabels, err := reduceFields(fieldOptions)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// fmt.Println("Values: ", fieldLabels)
|
|
product := 1
|
|
for i, label := range fieldLabels {
|
|
if strings.HasPrefix(label, "departure") {
|
|
product *= myTicket[i]
|
|
}
|
|
}
|
|
|
|
fmt.Printf("Product of all departures %d\n", product)
|
|
}
|
|
|
|
func main() {
|
|
part1()
|
|
part2()
|
|
}
|