diff --git a/2023/day01/main.go b/2023/day01/main.go new file mode 100644 index 0000000..2ec0de6 --- /dev/null +++ b/2023/day01/main.go @@ -0,0 +1,110 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + var sum int + for _, line := range strings.Split(input, "\n") { + var tens, ones int + for i := 0; i < len(line); i++ { + if strings.ContainsAny(line[i:i+1], "0123456789") { + tens = cast.ToInt(line[i : i+1]) + break + } + } + for i := len(line) - 1; i >= 0; i-- { + if strings.ContainsAny(line[i:i+1], "0123456789") { + ones = cast.ToInt(line[i : i+1]) + break + } + } + sum += tens*10 + ones + } + + return sum +} + +func part2(input string) int { + prefixes := map[string]int{ + "one": 1, + "two": 2, + "three": 3, + "four": 4, + "five": 5, + "six": 6, + "seven": 7, + "eight": 8, + "nine": 9, + "zero": 0, + } + for i := 0; i <= 9; i++ { + prefixes[cast.ToString(i)] = i + } + + var sum int + for _, line := range strings.Split(input, "\n") { + var first, last int + + for len(line) > 0 { + for prefix, val := range prefixes { + if doesStringHavePrefix(line, prefix) { + if first == 0 { + first = val + } + last = val + break + } + } + + // shorten line + line = line[1:] + } + + sum += first*10 + last + } + + return sum +} + +func doesStringHavePrefix(str string, prefix string) bool { + if len(str) < len(prefix) { + return false + } + return str[:len(prefix)] == prefix +} diff --git a/2023/day01/main_test.go b/2023/day01/main_test.go new file mode 100644 index 0000000..9b773de --- /dev/null +++ b/2023/day01/main_test.go @@ -0,0 +1,70 @@ +package main + +import ( + "testing" +) + +var example = `1abc2 +pqr3stu8vwx +a1b2c3d4e5f +treb7uchet` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 142, + }, + { + name: "actual", + input: input, + want: 55488, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +var example2 = `two1nine +eightwothree +abcone2threexyz +xtwone3four +4nineeightseven2 +zoneight234 +7pqrstsixteen` + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example2, + want: 281, + }, + { + name: "actual", + input: input, + want: 55614, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2023/day02/main.go b/2023/day02/main.go new file mode 100644 index 0000000..0fcb6f7 --- /dev/null +++ b/2023/day02/main.go @@ -0,0 +1,112 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/mathy" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +func part1(input string) int { + games := parseInput(input) + + var possibleIDSum int + + for _, g := range games { + isPossible := true + for _, step := range g.steps { + // only 12 red cubes, 13 green cubes, and 14 blue cubes + if step["red"] > 12 || step["green"] > 13 || step["blue"] > 14 { + isPossible = false + break + } + } + + if isPossible { + possibleIDSum += g.id + } + } + + return possibleIDSum +} + +func part2(input string) int { + games := parseInput(input) + + var sum int + + for _, g := range games { + lowestPossibleCount := map[string]int{} + + for _, step := range g.steps { + lowestPossibleCount["red"] = mathy.MaxInt(lowestPossibleCount["red"], step["red"]) + lowestPossibleCount["green"] = mathy.MaxInt(lowestPossibleCount["green"], step["green"]) + lowestPossibleCount["blue"] = mathy.MaxInt(lowestPossibleCount["blue"], step["blue"]) + } + + sum += lowestPossibleCount["red"] * lowestPossibleCount["blue"] * lowestPossibleCount["green"] + } + + return sum +} + +type game struct { + id int + steps []map[string]int +} + +func parseInput(input string) (ans []game) { + for i, line := range strings.Split(input, "\n") { + parts := strings.Split(line, ": ") + g := game{ + id: i + 1, + } + + for _, p := range strings.Split(parts[1], "; ") { + step := map[string]int{} + for _, group := range strings.Split(p, ", ") { + numberColor := strings.Split(group, " ") + if len(numberColor) != 2 { + panic(fmt.Sprintf("group not in two pieces %q", group)) + } + + step[numberColor[1]] = cast.ToInt(numberColor[0]) + } + g.steps = append(g.steps, step) + } + ans = append(ans, g) + } + return ans +} diff --git a/2023/day02/main_test.go b/2023/day02/main_test.go new file mode 100644 index 0000000..dcf2c1c --- /dev/null +++ b/2023/day02/main_test.go @@ -0,0 +1,63 @@ +package main + +import ( + "testing" +) + +var example = `Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green +Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue +Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red +Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red +Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 8, + }, + { + name: "actual", + input: input, + want: 2632, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 2286, + }, + // { + // name: "actual", + // input: input, + // want: 0, + // }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/2023/day03/main.go b/2023/day03/main.go new file mode 100644 index 0000000..d24e0b2 --- /dev/null +++ b/2023/day03/main.go @@ -0,0 +1,198 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "regexp" + "strings" + + "github.com/alexchao26/advent-of-code-go/cast" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +var numReg = regexp.MustCompile("[0-9]") + +func part1(input string) int { + matrix := [][]string{} + + // collect special characters + specialChars := map[string]bool{} + for _, row := range strings.Split(input, "\n") { + matrix = append(matrix, strings.Split(row, "")) + for _, val := range strings.Split(row, "") { + if !numReg.MatchString(val) && val != "." { + specialChars[val] = true + } + } + } + specialCharsString := "" + for k := range specialChars { + specialCharsString += k + } + + var sum int + + diffs := [8][2]int{ + {-1, -1}, + {-1, 0}, + {-1, 1}, + {0, -1}, + {0, 1}, + {1, -1}, + {1, 0}, + {1, 1}, + } + + seen := map[[2]int]bool{} + for r, row := range matrix { + for c, val := range row { + coords := [2]int{r, c} + if seen[coords] { + continue + } + seen[coords] = true + + // if we hit a number, collect the entire number and check along the way if it's + // adjacent to a special char + if numReg.MatchString(val) { + hasAdjacentSpecialChar := false + + numStr := "" + for j := 0; j+c < len(matrix[0]); j++ { + char := row[c+j] + // breaks on period or special character, loop itself breaks on out of range + if !numReg.MatchString(char) { + break + } + // keep collecting number + numStr += char + + // check all 8 directions for special char + for _, d := range diffs { + dr, dc := r+d[0], c+j+d[1] + if dr >= 0 && dr < len(matrix) && dc >= 0 && dc < len(matrix[0]) { + if strings.ContainsAny(matrix[dr][dc], specialCharsString) { + hasAdjacentSpecialChar = true + } + seen[[2]int{r, c + j}] = true + } + } + } + + if hasAdjacentSpecialChar { + sum += cast.ToInt(numStr) + } + + } + } + } + + return sum +} + +// getNumber returns -1 if a number is "not found" which could include the number +// already being seen +func getNumber(matrix [][]string, coord [2]int, seen map[[2]int]bool) int { + if !numReg.MatchString(matrix[coord[0]][coord[1]]) { + return -1 + } + if seen[coord] { + return -1 + } + // go to the left most digit + r, c := coord[0], coord[1] + for c-1 >= 0 { + if numReg.MatchString(matrix[r][c-1]) { + c-- + } else { + break + } + } + + numStr := "" + + for c < len(matrix[0]) && numReg.MatchString(matrix[r][c]) { + numStr += matrix[r][c] + seen[[2]int{r, c}] = true + c++ + } + + return cast.ToInt(numStr) +} + +func part2(input string) int { + // lucky edge case that there are not multiple gears that share the same number such as + // ....*.... + // .123.456. OR ...123*456*789... + // ....*.... + // with the current useage of "seen", this would only get counted once + seen := map[[2]int]bool{} + + matrix := [][]string{} + for _, row := range strings.Split(input, "\n") { + matrix = append(matrix, strings.Split(row, "")) + } + + diffs := [8][2]int{ + {-1, -1}, + {-1, 0}, + {-1, 1}, + {0, -1}, + {0, 1}, + {1, -1}, + {1, 0}, + {1, 1}, + } + + sum := 0 + for r, rows := range matrix { + for c, val := range rows { + if val == "*" { + nums := []int{} + for _, diff := range diffs { + dr, dc := r+diff[0], c+diff[1] + if dr >= 0 && dr < len(matrix) && dc >= 0 && dc < len(matrix[0]) { + foundNum := getNumber(matrix, [2]int{dr, dc}, seen) + if foundNum != -1 { + nums = append(nums, foundNum) + } + } + } + + if len(nums) == 2 { + sum += nums[0] * nums[1] + } + } + } + } + return sum +} diff --git a/2023/day03/main_test.go b/2023/day03/main_test.go new file mode 100644 index 0000000..1aed0f5 --- /dev/null +++ b/2023/day03/main_test.go @@ -0,0 +1,68 @@ +package main + +import ( + "testing" +) + +var example = `467..114.. +...*...... +..35..633. +......#... +617*...... +.....+.58. +..592..... +......755. +...$.*.... +.664.598..` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 4361, + }, + { + name: "actual", + input: input, + want: 536576, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 467835, + }, + // { + // name: "actual", + // input: input, + // want: 0, + // }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +}