diff options
| author | DJ O'Leary <dijitol@proton.me> | 2026-06-26 18:04:09 +0200 |
|---|---|---|
| committer | DJ O'Leary <dijitol@proton.me> | 2026-06-26 18:04:09 +0200 |
| commit | 6d8fb621d1e93b83786890d6d9cb6f0d8defa8ae (patch) | |
| tree | 17c90d3984c114aa023600cc5bec6f6004b02243 /go/2023 | |
| parent | ad4ea5ca4bc9f7540ee1d6a4fa2d881709dc6459 (diff) | |
refactor!: invert repo directory structure
now is language first
Diffstat (limited to 'go/2023')
27 files changed, 2524 insertions, 0 deletions
diff --git a/go/2023/.envrc b/go/2023/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/go/2023/.envrc @@ -0,0 +1 @@ +use flake diff --git a/go/2023/.gitignore b/go/2023/.gitignore new file mode 100644 index 0000000..9f4cb5b --- /dev/null +++ b/go/2023/.gitignore @@ -0,0 +1,21 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +out/ + +# Go workspace file +go.work +go.work.sum diff --git a/go/2023/README.md b/go/2023/README.md new file mode 100644 index 0000000..9cda93b --- /dev/null +++ b/go/2023/README.md @@ -0,0 +1,21 @@ +# Go 2023 + +## Getting Started + +### How to Build + +```sh +task build +``` + +### How to Run + +```sh +task run +``` + +### How to Test + +```sh +task test +``` diff --git a/go/2023/Taskfile.yml b/go/2023/Taskfile.yml new file mode 100644 index 0000000..a07c24c --- /dev/null +++ b/go/2023/Taskfile.yml @@ -0,0 +1,23 @@ +# https://taskfile.dev + +version: "3" +tasks: + default: + desc: Prints this message + cmds: + - cmd: task --list-all + build: + cmds: + - cmd: go build -o=out/Advent_of_Code_2023 main.go + run: + cmds: + - cmd: go run main.go {{.CLI_ARGS}} + test: + cmds: + - cmd: go test ./... + test:coverage: + desc: Runs tests and produce coverage report + cmds: + - cmd: mkdir --parents out + - cmd: go test -coverprofile=out/coverage.out ./... + - cmd: go tool cover -html=out/coverage.out diff --git a/go/2023/flake.lock b/go/2023/flake.lock new file mode 100644 index 0000000..b7695d9 --- /dev/null +++ b/go/2023/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1741010256, + "narHash": "sha256-WZNlK/KX7Sni0RyqLSqLPbK8k08Kq7H7RijPJbq9KHM=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "ba487dbc9d04e0634c64e3b1f0d25839a0a68246", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/go/2023/flake.nix b/go/2023/flake.nix new file mode 100644 index 0000000..39dda89 --- /dev/null +++ b/go/2023/flake.nix @@ -0,0 +1,28 @@ +{ + description = "Advent of Code 2023: Go"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + }; + + outputs = + { nixpkgs, ... }: + let + pkgs = nixpkgs.legacyPackages.x86_64-linux; + in + { + devShells.x86_64-linux.default = pkgs.mkShell { + # build tools + nativeBuildInputs = with pkgs; [ + go + ]; + # dependencies + buildInputs = with pkgs; [ + delve + gopls + gotools + golangci-lint + ]; + }; + }; +} diff --git a/go/2023/go.mod b/go/2023/go.mod new file mode 100644 index 0000000..9fba23c --- /dev/null +++ b/go/2023/go.mod @@ -0,0 +1,11 @@ +module github.com/DJOLEARY/Advent_of_Code/2023/Go + +go 1.22.3 + +require github.com/outrigdev/outrig v0.8.2 + +require ( + github.com/google/uuid v1.6.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/term v0.29.0 // indirect +) diff --git a/go/2023/go.sum b/go/2023/go.sum new file mode 100644 index 0000000..3c60b26 --- /dev/null +++ b/go/2023/go.sum @@ -0,0 +1,8 @@ +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/outrigdev/outrig v0.8.2 h1:+qR+N6BXvJmFXsHmksaAJk4sgItxHLbWXN+9K8W2UVM= +github.com/outrigdev/outrig v0.8.2/go.mod h1:8ULH17Fc55s9zkf6S2ndQ43UoiJ0j28t9A0lwjs7rQc= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= diff --git a/go/2023/internal/day_01/day_01.go b/go/2023/internal/day_01/day_01.go new file mode 100644 index 0000000..9cd0a49 --- /dev/null +++ b/go/2023/internal/day_01/day_01.go @@ -0,0 +1,114 @@ +package day_01 + +import ( + "errors" + "fmt" + "os" + "strconv" + "strings" +) + +func Solve() { + filename := "../_input/day_01.txt" + content := readFile(filename) + + numbers := []int{} + + lines := strings.Split(content, "\n") + for _, line := range lines { + first_num, last_num, err := findNumbers(line) + if err != nil { + continue + } + + num_str := first_num + last_num + num := strToInt(num_str) + numbers = append(numbers, num) + } + + acc := accumulate(numbers) + + fmt.Println(acc) +} + +func readFile(filename string) string { + content, err := os.ReadFile(filename) + if err != nil { + panic(err) + } + + return string(content) +} + +var num_map = map[string]string{ + "zero": "0", + "0": "0", + "one": "1", + "1": "1", + "two": "2", + "2": "2", + "three": "3", + "3": "3", + "four": "4", + "4": "4", + "five": "5", + "5": "5", + "six": "6", + "6": "6", + "seven": "7", + "7": "7", + "eight": "8", + "8": "8", + "nine": "9", + "9": "9", +} + +func findNumbers(line string) (string, string, error) { + if line == "" || line == "\n" { + return "", "", errors.New("Empty line") + } + + found_numbers := []string{} + window_size := 5 + for i := range line { + window_end := i + window_size + if window_end > len(line) { + window_end = len(line) + } + sub_str := line[i:window_end] + + for key, value := range num_map { + if strings.HasPrefix(sub_str, key) { + found_numbers = append(found_numbers, value) + } + } + } + + if len(found_numbers) == 0 { + return "", "", errors.New("No numbers found") + } + + first := found_numbers[0] + last := found_numbers[len(found_numbers)-1] + + return first, last, nil +} + +func accumulate(numbers []int) int { + acc := 0 + + for _, num := range numbers { + acc += num + } + + return acc +} + +func strToInt(str string) int { + num, err := strconv.ParseInt(str, 0, 0) + if err != nil { + panic(err) + } + + return int(num) +} diff --git a/go/2023/internal/day_01/day_01_test.go b/go/2023/internal/day_01/day_01_test.go new file mode 100644 index 0000000..8063619 --- /dev/null +++ b/go/2023/internal/day_01/day_01_test.go @@ -0,0 +1,31 @@ +package day_01 + +import ( + "testing" +) + +type input string +type answer string + +func TestFindNumbers(t *testing.T) { + data := map[input]answer{ + "1abc2": "12", + "pqr3stu8vwx": "38", + "a1b2c3d4e5f": "15", + "treb7uchet": "77", + } + + for input, expected := range data { + actual_first, actual_last, err := findNumbers(string(input)) + + if err != nil { + t.Error("find_numbers returned an error") + } + + actual := actual_first + actual_last + + if actual != string(expected) { + t.Errorf("Expected %s, got %s", expected, actual) + } + } +} diff --git a/go/2023/internal/day_02/part_01/part_01.go b/go/2023/internal/day_02/part_01/part_01.go new file mode 100644 index 0000000..9efe20f --- /dev/null +++ b/go/2023/internal/day_02/part_01/part_01.go @@ -0,0 +1,138 @@ +package part_01 + +import ( + "os" + "regexp" + "strconv" + "strings" +) + +type cubes struct { + red int + blue int + green int +} + +func Solve() { + contents, err := os.ReadFile("../_input/day_02.txt") + if err != nil { + panic(err) + } + + lines := strings.Split(string(contents), "\n") + possibleGameIds := processLines(lines) + + println(accumulate(possibleGameIds)) +} + +func processLines(lines []string) []int { + var possibleGames []int + + maxCubes := cubes{red: 12, blue: 14, green: 13} + + for _, line := range lines { + if line == "" || line == "\n" { + continue + } + + shownCubes := convertLineToCubes(line) + + isPossible := isPossibleGame(shownCubes, maxCubes) + if isPossible { + gameId := getGameId(line) + possibleGames = append(possibleGames, gameId) + } + } + + return possibleGames +} + +func convertLineToCubes(line string) []cubes { + var shownCubes []cubes + + redRegex := regexp.MustCompile(`(\d+) red`) + blueRegex := regexp.MustCompile(`(\d+) blue`) + greenRegex := regexp.MustCompile(`(\d+) green`) + + for _, round := range strings.Split(line, ";") { + var cubes cubes + + redMatches := redRegex.FindStringSubmatch(round) + if redMatches != nil { + red, err := strconv.Atoi(redMatches[1]) + if err != nil { + panic("Could not convert red to int: " + redMatches[1]) + } else { + cubes.red = red + } + } + + blueMatches := blueRegex.FindStringSubmatch(round) + if blueMatches != nil { + blue, err := strconv.Atoi(blueMatches[1]) + if err != nil { + panic("Could not convert green to int: " + blueMatches[1]) + } else { + cubes.blue = blue + } + } + + greenMatches := greenRegex.FindStringSubmatch(round) + if greenMatches != nil { + green, err := strconv.Atoi(greenMatches[1]) + if err != nil { + panic("Could not convert green to int: " + greenMatches[1]) + } else { + cubes.green = green + } + } + + shownCubes = append(shownCubes, cubes) + } + + return shownCubes +} + +func isPossibleGame(shownCubes []cubes, maxCubes cubes) bool { + for _, cubes := range shownCubes { + if cubes.red > maxCubes.red { + return false + } + + if cubes.blue > maxCubes.blue { + return false + } + + if cubes.green > maxCubes.green { + return false + } + } + + return true +} + +func getGameId(line string) int { + regex := regexp.MustCompile(`^Game (\d+):`) + matches := regex.FindStringSubmatch(line) + if matches == nil || len(matches) < 2 { + println(matches) + panic("No game id found in line: '" + line + "'") + } + + gameId, err := strconv.Atoi(matches[1]) + if err != nil { + panic("Could not convert game id to int: " + matches[1]) + } + + return gameId +} + +func accumulate(gameIds []int) int { + var acc int + + for _, gameId := range gameIds { + acc += gameId + } + + return acc +} diff --git a/go/2023/internal/day_02/part_01/part_01_test.go b/go/2023/internal/day_02/part_01/part_01_test.go new file mode 100644 index 0000000..33e5fbf --- /dev/null +++ b/go/2023/internal/day_02/part_01/part_01_test.go @@ -0,0 +1,102 @@ +package part_01 + +import ( + "testing" +) + +type data struct { + gameId int + isPossible bool + shownCubes []cubes + line string +} + +var testInput = []data{ + { + gameId: 1, + isPossible: true, + shownCubes: []cubes{ + {red: 4, blue: 3}, + {red: 1, blue: 6, green: 2}, + {green: 2}, + }, + line: "Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green", + }, + { + gameId: 2, + isPossible: true, + shownCubes: []cubes{ + {blue: 1, green: 2}, + {red: 1, blue: 4, green: 3}, + {blue: 1, green: 1}, + }, + line: "Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue", + }, + { + gameId: 3, + isPossible: false, + shownCubes: []cubes{ + {red: 20, blue: 6, green: 8}, + {red: 4, blue: 5, green: 13}, + {red: 1, green: 5}, + }, + line: "Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red", + }, + { + gameId: 4, + isPossible: false, + shownCubes: []cubes{ + {red: 3, blue: 6, green: 1}, + {red: 6, green: 3}, + {red: 14, blue: 15, green: 3}, + }, + line: "Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red", + }, + { + gameId: 5, + isPossible: true, + shownCubes: []cubes{ + {red: 6, blue: 1, green: 3}, + {red: 1, blue: 2, green: 2}, + }, + line: "Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green", + }, +} + +func TestConvertLineToCubes(t *testing.T) { + for _, data := range testInput { + actualCubes := convertLineToCubes(data.line) + + if len(data.shownCubes) != len(actualCubes) { + t.Errorf("Expected length of shownCubes to be %d, got %d", len(data.shownCubes), len(actualCubes)) + } + + for i, cubes := range data.shownCubes { + if cubes != actualCubes[i] { + t.Errorf("Expected shownCubes to be %v, got %v", cubes, actualCubes[i]) + } + } + } +} + +func TestIsPossibleGame(t *testing.T) { + maxCubes := cubes{red: 12, blue: 13, green: 14} + + for _, data := range testInput { + actualIsPossible := isPossibleGame(data.shownCubes, maxCubes) + + if data.isPossible != actualIsPossible { + t.Errorf("Expected isPossibleGame to be %t, got %t for game id %d", data.isPossible, actualIsPossible, data.gameId) + } + } +} + +func TestGetGameId(t *testing.T) { + for _, data := range testInput { + actualGameId := getGameId(data.line) + + if data.gameId != actualGameId { + t.Errorf("Expected gameId to be %d, got %d", data.gameId, actualGameId) + } + } +} diff --git a/go/2023/internal/day_02/part_02/part_02.go b/go/2023/internal/day_02/part_02/part_02.go new file mode 100644 index 0000000..ffb9691 --- /dev/null +++ b/go/2023/internal/day_02/part_02/part_02.go @@ -0,0 +1,114 @@ +package part_02 + +import ( + "os" + "regexp" + "strconv" + "strings" +) + +type cubes struct { + red int + blue int + green int +} + +func Solve() { + contents, err := os.ReadFile("../_input/day_02.txt") + if err != nil { + panic(err) + } + + lines := strings.Split(string(contents), "\n") + answer := processLines(lines) + + println(answer) +} + +func processLines(lines []string) int { + var answer int + + for _, line := range lines { + if line == "" || line == "\n" { + continue + } + + shownCubes := convertLineToCubes(line) + + maxCubes := getMaximumCubes(shownCubes) + power := getPower(maxCubes) + answer += power + } + + return answer +} + +func convertLineToCubes(line string) []cubes { + var shownCubes []cubes + + redRegex := regexp.MustCompile(`(\d+) red`) + blueRegex := regexp.MustCompile(`(\d+) blue`) + greenRegex := regexp.MustCompile(`(\d+) green`) + + for _, round := range strings.Split(line, ";") { + var cubes cubes + + redMatches := redRegex.FindStringSubmatch(round) + if redMatches != nil { + red, err := strconv.Atoi(redMatches[1]) + if err != nil { + panic("Could not convert red to int: " + redMatches[1]) + } else { + cubes.red = red + } + } + + blueMatches := blueRegex.FindStringSubmatch(round) + if blueMatches != nil { + blue, err := strconv.Atoi(blueMatches[1]) + if err != nil { + panic("Could not convert green to int: " + blueMatches[1]) + } else { + cubes.blue = blue + } + } + + greenMatches := greenRegex.FindStringSubmatch(round) + if greenMatches != nil { + green, err := strconv.Atoi(greenMatches[1]) + if err != nil { + panic("Could not convert green to int: " + greenMatches[1]) + } else { + cubes.green = green + } + } + + shownCubes = append(shownCubes, cubes) + } + + return shownCubes +} + +func getMaximumCubes(shownCubes []cubes) cubes { + var maxCubes cubes + + for _, cubes := range shownCubes { + if cubes.red > maxCubes.red { + maxCubes.red = cubes.red + } + + if cubes.blue > maxCubes.blue { + maxCubes.blue = cubes.blue + } + + if cubes.green > maxCubes.green { + maxCubes.green = cubes.green + } + } + + return maxCubes +} + +func getPower(maxCubes cubes) int { + return maxCubes.red * maxCubes.blue * maxCubes.green +} diff --git a/go/2023/internal/day_02/part_02/part_02_test.go b/go/2023/internal/day_02/part_02/part_02_test.go new file mode 100644 index 0000000..6952899 --- /dev/null +++ b/go/2023/internal/day_02/part_02/part_02_test.go @@ -0,0 +1,84 @@ +package part_02 + +import ( + "testing" +) + +type data struct { + shownCubes []cubes + maxCubes cubes + line string +} + +var testInput = []data{ + { + shownCubes: []cubes{ + {red: 4, blue: 3}, + {red: 1, blue: 6, green: 2}, + {green: 2}, + }, + maxCubes: cubes{red: 4, blue: 6, green: 2}, + line: "Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green", + }, + { + shownCubes: []cubes{ + {blue: 1, green: 2}, + {red: 1, blue: 4, green: 3}, + {blue: 1, green: 1}, + }, + maxCubes: cubes{red: 1, blue: 4, green: 3}, + line: "Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue", + }, + { + shownCubes: []cubes{ + {red: 20, blue: 6, green: 8}, + {red: 4, blue: 5, green: 13}, + {red: 1, green: 5}, + }, + maxCubes: cubes{red: 20, blue: 6, green: 13}, + line: "Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red", + }, + { + shownCubes: []cubes{ + {red: 3, blue: 6, green: 1}, + {red: 6, green: 3}, + {red: 14, blue: 15, green: 3}, + }, + maxCubes: cubes{red: 14, blue: 15, green: 3}, + line: "Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red", + }, + { + shownCubes: []cubes{ + {red: 6, blue: 1, green: 3}, + {red: 1, blue: 2, green: 2}, + }, + maxCubes: cubes{red: 6, blue: 2, green: 3}, + line: "Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green", + }, +} + +func TestConvertLineToCubes(t *testing.T) { + for _, data := range testInput { + actualCubes := convertLineToCubes(data.line) + + if len(data.shownCubes) != len(actualCubes) { + t.Errorf("Expected length of shownCubes to be %d, got %d", len(data.shownCubes), len(actualCubes)) + } + + for i, cubes := range data.shownCubes { + if cubes != actualCubes[i] { + t.Errorf("Expected shownCubes to be %v, got %v", cubes, actualCubes[i]) + } + } + } +} + +func TestGetMaximumCubes(t *testing.T) { + for _, data := range testInput { + actualMaxCubes := getMaximumCubes(data.shownCubes) + + if actualMaxCubes != data.maxCubes { + t.Errorf("Expected maxCubes to be %v, got %v", data.maxCubes, actualMaxCubes) + } + } +} diff --git a/go/2023/internal/day_03/part_01/part_01.go b/go/2023/internal/day_03/part_01/part_01.go new file mode 100644 index 0000000..49837b3 --- /dev/null +++ b/go/2023/internal/day_03/part_01/part_01.go @@ -0,0 +1,123 @@ +package part_01 + +/** + * TRIED: + * 530923 + */ + +import ( + "os" + "strconv" + "strings" +) + +const USE_SAMPLE = true + +func Solve() { + var content []byte + if USE_SAMPLE { + content, _ = os.ReadFile("../_input/day_03_part_01_sample.txt") + } else { + content, _ = os.ReadFile("../_input/day_03.txt") + } + lines := strings.Split(string(content), "\n") + + validNumbers := walkLines(lines) + + answer := 0 + for _, number := range validNumbers { + answer += number + } + + println(answer) +} + +func walkLines(lines []string) []int { + var validNumbers []int + + for y := 0; y < len(lines); y++ { + line := lines[y] + + if line == "" || line == "\n" { + continue + } + + // Counts as a symbol... + filteredLine := strings.ReplaceAll(line, "\r", "") + + previousWasNumber := false + number := "" + for x := 0; x < len(filteredLine); x++ { + char := filteredLine[x] + + _, err := strconv.Atoi(string(char)) + isNumeric := err == nil + + if isNumeric { + previousWasNumber = true + number += string(char) + continue + } + + if !previousWasNumber { + continue + } + + isValid := checkValidity(lines, x, y, len(number)) + if isValid { + num, _ := strconv.Atoi(number) + validNumbers = append(validNumbers, num) + } + + println(number) + + previousWasNumber = false + number = "" + } + } + + return validNumbers +} + +// N = number +// k = numOfDigits + 1 +// (x-k,y-1) (x-1,y-1) ( x ,y-1) +// (x-k, y ) N ( x , y ) +// (x-k,y+1) (x-1,y+1) ( x ,y+1) +func checkValidity(lines []string, x int, y int, numOfDigits int) bool { + + for i := max(y-1, 0); i <= y+1; i++ { + + var debugLine string + + for j := max(x-(numOfDigits+1), 0); j <= x; j++ { + + if lineExists := len(lines) > i; !lineExists { + continue + } + + if charExists := len(lines[i]) > j; !charExists { + continue + } + + char := rune(lines[i][j]) + debugLine += string(char) + + if char == '.' { + continue + } + + _, err := strconv.Atoi(string(char)) + isNumeric := err == nil + if isNumeric { + continue + } + + debugLine = "" //nolint:ineffassign // Only used for debugging and needs to be reset + + return true + } + } + + return false +} diff --git a/go/2023/internal/day_03/part_01/part_01_test.go b/go/2023/internal/day_03/part_01/part_01_test.go new file mode 100644 index 0000000..a077fbd --- /dev/null +++ b/go/2023/internal/day_03/part_01/part_01_test.go @@ -0,0 +1,78 @@ +package part_01 + +import ( + "os" + "strings" + "testing" +) + +func TestWalkLines(t *testing.T) { + lines := getSampleLines() + expected := 4361 + + validNumbers := walkLines(lines) + + actual := 0 + for _, number := range validNumbers { + actual += number + } + + if actual != expected { + t.Errorf("Expected answer to be %d, got %d", expected, actual) + } +} + +func TestCheckValidity(t *testing.T) { + lines := getSampleLines() + + tests := []struct { + x int + y int + length int + expected bool + }{ + {3, 0, 3, true}, // 467 + {8, 0, 3, false}, // 114 + {9, 0, 1, false}, // 3 - Added to check for '\r' counting as a symbol + {4, 2, 2, true}, // 35 + {9, 2, 3, true}, // 633 + {9, 3, 1, false}, // 3 - Added to check if number continues to next line + {3, 4, 3, true}, // 617 + {9, 5, 2, false}, // 58 + {5, 6, 3, true}, // 592 + {9, 7, 3, true}, // 775 + {4, 9, 3, true}, // 664 + {8, 9, 3, true}, // 598 + } + + for _, test := range tests { + actual := checkValidity(lines, test.x, test.y, test.length) + + if actual != test.expected { + t.Errorf("Expected %d, %d, %d to be %t, got %t", test.x, test.y, test.length, test.expected, actual) + } + } +} + +func getSampleLines() []string { + contents, err := os.ReadFile("../../../../_input/day_03_part_01_sample.txt") + if err != nil { + panic(err) + } + + lines := strings.Split(string(contents), "\n") + + var filteredLines []string + for _, line := range lines { + if line == "" || line == "\n" { + continue + } + + // Counts as a symbol... + filteredLine := strings.ReplaceAll(line, "\r", "") + + filteredLines = append(filteredLines, filteredLine) + } + + return filteredLines +} diff --git a/go/2023/internal/day_04/part_01/part_01.go b/go/2023/internal/day_04/part_01/part_01.go new file mode 100644 index 0000000..1a215b1 --- /dev/null +++ b/go/2023/internal/day_04/part_01/part_01.go @@ -0,0 +1,130 @@ +package part_01 + +import ( + "os" + "slices" + "strconv" + "strings" +) + +const USE_SAMPLE = false + +type UnparsedCard struct { + id string + winningNumbers []string + haveNumbers []string +} + +type Card struct { + id int + winningNumbers []int + haveNumbers []int +} + +func (c UnparsedCard) parse() Card { + var card Card + + id, err := strconv.Atoi(c.id) + if err != nil { + panic(err) + } + card.id = id + + for _, number := range c.winningNumbers { + num, err := strconv.Atoi(number) + if err != nil { + panic(err) + } + card.winningNumbers = append(card.winningNumbers, num) + } + + for _, number := range c.haveNumbers { + num, err := strconv.Atoi(number) + if err != nil { + panic(err) + } + card.haveNumbers = append(card.haveNumbers, num) + } + + return card +} + +func Solve() { + var contents []byte + if USE_SAMPLE { + contents, _ = os.ReadFile("../_input/day_04_part_01_sample.txt") + } else { + contents, _ = os.ReadFile("../_input/day_04.txt") + } + + lines := strings.Split(string(contents), "\n") + + answer := 0 + for _, line := range lines { + if line == "" || line == "\n" { + continue + } + + card := parseCardLine(line) + score := tallyScore(card) + + answer += score + } + + println(answer) +} + +// "Card 1: 41 48 83 86 17 | 83 86 6 31 17 9 48 53" +func parseCardLine(line string) Card { + var unparsedCard UnparsedCard + + filteredLine := strings.ReplaceAll(line, "\r", "") + + // "Card 1" + card_parts := strings.Split(filteredLine, ":")[0] + id_parts := strings.Split(card_parts, " ") + id := id_parts[len(id_parts)-1] + unparsedCard.id = strings.ReplaceAll(id, " ", "") + + // "41 48 83 86 17 | 83 86 6 31 17 9 48 53" + numbers := strings.Split(filteredLine, ":")[1] + + // " 41 48 83 86 17" + winningNumberString := strings.Split(numbers, "|")[0] + // ["", "41", "48", "83", "86", "17"] + winningNumbers := strings.Split(winningNumberString, " ") + for _, num := range winningNumbers { + if num == "" || num == " " { + continue + } + unparsedCard.winningNumbers = append(unparsedCard.winningNumbers, num) + } + + // " 83 86 6 31 17 9 48 53" + haveNumberString := strings.Split(numbers, "|")[1] + // ["", "83", "86", "", "6", "31", "17", "", "9", "48", "53"] + haveNumbers := strings.Split(haveNumberString, " ") + for _, num := range haveNumbers { + if num == "" || num == " " { + continue + } + unparsedCard.haveNumbers = append(unparsedCard.haveNumbers, num) + } + + return unparsedCard.parse() +} + +func tallyScore(card Card) int { + score := 0 + for _, winningNumber := range card.winningNumbers { + if slices.Contains(card.haveNumbers, winningNumber) { + if score == 0 { + score = 1 + } else { + score *= 2 + } + } + } + + return score +} diff --git a/go/2023/internal/day_04/part_01/part_01_test.go b/go/2023/internal/day_04/part_01/part_01_test.go new file mode 100644 index 0000000..f6f1a83 --- /dev/null +++ b/go/2023/internal/day_04/part_01/part_01_test.go @@ -0,0 +1,48 @@ +package part_01 + +import ( + "testing" +) + +func TestParseCardLine(t *testing.T) { + input := "Card 1: 41 48 83 86 17 | 83 86 6 31 17 9 48 53" + expected := Card{1, []int{41, 48, 83, 86, 17}, []int{83, 86, 6, 31, 17, 9, 48, 53}} + + actual := parseCardLine(input) + + if actual.id != expected.id { + t.Errorf("ERROR: ID %d does not match expected %d", actual.id, expected.id) + } + + if len(actual.winningNumbers) != len(expected.winningNumbers) { + t.Errorf("ERROR: len(WinningNumbers) %d does not match expected %d", len(actual.winningNumbers), len(expected.winningNumbers)) + } + + if len(actual.haveNumbers) != len(expected.haveNumbers) { + t.Errorf("ERROR: len(HaveNumbers) %d does not match expected %d", len(actual.haveNumbers), len(expected.haveNumbers)) + } + + for i, num := range actual.winningNumbers { + if num != expected.winningNumbers[i] { + t.Errorf("ERROR: WinningNumber %d does not match expected %d", num, expected.winningNumbers[i]) + } + } + + for i, num := range actual.haveNumbers { + if num != expected.haveNumbers[i] { + t.Errorf("ERROR: HaveNumber %d does not match expected %d", num, expected.haveNumbers[i]) + } + } + +} + +func TestTallyScore(t *testing.T) { + card := Card{1, []int{41, 48, 83, 86, 17}, []int{83, 86, 6, 31, 17, 9, 48, 53}} + expected := 8 + + actual := tallyScore(card) + + if actual != expected { + t.Errorf("ERROR: Score %d does not match expected %d", actual, expected) + } +} diff --git a/go/2023/internal/day_04/part_02/part_02.go b/go/2023/internal/day_04/part_02/part_02.go new file mode 100644 index 0000000..a9e3152 --- /dev/null +++ b/go/2023/internal/day_04/part_02/part_02.go @@ -0,0 +1,142 @@ +package part_02 + +import ( + "os" + "slices" + "strconv" + "strings" +) + +const USE_SAMPLE = false + +type UnparsedCard struct { + id string + winningNumbers []string + haveNumbers []string +} + +type Card struct { + id int + winningNumbers []int + haveNumbers []int +} + +func (c UnparsedCard) parse() Card { + var card Card + + id, err := strconv.Atoi(c.id) + if err != nil { + panic(err) + } + card.id = id + + for _, number := range c.winningNumbers { + num, err := strconv.Atoi(number) + if err != nil { + panic(err) + } + card.winningNumbers = append(card.winningNumbers, num) + } + + for _, number := range c.haveNumbers { + num, err := strconv.Atoi(number) + if err != nil { + panic(err) + } + card.haveNumbers = append(card.haveNumbers, num) + } + + return card +} + +func Solve() { + var contents []byte + if USE_SAMPLE { + contents, _ = os.ReadFile("../_input/day_04_part_02_sample.txt") + } else { + contents, _ = os.ReadFile("../_input/day_04.txt") + } + + lines := strings.Split(string(contents), "\n") + + dict := map[int]Card{} + var cards []Card + for _, line := range lines { + if line == "" || line == "\n" { + continue + } + + card := parseCardLine(line) + + dict[card.id] = card + cards = append(cards, card) + } + + answer := processCards(dict, cards) + + println(answer) +} + +// "Card 1: 41 48 83 86 17 | 83 86 6 31 17 9 48 53" +func parseCardLine(line string) Card { + var unparsedCard UnparsedCard + + filteredLine := strings.ReplaceAll(line, "\r", "") + + // "Card 1" + card_parts := strings.Split(filteredLine, ":")[0] + id_parts := strings.Split(card_parts, " ") + id := id_parts[len(id_parts)-1] + unparsedCard.id = strings.ReplaceAll(id, " ", "") + + // "41 48 83 86 17 | 83 86 6 31 17 9 48 53" + numbers := strings.Split(filteredLine, ":")[1] + + // " 41 48 83 86 17" + winningNumberString := strings.Split(numbers, "|")[0] + // ["", "41", "48", "83", "86", "17"] + winningNumbers := strings.Split(winningNumberString, " ") + for _, num := range winningNumbers { + if num == "" || num == " " { + continue + } + unparsedCard.winningNumbers = append(unparsedCard.winningNumbers, num) + } + + // " 83 86 6 31 17 9 48 53" + haveNumberString := strings.Split(numbers, "|")[1] + // ["", "83", "86", "", "6", "31", "17", "", "9", "48", "53"] + haveNumbers := strings.Split(haveNumberString, " ") + for _, num := range haveNumbers { + if num == "" || num == " " { + continue + } + unparsedCard.haveNumbers = append(unparsedCard.haveNumbers, num) + } + + return unparsedCard.parse() +} + +func processCards(dict map[int]Card, cards []Card) int { + for i := 0; i < len(cards); i++ { + card := cards[i] + matchCount := calculateNumberOfMatches(card) + for j := card.id + 1; j < (card.id + 1 + matchCount); j++ { + copyCard := dict[j] + cards = append(cards, copyCard) + } + } + + return len(cards) +} + +func calculateNumberOfMatches(card Card) int { + matchCount := 0 + for _, winningNumber := range card.winningNumbers { + if slices.Contains(card.haveNumbers, winningNumber) { + matchCount++ + } + } + + return matchCount +} diff --git a/go/2023/internal/day_04/part_02/part_02_test.go b/go/2023/internal/day_04/part_02/part_02_test.go new file mode 100644 index 0000000..3c66a2e --- /dev/null +++ b/go/2023/internal/day_04/part_02/part_02_test.go @@ -0,0 +1,143 @@ +package part_02 + +import ( + "testing" +) + +func TestParseCardLine(t *testing.T) { + input := "Card 1: 41 48 83 86 17 | 83 86 6 31 17 9 48 53" + expected := Card{1, []int{41, 48, 83, 86, 17}, []int{83, 86, 6, 31, 17, 9, 48, 53}} + + actual := parseCardLine(input) + + if actual.id != expected.id { + t.Errorf("ERROR: ID %d does not match expected %d", actual.id, expected.id) + } + + if len(actual.winningNumbers) != len(expected.winningNumbers) { + t.Errorf("ERROR: len(WinningNumbers) %d does not match expected %d", len(actual.winningNumbers), len(expected.winningNumbers)) + } + + if len(actual.haveNumbers) != len(expected.haveNumbers) { + t.Errorf("ERROR: len(HaveNumbers) %d does not match expected %d", len(actual.haveNumbers), len(expected.haveNumbers)) + } + + for i, num := range actual.winningNumbers { + if num != expected.winningNumbers[i] { + t.Errorf("ERROR: WinningNumber %d does not match expected %d", num, expected.winningNumbers[i]) + } + } + + for i, num := range actual.haveNumbers { + if num != expected.haveNumbers[i] { + t.Errorf("ERROR: HaveNumber %d does not match expected %d", num, expected.haveNumbers[i]) + } + } + +} + +func TestProcessCards(t *testing.T) { + cards := []Card{ + // Card 1: 41 48 83 86 17 | 83 86 6 31 17 9 48 53 + { + 1, + []int{41, 48, 83, 86, 17}, + []int{83, 86, 6, 31, 17, 9, 48, 53}, + }, + // Card 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19 + { + 2, + []int{13, 32, 20, 16, 61}, + []int{61, 30, 68, 82, 17, 32, 24, 19}, + }, + // Card 3: 1 21 53 59 44 | 69 82 63 72 16 21 14 1 + { + 3, + []int{1, 21, 53, 59, 44}, + []int{69, 82, 63, 72, 16, 21, 14, 1}, + }, + // Card 4: 41 92 73 84 69 | 59 84 76 51 58 5 54 83 + { + 4, + []int{41, 92, 73, 84, 69}, + []int{59, 84, 76, 51, 58, 5, 54, 83}, + }, + // Card 5: 87 83 26 28 32 | 88 30 70 12 93 22 82 36 + { + 5, + []int{87, 83, 26, 28, 32}, + []int{88, 30, 70, 12, 93, 22, 82, 36}, + }, + // Card 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11 + { + 6, + []int{31, 18, 13, 56, 72}, + []int{74, 77, 10, 23, 35, 67, 36, 11}, + }, + } + dict := map[int]Card{} + for _, card := range cards { + dict[card.id] = card + } + expected := 30 + + actual := processCards(dict, cards) + + if actual != expected { + t.Errorf("ERROR: Total number of cards %d does not match expected %d", actual, expected) + } +} + +func TestCalculateNumberOfMatches(t *testing.T) { + cards := []Card{ + // Card 1: 41 48 83 86 17 | 83 86 6 31 17 9 48 53 + { + 1, + []int{41, 48, 83, 86, 17}, + []int{83, 86, 6, 31, 17, 9, 48, 53}, + }, + // Card 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19 + { + 2, + []int{13, 32, 20, 16, 61}, + []int{61, 30, 68, 82, 17, 32, 24, 19}, + }, + // Card 3: 1 21 53 59 44 | 69 82 63 72 16 21 14 1 + { + 3, + []int{1, 21, 53, 59, 44}, + []int{69, 82, 63, 72, 16, 21, 14, 1}, + }, + // Card 4: 41 92 73 84 69 | 59 84 76 51 58 5 54 83 + { + 4, + []int{41, 92, 73, 84, 69}, + []int{59, 84, 76, 51, 58, 5, 54, 83}, + }, + // Card 5: 87 83 26 28 32 | 88 30 70 12 93 22 82 36 + { + 5, + []int{87, 83, 26, 28, 32}, + []int{88, 30, 70, 12, 93, 22, 82, 36}, + }, + // Card 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11 + { + 6, + []int{31, 18, 13, 56, 72}, + []int{74, 77, 10, 23, 35, 67, 36, 11}, + }, + } + expected := 9 + + total := 0 + for _, card := range cards { + matches := calculateNumberOfMatches(card) + total += matches + + cards = append(cards, card) + } + + if total != expected { + t.Errorf("ERROR: Score %d does not match expected %d", total, expected) + } +} diff --git a/go/2023/internal/day_06/part_01/part_01.go b/go/2023/internal/day_06/part_01/part_01.go new file mode 100644 index 0000000..5735924 --- /dev/null +++ b/go/2023/internal/day_06/part_01/part_01.go @@ -0,0 +1,85 @@ +package part_01 + +import ( + "os" + "strconv" + "strings" +) + +const USE_SAMPLE = false + +/** + * Tried: + * - 72128 (too low) + * - 3317888 (correct!) + */ +func Solve() { + var filePath string + if USE_SAMPLE { + filePath = "../_input/day_06_part_01_sample.txt" + } else { + filePath = "../_input/day_06.txt" + } + contents, _ := os.ReadFile(filePath) + + lines := strings.Split(string(contents), "\r\n") + + timeLine := lines[0] + times := parseTimeLine(timeLine) + + distLine := lines[1] + distances := parseDistanceLine(distLine) + + answer := calculateAnswer(times, distances) + + println(answer) +} + +func parseTimeLine(timeLine string) []int { + prefix := "Time:" + return parseLine(prefix, timeLine) +} + +func parseDistanceLine(distLine string) []int { + prefix := "Distance:" + return parseLine(prefix, distLine) +} + +func parseLine(prefix string, line string) []int { + withoutTitle := strings.TrimPrefix(line, prefix) + + numbers := []int{} + for _, number := range strings.Split(withoutTitle, " ") { + if number == "" { + continue + } + numberInt, _ := strconv.Atoi(number) + numbers = append(numbers, numberInt) + } + return numbers +} + +func calculateAnswer(times []int, distances []int) int { + answer := 1 + for i, maxTime := range times { + recordDistance := distances[i] + + var greaterThanRecord []int + for buttonPressedFor := 0; buttonPressedFor <= maxTime; buttonPressedFor++ { + distance := calculateDistance(buttonPressedFor, maxTime) + if distance > recordDistance { + greaterThanRecord = append(greaterThanRecord, distance) + } + } + + if len(greaterThanRecord) > 0 { + answer *= len(greaterThanRecord) + } + } + + return answer +} + +func calculateDistance(buttonPressedFor int, maxTime int) int { + return buttonPressedFor * (maxTime - buttonPressedFor) +} diff --git a/go/2023/internal/day_06/part_01/part_01_test.go b/go/2023/internal/day_06/part_01/part_01_test.go new file mode 100644 index 0000000..7af828c --- /dev/null +++ b/go/2023/internal/day_06/part_01/part_01_test.go @@ -0,0 +1,96 @@ +package part_01 + +import "testing" + +func TestParseTimeLine(t *testing.T) { + tests := map[string]struct { + input string + expected []int + }{ + "empty": {"", []int{}}, + "sample": {"Time: 7 15 30", []int{7, 15, 30}}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + actual := parseTimeLine(test.input) + if len(actual) != len(test.expected) { + t.Errorf("Expected %d, got %d", len(test.expected), len(actual)) + } + for i := 0; i < len(actual); i++ { + if actual[i] != test.expected[i] { + t.Errorf("Expected %d, got %d", test.expected[i], actual[i]) + } + } + }) + } +} + +func TestParseDistLine(t *testing.T) { + tests := map[string]struct { + input string + expected []int + }{ + "empty": {"", []int{}}, + "sample": {"Distance: 9 40 200", []int{9, 40, 200}}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + actual := parseDistanceLine(test.input) + if len(actual) != len(test.expected) { + t.Errorf("Expected %d, got %d", len(test.expected), len(actual)) + } + for i := 0; i < len(actual); i++ { + if actual[i] != test.expected[i] { + t.Errorf("Expected %d, got %d", test.expected[i], actual[i]) + } + } + }) + } +} + +func TestCalculateAnswer(t *testing.T) { + tests := map[string]struct { + times []int + distances []int + expected int + }{ + "sample": {[]int{7, 15, 30}, []int{9, 40, 200}, 288}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + actual := calculateAnswer(test.times, test.distances) + if actual != test.expected { + t.Errorf("Expected %d, got %d", test.expected, actual) + } + }) + } +} + +func TestCalculateDistance(t *testing.T) { + tests := map[string]struct { + maxTime int + buttonPressedFor int + expected int + }{ + "7_7": {7, 7, 0}, + "7_6": {7, 6, 6}, + "7_5": {7, 5, 10}, + "7_4": {7, 4, 12}, + "7_3": {7, 3, 12}, + "7_2": {7, 2, 10}, + "7_1": {7, 1, 6}, + "7_0": {7, 0, 0}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + actual := calculateDistance(test.buttonPressedFor, test.maxTime) + if actual != test.expected { + t.Errorf("Expected %d, got %d", test.expected, actual) + } + }) + } +} diff --git a/go/2023/internal/day_06/part_02/part_02.go b/go/2023/internal/day_06/part_02/part_02.go new file mode 100644 index 0000000..316b169 --- /dev/null +++ b/go/2023/internal/day_06/part_02/part_02.go @@ -0,0 +1,81 @@ +package part_02 + +import ( + "os" + "strconv" + "strings" +) + +const USE_SAMPLE = false + +/** + * Tried: + * - 24655068 (correct!) + */ +func Solve() { + var filePath string + if USE_SAMPLE { + filePath = "../_input/day_06_part_02_sample.txt" + } else { + filePath = "../_input/day_06.txt" + } + contents, _ := os.ReadFile(filePath) + + lines := strings.Split(string(contents), "\r\n") + + timeLine := lines[0] + time := parseTimeLine(timeLine) + + distLine := lines[1] + distance := parseDistanceLine(distLine) + + answer := calculateAnswer(time, distance) + + println(answer) +} + +func parseTimeLine(timeLine string) int { + prefix := "Time:" + return parseLine(prefix, timeLine) +} + +func parseDistanceLine(distLine string) int { + prefix := "Distance:" + return parseLine(prefix, distLine) +} + +func parseLine(prefix string, line string) int { + withoutTitle := strings.TrimPrefix(line, prefix) + + numbers := []string{} + for _, number := range strings.Split(withoutTitle, " ") { + if number == "" { + continue + } + numbers = append(numbers, number) + } + + number := strings.Join(numbers, "") + numberInt, err := strconv.Atoi(number) + if err != nil { + panic(err) + } + + return numberInt +} + +func calculateAnswer(maxTime int, recordDistance int) int { + var greaterThanRecord []int + for buttonPressedFor := 0; buttonPressedFor <= maxTime; buttonPressedFor++ { + distance := calculateDistance(buttonPressedFor, maxTime) + if distance > recordDistance { + greaterThanRecord = append(greaterThanRecord, distance) + } + } + + return len(greaterThanRecord) +} + +func calculateDistance(buttonPressedFor int, maxTime int) int { + return buttonPressedFor * (maxTime - buttonPressedFor) +} diff --git a/go/2023/internal/day_06/part_02/part_02_test.go b/go/2023/internal/day_06/part_02/part_02_test.go new file mode 100644 index 0000000..67924dc --- /dev/null +++ b/go/2023/internal/day_06/part_02/part_02_test.go @@ -0,0 +1,84 @@ +package part_02 + +import "testing" + +func TestParseTimeLine(t *testing.T) { + tests := map[string]struct { + input string + expected int + }{ + "sample": {"Time: 7 15 30", 71530}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + actual := parseTimeLine(test.input) + if actual != test.expected { + t.Errorf("Expected %d, got %d", test.expected, actual) + } + }) + } +} + +func TestParseDistLine(t *testing.T) { + tests := map[string]struct { + input string + expected int + }{ + "sample": {"Distance: 9 40 200", 940200}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + actual := parseDistanceLine(test.input) + if actual != test.expected { + t.Errorf("Expected %d, got %d", test.expected, actual) + } + }) + } +} + +func TestCalculateAnswer(t *testing.T) { + tests := map[string]struct { + maxTime int + recordDistance int + expected int + }{ + "sample": {71530, 940200, 71503}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + actual := calculateAnswer(test.maxTime, test.recordDistance) + if actual != test.expected { + t.Errorf("Expected %d, got %d", test.expected, actual) + } + }) + } +} + +func TestCalculateDistance(t *testing.T) { + tests := map[string]struct { + maxTime int + buttonPressedFor int + expected int + }{ + "7_7": {7, 7, 0}, + "7_6": {7, 6, 6}, + "7_5": {7, 5, 10}, + "7_4": {7, 4, 12}, + "7_3": {7, 3, 12}, + "7_2": {7, 2, 10}, + "7_1": {7, 1, 6}, + "7_0": {7, 0, 0}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + actual := calculateDistance(test.buttonPressedFor, test.maxTime) + if actual != test.expected { + t.Errorf("Expected %d, got %d", test.expected, actual) + } + }) + } +} diff --git a/go/2023/internal/day_07/part_01/part_01.go b/go/2023/internal/day_07/part_01/part_01.go new file mode 100644 index 0000000..a485dfd --- /dev/null +++ b/go/2023/internal/day_07/part_01/part_01.go @@ -0,0 +1,273 @@ +package part_01 + +import ( + "os" + "strconv" + "strings" +) + +const USE_SAMPLE = false + +type handType int + +const ( + HIGH_CARD handType = iota + PAIR + TWO_PAIR + THREE_OF_A_KIND + FULL_HOUSE + FOUR_OF_A_KIND + FIVE_OF_A_KIND +) + +func (h handType) String() string { + return [...]string{"HIGH_CARD", "PAIR", "TWO_PAIR", "THREE_OF_A_KIND", "FULL_HOUSE", "FOUR_OF_A_KIND", "FIVE_OF_A_KIND"}[h] +} + +type cardType int + +const ( + TWO cardType = iota + THREE + FOUR + FIVE + SIX + SEVEN + EIGHT + NINE + TEN + JACK + QUEEN + KING + ACE +) + +func (c cardType) String() string { + return [...]string{"TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN", "EIGHT", "NINE", "TEN", "JACK", "QUEEN", "KING", "ACE"}[c] +} + +type hand struct { + strength handType + cards []cardType + bet int +} + +/** + * Tried: + * - 252489894 (too high) + * - 229176554 (too low) + * - 200253108 (too low) + */ +func Solve() { + var filePath string + if USE_SAMPLE { + filePath = "../_input/day_07_part_01_sample.txt" + } else { + filePath = "../_input/day_07.txt" + } + contents, err := os.ReadFile(filePath) + if err != nil { + panic(err) + } + + lines := strings.Split(string(contents), "\r\n") + var hands []hand + for _, line := range lines { + if line == "" { + continue + } + + hands = append(hands, parseLine(line)) + } + + mappedByStrength := mapByStrength(hands) + + sortedByHighest := make(map[handType][]hand) + for _, strength := range []handType{HIGH_CARD, PAIR, TWO_PAIR, THREE_OF_A_KIND, FULL_HOUSE, FOUR_OF_A_KIND, FIVE_OF_A_KIND} { + if _, ok := mappedByStrength[strength]; !ok { + continue + } + + toBeSorted := mappedByStrength[strength] + sortedByHighest[strength] = sortByHighest(toBeSorted) + } + + flattened := flatten(sortedByHighest) + + answer := calculateWinnings(flattened) + + println("Answer: ", answer) +} + +func parseLine(line string) hand { + parts := strings.Split(line, " ") + + unparsedCards := strings.Split(parts[0], "") + cards := parseCards(unparsedCards) + strength := calculateStrength(cards) + + bet, err := strconv.Atoi(parts[1]) + if err != nil { + panic(err) + } + + return hand{ + strength, + cards, + bet, + } +} + +func parseCards(unparsedCards []string) []cardType { + var cards []cardType + for _, card := range unparsedCards { + switch card { + case "2": + cards = append(cards, TWO) + case "3": + cards = append(cards, THREE) + case "4": + cards = append(cards, FOUR) + case "5": + cards = append(cards, FIVE) + case "6": + cards = append(cards, SIX) + case "7": + cards = append(cards, SEVEN) + case "8": + cards = append(cards, EIGHT) + case "9": + cards = append(cards, NINE) + case "T": + cards = append(cards, TEN) + case "J": + cards = append(cards, JACK) + case "Q": + cards = append(cards, QUEEN) + case "K": + cards = append(cards, KING) + case "A": + cards = append(cards, ACE) + } + } + return cards +} + +func calculateStrength(cards []cardType) handType { + strength := map[cardType]int{} + for _, card := range cards { + if _, ok := strength[card]; !ok { + strength[card] = 0 + } + strength[card]++ + } + + var pairs int + var hasThreeOfAKind bool + for _, count := range strength { + if count == 5 { + return FIVE_OF_A_KIND + } + + if count == 4 { + return FOUR_OF_A_KIND + } + + if count == 3 { + hasThreeOfAKind = true + } + + if count == 2 { + pairs++ + } + } + + if hasThreeOfAKind && pairs == 1 { + return FULL_HOUSE + } + + if hasThreeOfAKind { + return THREE_OF_A_KIND + } + + if pairs == 2 { + return TWO_PAIR + } + + if pairs == 1 { + return PAIR + } + + return HIGH_CARD +} + +func mapByStrength(hands []hand) map[handType][]hand { + + handsByStrength := make(map[handType][]hand) + for _, hand := range hands { + handsByStrength[hand.strength] = append(handsByStrength[hand.strength], hand) + } + + return handsByStrength +} + +// low to high +func sortByHighest(hands []hand) []hand { + var partition = func(arr []hand, low, high int) ([]hand, int) { + pivot := arr[high] + index := low + + for i := low; i < high; i++ { + // Always 5 cards + for j := 0; j < 5; j++ { + if arr[i].cards[j] < pivot.cards[j] { + arr[i], arr[index] = arr[index], arr[i] + index++ + break + } + + if arr[i].cards[j] != pivot.cards[j] { + break + } + } + } + + arr[high], arr[index] = arr[index], arr[high] + + return arr, index + } + + var quickSort func(arr []hand, low, high int) []hand + quickSort = func(arr []hand, low, high int) []hand { + if low < high { + arr, pivot := partition(arr, low, high) + + quickSort(arr, low, pivot-1) + quickSort(arr, pivot+1, high) + } + + return arr + } + + hands = quickSort(hands, 0, len(hands)-1) + + return hands +} + +func flatten(hands map[handType][]hand) []hand { + var flat []hand + for _, handType := range []handType{HIGH_CARD, PAIR, TWO_PAIR, THREE_OF_A_KIND, FULL_HOUSE, FOUR_OF_A_KIND, FIVE_OF_A_KIND} { + flat = append(flat, hands[handType]...) + } + return flat +} + +func calculateWinnings(hands []hand) int { + var winnings int + for i, hand := range hands { + earned := hand.bet * (i + 1) + winnings += earned + println("hand ", i, ": { strength: ", hand.strength.String(), ", cards: [", hand.cards[0], hand.cards[1], hand.cards[2], hand.cards[3], hand.cards[4], "], earned: ", earned, " }") + } + return winnings +} diff --git a/go/2023/internal/day_07/part_01/part_01_test.go b/go/2023/internal/day_07/part_01/part_01_test.go new file mode 100644 index 0000000..6ceabe8 --- /dev/null +++ b/go/2023/internal/day_07/part_01/part_01_test.go @@ -0,0 +1,432 @@ +package part_01 + +import "testing" + +func TestParseLine(t *testing.T) { + tests := map[string]struct { + input string + expected hand + }{ + "sample_1": { + input: "32T3K 765", + expected: hand{ + strength: PAIR, + cards: []cardType{THREE, TWO, TEN, THREE, KING}, + bet: 765, + }, + }, + "sample_2": { + input: "T55J5 684", + expected: hand{ + strength: THREE_OF_A_KIND, + cards: []cardType{TEN, FIVE, FIVE, JACK, FIVE}, + bet: 684, + }, + }, + "sample_3": { + input: "KK677 28", + expected: hand{ + strength: TWO_PAIR, + cards: []cardType{KING, KING, SIX, SEVEN, SEVEN}, + bet: 28, + }, + }, + "sample_4": { + input: "KTJJT 220", + expected: hand{ + strength: TWO_PAIR, + cards: []cardType{KING, TEN, JACK, JACK, TEN}, + bet: 220, + }, + }, + "sample_5": { + input: "QQQJA 483", + expected: hand{ + strength: THREE_OF_A_KIND, + cards: []cardType{QUEEN, QUEEN, QUEEN, JACK, ACE}, + bet: 483, + }, + }, + "full_house": { + input: "QQQAA 999", + expected: hand{ + strength: FULL_HOUSE, + cards: []cardType{QUEEN, QUEEN, QUEEN, ACE, ACE}, + bet: 999, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + actual := parseLine(test.input) + + if actual.strength != test.expected.strength { + t.Errorf("Strength: expected %v, got %v", test.expected.strength, actual.strength) + } + + if len(actual.cards) != len(test.expected.cards) { + t.Errorf("Cards: expected %v, got %v", test.expected.cards, actual.cards) + } + + for i, card := range actual.cards { + if card != test.expected.cards[i] { + t.Errorf("Card Order: expected %v, got %v", test.expected.cards, actual.cards) + } + } + + if actual.bet != test.expected.bet { + t.Errorf("Bet: expected %v, got %v", test.expected.bet, actual.bet) + } + }) + } +} + +func TestMapByStrength(t *testing.T) { + tests := map[string]struct { + input []hand + expected map[handType][]hand + }{ + "sample": { + input: []hand{ + { + strength: PAIR, + cards: []cardType{THREE, TWO, TEN, THREE, KING}, + bet: 765, + }, + { + strength: THREE_OF_A_KIND, + cards: []cardType{TEN, FIVE, FIVE, JACK, FIVE}, + bet: 684, + }, + { + strength: TWO_PAIR, + cards: []cardType{KING, KING, SIX, SEVEN, SEVEN}, + bet: 28, + }, + { + strength: TWO_PAIR, + cards: []cardType{KING, TEN, JACK, JACK, TEN}, + bet: 220, + }, + { + strength: THREE_OF_A_KIND, + cards: []cardType{QUEEN, QUEEN, QUEEN, JACK, ACE}, + bet: 483, + }, + }, + expected: map[handType][]hand{ + PAIR: { + { + strength: PAIR, + cards: []cardType{THREE, TWO, TEN, THREE, KING}, + bet: 765, + }, + }, + TWO_PAIR: { + { + strength: TWO_PAIR, + cards: []cardType{KING, KING, SIX, SEVEN, SEVEN}, + bet: 28, + }, + { + strength: TWO_PAIR, + cards: []cardType{KING, TEN, JACK, JACK, TEN}, + bet: 220, + }, + }, + THREE_OF_A_KIND: { + { + strength: THREE_OF_A_KIND, + cards: []cardType{TEN, FIVE, FIVE, JACK, FIVE}, + bet: 684, + }, + { + strength: THREE_OF_A_KIND, + cards: []cardType{QUEEN, QUEEN, QUEEN, JACK, ACE}, + bet: 483, + }, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + actual := mapByStrength(test.input) + + for strength, hands := range actual { + expected := test.expected[strength] + + if len(hands) != len(expected) { + t.Errorf("Length: expected %v, got %v", len(expected), len(hands)) + } + + for i, hand := range hands { + if hand.strength != expected[i].strength { + t.Errorf("Strength: expected %v, got %v", expected[i].strength, hand.strength) + } + + if len(hand.cards) != len(expected[i].cards) { + t.Errorf("Cards: expected %v, got %v", expected[i].cards, hand.cards) + } + + for j, card := range hand.cards { + if card != expected[i].cards[j] { + t.Errorf("Card Order: expected %v, got %v", expected[i].cards, hand.cards) + } + } + + if hand.bet != expected[i].bet { + t.Errorf("Bet: expected %v, got %v", expected[i].bet, hand.bet) + } + } + } + }) + } +} + +func TestSortByHighest(t *testing.T) { + tests := map[string]struct { + input []hand + expected []hand + }{ + "sample": { + input: []hand{ + { + strength: PAIR, + cards: []cardType{THREE, TWO, TEN, THREE, KING}, + bet: 765, + }, + { + strength: THREE_OF_A_KIND, + cards: []cardType{TEN, FIVE, FIVE, JACK, FIVE}, + bet: 684, + }, + { + strength: TWO_PAIR, + cards: []cardType{KING, KING, SIX, SEVEN, SEVEN}, + bet: 28, + }, + { + strength: TWO_PAIR, + cards: []cardType{KING, TEN, JACK, JACK, TEN}, + bet: 220, + }, + { + strength: THREE_OF_A_KIND, + cards: []cardType{QUEEN, QUEEN, QUEEN, JACK, ACE}, + bet: 483, + }, + }, + expected: []hand{ + { + strength: PAIR, + cards: []cardType{THREE, TWO, TEN, THREE, KING}, + bet: 765, + }, + { + strength: THREE_OF_A_KIND, + cards: []cardType{TEN, FIVE, FIVE, JACK, FIVE}, + bet: 684, + }, + { + strength: THREE_OF_A_KIND, + cards: []cardType{QUEEN, QUEEN, QUEEN, JACK, ACE}, + bet: 483, + }, + { + strength: TWO_PAIR, + cards: []cardType{KING, TEN, JACK, JACK, TEN}, + bet: 220, + }, + { + strength: TWO_PAIR, + cards: []cardType{KING, KING, SIX, SEVEN, SEVEN}, + bet: 28, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + actual := sortByHighest(test.input) + + if len(actual) != len(test.expected) { + t.Errorf("Length: expected %v, got %v", len(test.expected), len(actual)) + } + + for i, hand := range actual { + if hand.strength != test.expected[i].strength { + t.Errorf("Strength: expected %v, got %v", test.expected[i].strength, hand.strength) + } + + if len(hand.cards) != len(test.expected[i].cards) { + t.Errorf("Cards: expected %v, got %v", test.expected[i].cards, hand.cards) + } + + for j, card := range hand.cards { + if card != test.expected[i].cards[j] { + t.Errorf("Card Order: expected %v, got %v", test.expected[i].cards, hand.cards) + } + } + + if hand.bet != test.expected[i].bet { + t.Errorf("Bet: expected %v, got %v", test.expected[i].bet, hand.bet) + } + } + }) + } +} + +func TestFlatten(t *testing.T) { + tests := map[string]struct { + input map[handType][]hand + expected []hand + }{ + "sample": { + // Order jumbled to ensure sorting works + input: map[handType][]hand{ + TWO_PAIR: { + { + strength: TWO_PAIR, + cards: []cardType{KING, TEN, JACK, JACK, TEN}, + bet: 220, + }, + { + strength: TWO_PAIR, + cards: []cardType{KING, KING, SIX, SEVEN, SEVEN}, + bet: 28, + }, + }, + PAIR: { + { + strength: PAIR, + cards: []cardType{THREE, TWO, TEN, THREE, KING}, + bet: 765, + }, + }, + THREE_OF_A_KIND: { + { + strength: THREE_OF_A_KIND, + cards: []cardType{TEN, FIVE, FIVE, JACK, FIVE}, + bet: 684, + }, + { + strength: THREE_OF_A_KIND, + cards: []cardType{QUEEN, QUEEN, QUEEN, JACK, ACE}, + bet: 483, + }, + }, + }, + expected: []hand{ + { + strength: PAIR, + cards: []cardType{THREE, TWO, TEN, THREE, KING}, + bet: 765, + }, + { + strength: TWO_PAIR, + cards: []cardType{KING, TEN, JACK, JACK, TEN}, + bet: 220, + }, + { + strength: TWO_PAIR, + cards: []cardType{KING, KING, SIX, SEVEN, SEVEN}, + bet: 28, + }, + { + strength: THREE_OF_A_KIND, + cards: []cardType{TEN, FIVE, FIVE, JACK, FIVE}, + bet: 684, + }, + { + strength: THREE_OF_A_KIND, + cards: []cardType{QUEEN, QUEEN, QUEEN, JACK, ACE}, + bet: 483, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + actual := flatten(test.input) + + if len(actual) != len(test.expected) { + t.Errorf("Length: expected %v, got %v", len(test.expected), len(actual)) + } + + for i, hand := range actual { + expected := test.expected[i] + + if hand.strength != expected.strength { + t.Errorf("Strength: expected %v, got %v", expected.strength, hand.strength) + } + + if len(hand.cards) != len(expected.cards) { + t.Errorf("Cards: expected %v, got %v", expected.cards, hand.cards) + } + + for i, card := range hand.cards { + if card != expected.cards[i] { + t.Errorf("Card Order: expected %v, got %v", expected.cards, hand.cards) + } + } + + if hand.bet != expected.bet { + t.Errorf("Bet: expected %v, got %v", expected.bet, hand.bet) + } + } + }) + } +} + +func TestCalculateWinnings(t *testing.T) { + tests := map[string]struct { + input []hand + expected int + }{ + "sample": { + input: []hand{ + { + strength: PAIR, + cards: []cardType{THREE, TWO, TEN, THREE, KING}, + bet: 765, + }, + { + strength: TWO_PAIR, + cards: []cardType{KING, TEN, JACK, JACK, TEN}, + bet: 220, + }, + { + strength: TWO_PAIR, + cards: []cardType{KING, KING, SIX, SEVEN, SEVEN}, + bet: 28, + }, + { + strength: THREE_OF_A_KIND, + cards: []cardType{TEN, FIVE, FIVE, JACK, FIVE}, + bet: 684, + }, + { + strength: THREE_OF_A_KIND, + cards: []cardType{QUEEN, QUEEN, QUEEN, JACK, ACE}, + bet: 483, + }, + }, + expected: 6440, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + actual := calculateWinnings(test.input) + + if actual != test.expected { + t.Errorf("Winnings: expected %v, got %v", test.expected, actual) + } + }) + } +} diff --git a/go/2023/main.go b/go/2023/main.go new file mode 100644 index 0000000..7d71b47 --- /dev/null +++ b/go/2023/main.go @@ -0,0 +1,86 @@ +package main + +import ( + "flag" + "os" + + "github.com/DJOLEARY/Advent_of_Code/2023/Go/internal/day_01" + day_02_part_01 "github.com/DJOLEARY/Advent_of_Code/2023/Go/internal/day_02/part_01" + day_02_part_02 "github.com/DJOLEARY/Advent_of_Code/2023/Go/internal/day_02/part_02" + day_03_part_01 "github.com/DJOLEARY/Advent_of_Code/2023/Go/internal/day_03/part_01" + day_04_part_01 "github.com/DJOLEARY/Advent_of_Code/2023/Go/internal/day_04/part_01" + day_04_part_02 "github.com/DJOLEARY/Advent_of_Code/2023/Go/internal/day_04/part_02" + day_06_part_01 "github.com/DJOLEARY/Advent_of_Code/2023/Go/internal/day_06/part_01" + day_06_part_02 "github.com/DJOLEARY/Advent_of_Code/2023/Go/internal/day_06/part_02" + day_07_part_01 "github.com/DJOLEARY/Advent_of_Code/2023/Go/internal/day_07/part_01" + _ "github.com/outrigdev/outrig/autoinit" +) + +func main() { + day := flag.Int("day", 1, "Day to run") + part := flag.Int("part", 2, "Part to run, must be 1 or 2") + + flag.Parse() + + if *part < 1 || *part > 2 { + println("Invalid part number, must be 1 or 2") + os.Exit(1) + } + + switch *day { + case 1: + switch *part { + case 1: + println("Day 1 part 1 not implemented") + os.Exit(1) + case 2: + day_01.Solve() + } + + case 2: + switch *part { + case 1: + day_02_part_01.Solve() + case 2: + day_02_part_02.Solve() + } + + case 3: + switch *part { + case 1: + day_03_part_01.Solve() + case 2: + println("Day 3 part 2 not implemented") + os.Exit(1) + } + + case 4: + switch *part { + case 1: + day_04_part_01.Solve() + case 2: + day_04_part_02.Solve() + } + + case 6: + switch *part { + case 1: + day_06_part_01.Solve() + case 2: + day_06_part_02.Solve() + } + + case 7: + switch *part { + case 1: + day_07_part_01.Solve() + case 2: + println("Day 7 part 2 not implemented") + os.Exit(1) + } + + default: + println("Day not implemented") + os.Exit(1) + } +} |
