summaryrefslogtreecommitdiff
path: root/go/2023/internal
diff options
context:
space:
mode:
Diffstat (limited to 'go/2023/internal')
-rw-r--r--go/2023/internal/day_01/day_01.go114
-rw-r--r--go/2023/internal/day_01/day_01_test.go31
-rw-r--r--go/2023/internal/day_02/part_01/part_01.go138
-rw-r--r--go/2023/internal/day_02/part_01/part_01_test.go102
-rw-r--r--go/2023/internal/day_02/part_02/part_02.go114
-rw-r--r--go/2023/internal/day_02/part_02/part_02_test.go84
-rw-r--r--go/2023/internal/day_03/part_01/part_01.go123
-rw-r--r--go/2023/internal/day_03/part_01/part_01_test.go78
-rw-r--r--go/2023/internal/day_04/part_01/part_01.go130
-rw-r--r--go/2023/internal/day_04/part_01/part_01_test.go48
-rw-r--r--go/2023/internal/day_04/part_02/part_02.go142
-rw-r--r--go/2023/internal/day_04/part_02/part_02_test.go143
-rw-r--r--go/2023/internal/day_06/part_01/part_01.go85
-rw-r--r--go/2023/internal/day_06/part_01/part_01_test.go96
-rw-r--r--go/2023/internal/day_06/part_02/part_02.go81
-rw-r--r--go/2023/internal/day_06/part_02/part_02_test.go84
-rw-r--r--go/2023/internal/day_07/part_01/part_01.go273
-rw-r--r--go/2023/internal/day_07/part_01/part_01_test.go432
18 files changed, 2298 insertions, 0 deletions
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)
+ }
+ })
+ }
+}