diff options
author | Martin Polden <mpolden@mpolden.no> | 2023-12-08 15:50:19 +0100 |
---|---|---|
committer | Martin Polden <mpolden@mpolden.no> | 2023-12-08 16:28:47 +0100 |
commit | 7a08e0d00c7aa5cd976ab9a53bc65fe375072507 (patch) | |
tree | 3ba56cb54b2712711123270fc54e1825f1a6b088 /aoc23 | |
parent | b147937f39f66cab47005ac9af8562102c90e053 (diff) |
aoc23: switch to go
Diffstat (limited to 'aoc23')
-rw-r--r-- | aoc23/Makefile | 17 | ||||
-rw-r--r-- | aoc23/day01.py | 61 | ||||
-rw-r--r-- | aoc23/day01_test.go | 55 | ||||
-rw-r--r-- | aoc23/day02.py | 68 | ||||
-rw-r--r-- | aoc23/day02_test.go | 83 | ||||
-rwxr-xr-x | aoc23/gen.py | 54 | ||||
-rw-r--r-- | aoc23/go.mod | 3 | ||||
-rw-r--r-- | aoc23/util.go | 119 | ||||
-rw-r--r-- | aoc23/util.py | 51 |
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)] |