211 lines
4.9 KiB
Go
211 lines
4.9 KiB
Go
|
package main
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"math"
|
||
|
"os"
|
||
|
"regexp"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
type MapRange struct {
|
||
|
start int
|
||
|
size int
|
||
|
}
|
||
|
|
||
|
type ConversionMapEntry struct {
|
||
|
destRange MapRange
|
||
|
srcRange MapRange
|
||
|
}
|
||
|
|
||
|
type ConversionMap []ConversionMapEntry
|
||
|
|
||
|
type ConvertsBetween struct {
|
||
|
from string
|
||
|
to string
|
||
|
}
|
||
|
|
||
|
func (mapRange MapRange) Contains(n int) bool {
|
||
|
return n >= mapRange.start && n < mapRange.start+mapRange.size
|
||
|
}
|
||
|
|
||
|
func (conversionMap ConversionMap) ConvertsTo(n int) int {
|
||
|
for _, entry := range conversionMap {
|
||
|
if entry.srcRange.Contains(n) {
|
||
|
delta := n - entry.srcRange.start
|
||
|
return entry.destRange.start + delta
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return n
|
||
|
}
|
||
|
|
||
|
func main() {
|
||
|
if len(os.Args) != 2 {
|
||
|
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))
|
||
|
seeds, conversions, err := parseAlmanac(input)
|
||
|
if err != nil {
|
||
|
panic(fmt.Sprintf("failed to parse input: %s", err))
|
||
|
}
|
||
|
|
||
|
fmt.Printf("Part 1: %d\n", part1(seeds, conversions))
|
||
|
}
|
||
|
|
||
|
func part1(seeds []int, conversions map[ConvertsBetween]ConversionMap) int {
|
||
|
conversionSteps := []ConvertsBetween{
|
||
|
{from: "seed", to: "soil"},
|
||
|
{from: "soil", to: "fertilizer"},
|
||
|
{from: "fertilizer", to: "water"},
|
||
|
{from: "water", to: "light"},
|
||
|
{from: "light", to: "temperature"},
|
||
|
{from: "temperature", to: "humidity"},
|
||
|
{from: "humidity", to: "location"},
|
||
|
}
|
||
|
|
||
|
min := math.MaxInt
|
||
|
for _, seed := range seeds {
|
||
|
item := seed
|
||
|
for _, conversionStep := range conversionSteps {
|
||
|
conversionMap, ok := conversions[conversionStep]
|
||
|
if !ok {
|
||
|
panic(fmt.Sprintf("Missing conversion for %s-to-%s", conversionStep.from, conversionStep.to))
|
||
|
}
|
||
|
|
||
|
item = conversionMap.ConvertsTo(item)
|
||
|
}
|
||
|
|
||
|
if item < min {
|
||
|
min = item
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return min
|
||
|
}
|
||
|
|
||
|
func parseAlmanac(input string) ([]int, map[ConvertsBetween]ConversionMap, error) {
|
||
|
sections := strings.Split(input, "\n\n")
|
||
|
seeds, err := parseSeeds(sections[0])
|
||
|
if err != nil {
|
||
|
return nil, nil, fmt.Errorf("parse seeds: %w", err)
|
||
|
}
|
||
|
|
||
|
conversions := map[ConvertsBetween]ConversionMap{}
|
||
|
for i, section := range sections[1:] {
|
||
|
convertsBetween, conversionMap, err := parseConversionSection(section)
|
||
|
if err != nil {
|
||
|
return nil, nil, fmt.Errorf("parse section %d: %w", i, err)
|
||
|
}
|
||
|
|
||
|
conversions[convertsBetween] = conversionMap
|
||
|
}
|
||
|
|
||
|
return seeds, conversions, nil
|
||
|
}
|
||
|
|
||
|
func parseSeeds(seedSection string) ([]int, error) {
|
||
|
stripped := strings.TrimPrefix(seedSection, "seeds: ")
|
||
|
if stripped == seedSection {
|
||
|
return nil, errors.New("missing seeds prefix")
|
||
|
}
|
||
|
|
||
|
rawSeedNumbers := strings.Split(stripped, " ")
|
||
|
seedNumbers, err := tryParse(rawSeedNumbers, strconv.Atoi)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("invalid seed numbers: %w", err)
|
||
|
}
|
||
|
|
||
|
return seedNumbers, nil
|
||
|
}
|
||
|
|
||
|
func parseConversionSection(section string) (ConvertsBetween, ConversionMap, error) {
|
||
|
sectionLines := strings.Split(section, "\n")
|
||
|
if len(sectionLines) < 2 {
|
||
|
return ConvertsBetween{}, nil, errors.New("not enough information in section")
|
||
|
}
|
||
|
|
||
|
convertsBetween, err := parseSectionHeading(sectionLines[0])
|
||
|
if err != nil {
|
||
|
return ConvertsBetween{}, nil, fmt.Errorf("invalid section heading: %w", err)
|
||
|
}
|
||
|
|
||
|
conversionMap, err := parseConversionMap(sectionLines[1:])
|
||
|
if err != nil {
|
||
|
return ConvertsBetween{}, nil, fmt.Errorf("invalid conversion: %w", err)
|
||
|
}
|
||
|
|
||
|
return convertsBetween, conversionMap, nil
|
||
|
}
|
||
|
|
||
|
func parseSectionHeading(heading string) (ConvertsBetween, error) {
|
||
|
pattern := regexp.MustCompile(`^(\w+)-to-(\w+) map:`)
|
||
|
matches := pattern.FindStringSubmatch(heading)
|
||
|
if matches == nil {
|
||
|
return ConvertsBetween{}, errors.New("malformed heading")
|
||
|
}
|
||
|
|
||
|
return ConvertsBetween{
|
||
|
from: matches[1],
|
||
|
to: matches[2],
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
func parseConversionMap(lines []string) (ConversionMap, error) {
|
||
|
conversionMap := ConversionMap{}
|
||
|
for _, line := range lines {
|
||
|
rawEntryNumbers := strings.Split(line, " ")
|
||
|
entryNumbers, err := tryParse(rawEntryNumbers, strconv.Atoi)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("conversion map entry numbers: %w", err)
|
||
|
} else if len(entryNumbers) != 3 {
|
||
|
return nil, fmt.Errorf("expected 3 numbers in a conversion section, got %d", len(rawEntryNumbers))
|
||
|
}
|
||
|
|
||
|
dest := entryNumbers[0]
|
||
|
src := entryNumbers[1]
|
||
|
size := entryNumbers[2]
|
||
|
|
||
|
entry := ConversionMapEntry{
|
||
|
destRange: MapRange{start: dest, size: size},
|
||
|
srcRange: MapRange{start: src, size: size},
|
||
|
}
|
||
|
|
||
|
conversionMap = append(conversionMap, entry)
|
||
|
}
|
||
|
|
||
|
return conversionMap, nil
|
||
|
}
|
||
|
|
||
|
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
|
||
|
}
|