summaryrefslogtreecommitdiffstats
path: root/aoc23
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2023-12-08 15:50:19 +0100
committerMartin Polden <mpolden@mpolden.no>2023-12-08 16:28:47 +0100
commit7a08e0d00c7aa5cd976ab9a53bc65fe375072507 (patch)
tree3ba56cb54b2712711123270fc54e1825f1a6b088 /aoc23
parentb147937f39f66cab47005ac9af8562102c90e053 (diff)
aoc23: switch to go
Diffstat (limited to 'aoc23')
-rw-r--r--aoc23/Makefile17
-rw-r--r--aoc23/day01.py61
-rw-r--r--aoc23/day01_test.go55
-rw-r--r--aoc23/day02.py68
-rw-r--r--aoc23/day02_test.go83
-rwxr-xr-xaoc23/gen.py54
-rw-r--r--aoc23/go.mod3
-rw-r--r--aoc23/util.go119
-rw-r--r--aoc23/util.py51
9 files changed, 260 insertions, 251 deletions
diff --git a/aoc23/Makefile b/aoc23/Makefile
deleted file mode 100644
index 7daf986..0000000
--- a/aoc23/Makefile
+++ /dev/null
@@ -1,17 +0,0 @@
-days = $(wildcard day*.py)
-
-.PHONY: $(days)
-
-all: checkfmt $(days)
-
-$(days): mypy
- python3 $@
-
-mypy:
- mypy --strict --pretty *.py
-
-fmt:
- black --quiet *.py
-
-checkfmt:
- black --check --diff --quiet *.py
diff --git a/aoc23/day01.py b/aoc23/day01.py
deleted file mode 100644
index d66248f..0000000
--- a/aoc23/day01.py
+++ /dev/null
@@ -1,61 +0,0 @@
-"""Day 1: Trebuchet?!"""
-
-from typing import List
-from util import assert2, file_input, text_input
-
-example_input = """
-1abc2
-pqr3stu8vwx
-a1b2c3d4e5f
-treb7uchet
-"""
-
-example_input2 = """
-two1nine
-eightwothree
-abcone2threexyz
-xtwone3four
-4nineeightseven2
-zoneight234
-7pqrstsixteen
-"""
-
-
-def find_digits(text: str, parse_words: bool = False) -> List[int]:
- "Find numerical or english digits in text"
- words = ("one", "two", "three", "four", "five", "six", "seven", "eight", "nine")
- ints = []
- buf = ""
- for c in text:
- if ord(c) >= 49 and ord(c) <= 57:
- ints.append(int(c))
- elif parse_words:
- buf += c
- for i, w in enumerate(words):
- if len(buf) >= len(w) and buf[len(buf) - len(w) :] == w:
- ints.append(i + 1)
- return ints
-
-
-def sum_calibration_values(values: List[str], parse_words: bool = False) -> int:
- s = 0
- for n in values:
- ints = find_digits(n, parse_words)
- s += (ints[0] * 10) + ints[-1]
- return s
-
-
-def day1_1(values: List[str]) -> int:
- return sum_calibration_values(values)
-
-
-assert2(142, day1_1(text_input(example_input, str)))
-assert2(55488, day1_1(file_input(1, str)))
-
-
-def day1_2(values: List[str]) -> int:
- return sum_calibration_values(values, parse_words=True)
-
-
-assert2(281, day1_2(text_input(example_input2, str)))
-assert2(55614, day1_2(file_input(1, str)))
diff --git a/aoc23/day01_test.go b/aoc23/day01_test.go
new file mode 100644
index 0000000..e3119ea
--- /dev/null
+++ b/aoc23/day01_test.go
@@ -0,0 +1,55 @@
+package aoc23
+
+import (
+ "io"
+ "testing"
+)
+
+func findDigits(text string, parseWords bool) []int {
+ words := []string{"one", "two", "three", "four", "five", "six", "seven", "eight", "nine"}
+ var ints []int
+ buf := ""
+ for _, c := range text {
+ if isDigit(c) {
+ n := requireInt(string(c))
+ ints = append(ints, n)
+ } else if parseWords {
+ buf += string(c)
+ for i, w := range words {
+ if len(buf) >= len(w) && buf[len(buf)-len(w):] == w {
+ ints = append(ints, i+1)
+ break
+ }
+ }
+ }
+ }
+ return ints
+}
+
+func sumCalibrations(r io.Reader, parseWords bool) int {
+ values := parseLines(r, func(s string) []int { return findDigits(s, parseWords) })
+ return sum(map2(values, func(ints []int) int { return (ints[0] * 10) + ints[len(ints)-1] }))
+}
+
+func TestDay01(t *testing.T) {
+ example := `
+1abc2
+pqr3stu8vwx
+a1b2c3d4e5f
+treb7uchet
+`
+ assert(t, 142, run(partial(sumCalibrations, false), inputString(example)))
+ assert(t, 55488, run(partial(sumCalibrations, false), inputFile(1)))
+
+ example2 := `
+two1nine
+eightwothree
+abcone2threexyz
+xtwone3four
+4nineeightseven2
+zoneight234
+7pqrstsixteen
+`
+ assert(t, 281, run(partial(sumCalibrations, true), inputString(example2)))
+ assert(t, 55614, run(partial(sumCalibrations, true), inputFile(1)))
+}
diff --git a/aoc23/day02.py b/aoc23/day02.py
deleted file mode 100644
index 3fc79e4..0000000
--- a/aoc23/day02.py
+++ /dev/null
@@ -1,68 +0,0 @@
-"""Day 2: Cube Conundrum"""
-
-from typing import List, Dict, Tuple, NamedTuple
-from util import text_input, file_input, assert2, product
-
-example_input = """
-Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green
-Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue
-Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red
-Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red
-Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green
-"""
-
-CubeSet = Dict[str, int]
-Rgb = Tuple[int, int, int]
-
-
-class Game(NamedTuple):
- id: int
- cubes: List[CubeSet]
-
-
-def parse_game(line: str) -> Game:
- parts = line.split(": ")
- game_id = int(parts[0][5:])
- cubes = []
- for p in parts[1].split("; "):
- c: CubeSet = {}
- for p in p.split(", "):
- parts = p.split(" ")
- count = int(parts[0])
- color = parts[1]
- c[color] = count
- cubes.append(c)
- return Game(id=game_id, cubes=cubes)
-
-
-def valid_game(game: Game, rgb: Rgb) -> bool:
- r, g, b = rgb
- return all(
- c.get("red", 0) <= r and c.get("green", 0) <= g and c.get("blue", 0) <= b
- for c in game.cubes
- )
-
-
-def min_cubes(game: Game) -> Rgb:
- r, g, b = 0, 0, 0
- for c in game.cubes:
- r = max(r, c.get("red", 0))
- g = max(g, c.get("green", 0))
- b = max(b, c.get("blue", 0))
- return (r, g, b)
-
-
-def day2_1(games: List[Game]) -> int:
- return sum(g.id for g in games if valid_game(g, (12, 13, 14)))
-
-
-assert2(8, day2_1(text_input(example_input, parse_game)))
-assert2(3035, day2_1(file_input(2, parse_game)))
-
-
-def day2_2(games: List[Game]) -> int:
- return sum(product(min_cubes(g)) for g in games)
-
-
-assert2(2286, day2_2(text_input(example_input, parse_game)))
-assert2(66027, day2_2(file_input(2, parse_game)))
diff --git a/aoc23/day02_test.go b/aoc23/day02_test.go
new file mode 100644
index 0000000..bf7aeda
--- /dev/null
+++ b/aoc23/day02_test.go
@@ -0,0 +1,83 @@
+package aoc23
+
+import (
+ "io"
+ "strings"
+ "testing"
+)
+
+type Game struct {
+ id int
+ cubes []Rgb
+}
+
+type Rgb struct{ r, g, b int }
+
+func parseGame(line string) Game {
+ parts := strings.Split(line, ": ")
+ id := requireInt(parts[0][5:])
+ var cubes []Rgb
+ for _, p := range strings.Split(parts[1], "; ") {
+ c := Rgb{}
+ for _, p2 := range strings.Split(p, ", ") {
+ parts := strings.Split(p2, " ")
+ count := requireInt(parts[0])
+ color := parts[1]
+ switch color {
+ case "red":
+ c.r = count
+ case "blue":
+ c.b = count
+ case "green":
+ c.g = count
+ }
+ }
+ cubes = append(cubes, c)
+ }
+ return Game{id: id, cubes: cubes}
+}
+
+func validGame(game Game, rgb Rgb) bool {
+ return noneMatch(game.cubes, func(cubes Rgb) bool {
+ return cubes.r > rgb.r || cubes.g > rgb.g || cubes.b > rgb.b
+ })
+}
+
+func countValidGames(r io.Reader) int {
+ games := parseLines(r, parseGame)
+ valid := filter(games, partial(validGame, Rgb{12, 13, 14}))
+ return sum(map2(valid, func(g Game) int { return g.id }))
+}
+
+func minCubes(game Game) Rgb {
+ r, g, b := 0, 0, 0
+ for _, c := range game.cubes {
+ r = max(r, c.r)
+ g = max(g, c.g)
+ b = max(b, c.b)
+ }
+ return Rgb{r, g, b}
+}
+
+func minCubesProduct(r io.Reader) int {
+ games := parseLines(r, parseGame)
+ products := map2(games, compose(compose(minCubes, func(rgb Rgb) []int {
+ return []int{rgb.r, rgb.g, rgb.b}
+ }), product))
+ return sum(products)
+}
+
+func TestDay02(t *testing.T) {
+ example := `
+Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green
+Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue
+Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red
+Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red
+Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green
+`
+ assert(t, 8, run(countValidGames, inputString(example)))
+ assert(t, 3035, run(countValidGames, inputFile(2)))
+
+ assert(t, 2286, run(minCubesProduct, inputString(example)))
+ assert(t, 66027, run(minCubesProduct, inputFile(2)))
+}
diff --git a/aoc23/gen.py b/aoc23/gen.py
deleted file mode 100755
index 8e90574..0000000
--- a/aoc23/gen.py
+++ /dev/null
@@ -1,54 +0,0 @@
-#!/usr/bin/env python3
-
-import sys
-
-
-template = """\"\"\"Day {day}: {description}\"\"\"
-
-from typing import List
-from util import text_input, file_input, assert2
-
-example_input = \"\"\"
-{example_input}
-\"\"\"
-
-
-def day{day}_1(lines: List[str]) -> int:
- return 0
-
-
-assert2(0, day{day}_1(text_input(example_input, str)))
-assert2(0, day{day}_1(file_input({day}, str)))
-
-
-def day{day}_2(lines: List[str]) -> int:
- return 0
-
-
-assert2(0, day{day}_2(text_input(example_input, str)))
-assert2(0, day{day}_2(file_input({day}, str)))
-"""
-
-
-def fail(msg: str) -> None:
- print(msg, file=sys.stderr)
- sys.exit(1)
-
-
-def main() -> None:
- if len(sys.argv) < 3:
- fail(sys.argv[0] + ": <day> <description>")
- day = int(sys.argv[1])
- desc = sys.argv[2]
- filepath = "day{0:02d}.py".format(day)
- example_input = sys.stdin.read().strip()
- with open(filepath, "x") as f:
- content = template.format(
- day=day, description=desc, example_input=example_input
- )
- f.write(content)
- print("wrote", filepath, file=sys.stderr)
-
-
-if __name__ == "__main__":
- main()
diff --git a/aoc23/go.mod b/aoc23/go.mod
new file mode 100644
index 0000000..6984070
--- /dev/null
+++ b/aoc23/go.mod
@@ -0,0 +1,3 @@
+module github.com/mpolden/aoc/aoc23
+
+go 1.20
diff --git a/aoc23/util.go b/aoc23/util.go
new file mode 100644
index 0000000..b4bb9ab
--- /dev/null
+++ b/aoc23/util.go
@@ -0,0 +1,119 @@
+package aoc23
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "os"
+ "strconv"
+ "strings"
+ "testing"
+)
+
+func assert(t *testing.T, want, got int) {
+ t.Helper()
+ if got != want {
+ t.Errorf("got %d, want %d", got, want)
+ }
+}
+
+func run(f func(r io.Reader) int, r io.Reader) int {
+ if rc, ok := r.(io.ReadCloser); ok {
+ defer rc.Close()
+ }
+ return f(r)
+}
+
+func partial[V1, V2, R any](f func(V1, V2) R, frozenArg V2) func(V1) R {
+ return func(v1 V1) R { return f(v1, frozenArg) }
+}
+
+func compose[V1, R1, R2 any](f1 func(V1) R1, f2 func(R1) R2) func(V1) R2 {
+ return func(v V1) R2 { return f2(f1(v)) }
+}
+
+func parseLines[T any](r io.Reader, parser func(line string) T) []T {
+ scanner := bufio.NewScanner(r)
+ var values []T
+ for scanner.Scan() {
+ line := strings.TrimSpace(scanner.Text())
+ values = append(values, parser(line))
+ }
+ return values
+}
+
+func inputFile(day int) *os.File {
+ f, err := os.Open(fmt.Sprintf("input/input%02d.txt", day))
+ if err != nil {
+ panic(err)
+ }
+ return f
+}
+
+func inputString(s string) io.Reader { return strings.NewReader(strings.TrimSpace(s)) }
+
+func requireInt(s string) int {
+ n, err := strconv.Atoi(s)
+ if err != nil {
+ panic(err)
+ }
+ return n
+}
+
+func isDigit(r rune) bool { return int(r) >= 48 && int(r) <= 57 }
+
+func max(a, b int) int {
+ if a > b {
+ return a
+ }
+ return b
+}
+
+func min(a, b int) int {
+ if a < b {
+ return a
+ }
+ return b
+}
+
+func add(a, b int) int { return a + b }
+
+func mul(a, b int) int { return a * b }
+
+func reduce[V any](values []V, f func(a, b V) V, initial V) V {
+ acc := initial
+ for _, v := range values {
+ acc = f(acc, v)
+ }
+ return acc
+}
+
+func map2[V any, R any](values []V, f func(v V) R) []R {
+ mapped := make([]R, len(values))
+ for i, v := range values {
+ mapped[i] = f(v)
+ }
+ return mapped
+}
+
+func filter[V any](values []V, pred func(v V) bool) []V {
+ var filtered []V
+ for _, v := range values {
+ if pred(v) {
+ filtered = append(filtered, v)
+ }
+ }
+ return filtered
+}
+
+func noneMatch[V any](values []V, pred func(v V) bool) bool {
+ return len(filter(values, pred)) == 0
+}
+
+func allMatch[V any](values []V, pred func(v V) bool) bool {
+ return len(filter(values, pred)) == len(values)
+}
+
+func product(ints []int) int { return reduce(ints, mul, 1) }
+
+func sum(ints []int) int { return reduce(ints, add, 0) }
diff --git a/aoc23/util.py b/aoc23/util.py
deleted file mode 100644
index 5310c1e..0000000
--- a/aoc23/util.py
+++ /dev/null
@@ -1,51 +0,0 @@
-import os.path
-import re
-
-from functools import reduce
-from typing import Callable, List, Optional, Iterable, TypeVar, Any
-
-T = TypeVar("T")
-Parser = Callable[[str], T]
-
-
-def assert2(want: Any, got: Any) -> None:
- """A better assert"""
- if got != want:
- raise AssertionError("got {}, want {}".format(repr(got), repr(want)))
-
-
-def file_input(day: int, parser: Parser[T], sep: Optional[str] = "\n") -> List[T]:
- "Read input file for given day, split it into lines and apply parser to each line"
- filename = os.path.join("input", "input{:02d}.txt".format(day))
- with open(filename) as f:
- return text_input(f.read(), parser, sep)
-
-
-def text_input(text: str, parser: Parser[T], sep: Optional[str] = "\n") -> List[T]:
- "Split text into lines and apply parser to each line"
- return [parser(line) for line in text.strip("\n").split(sep)]
-
-
-def ints(text: str) -> List[int]:
- "Find integers in text"
- return [int(n) for n in re.findall(r"-?[0-9]+", text)]
-
-
-def digits(text: str) -> List[int]:
- "Split text into individual digits"
- return [int(c) for c in text]
-
-
-def quantify(iterable: Iterable[T], pred: Callable[[T], bool]) -> int:
- "Count how many times pred is true for items in iterable"
- return sum(1 for item in iterable if pred(item))
-
-
-def product(iterable: Iterable[int]) -> int:
- "Product of all elements in iterable"
- return reduce(lambda acc, n: acc * n, iterable)
-
-
-def partition(items: List[T], size: int) -> List[List[T]]:
- "Partition list into sub-lists of given size"
- return [items[i : i + size] for i in range(0, len(items), size)]