diff --git a/Makefile b/Makefile index 924a0ccc..697d2599 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,9 @@ SHELL := env APP_NAME=$(APP_NAME) $(SHELL) SHELL := env GOTOOLS_IMAGE_TAG=$(GOTOOLS_IMAGE_TAG) $(SHELL) +AOC_PUZZLE_URL= +SHELL := env AOC_PUZZLE_URL=$(AOC_PUZZLE_URL) $(SHELL) + COMPOSE_TOOLS_FILE=deployments/docker-compose/go-tools-docker-compose.yml COMPOSE_TOOLS_CMD_BASE=docker compose -f $(COMPOSE_TOOLS_FILE) COMPOSE_TOOLS_CMD_UP=$(COMPOSE_TOOLS_CMD_BASE) up --exit-code-from diff --git a/internal/puzzles/constants.go b/internal/puzzles/constants.go index f9e9f02e..c298a57d 100644 --- a/internal/puzzles/constants.go +++ b/internal/puzzles/constants.go @@ -54,6 +54,7 @@ const ( Year2021 // 2021 Year2022 // 2022 Year2023 // 2023 + Year2024 // 2024 yearSentinel ) diff --git a/internal/puzzles/solutions/2017/day02/solution.go b/internal/puzzles/solutions/2017/day02/solution.go index 0d8118f9..a418f6d0 100644 --- a/internal/puzzles/solutions/2017/day02/solution.go +++ b/internal/puzzles/solutions/2017/day02/solution.go @@ -28,7 +28,7 @@ func (s solution) Year() string { func (s solution) Part1(input io.Reader) (string, error) { var f checksumFunc = func(row []string) (int, error) { - var min, max int + var minVal, maxVal int for i, number := range row { d, err := strconv.Atoi(number) @@ -37,19 +37,19 @@ func (s solution) Part1(input io.Reader) (string, error) { } if i == 0 { - min, max = d, d + minVal, maxVal = d, d } - if d < min { - min = d + if d < minVal { + minVal = d } - if d > max { - max = d + if d > maxVal { + maxVal = d } } - return max - min, nil + return maxVal - minVal, nil } return findChecksum(input, f) diff --git a/internal/puzzles/solutions/2024/day01/solution.go b/internal/puzzles/solutions/2024/day01/solution.go new file mode 100755 index 00000000..94c0627c --- /dev/null +++ b/internal/puzzles/solutions/2024/day01/solution.go @@ -0,0 +1,126 @@ +// Package day01 contains solution for https://adventofcode.com/2024/day/1 puzzle. +package day01 + +import ( + "bufio" + "fmt" + "io" + "slices" + "strconv" + "strings" + + "github.com/obalunenko/advent-of-code/internal/puzzles" +) + +func init() { + puzzles.Register(solution{}) +} + +type solution struct{} + +func (s solution) Year() string { + return puzzles.Year2024.String() +} + +func (s solution) Day() string { + return puzzles.Day01.String() +} + +func (s solution) Part1(input io.Reader) (string, error) { + l, err := parseInput(input) + if err != nil { + return "", fmt.Errorf("failed to parse input: %w", err) + } + + slices.Sort(l.itemsA) + slices.Sort(l.itemsB) + + var sum int + + for i := 0; i < len(l.itemsA); i++ { + d := l.itemsA[i] - l.itemsB[i] + if d < 0 { + d = -d + } + + sum += d + } + + return strconv.Itoa(sum), nil +} + +func (s solution) Part2(input io.Reader) (string, error) { + l, err := parseInput(input) + if err != nil { + return "", fmt.Errorf("failed to parse input: %w", err) + } + + seenA := make(map[int]int) + + for _, a := range l.itemsA { + seenA[a] = 0 + + for _, b := range l.itemsB { + if a == b { + seenA[a]++ + } + } + } + + var sum int + + for _, a := range l.itemsA { + sum += a * seenA[a] + } + + return strconv.Itoa(sum), nil +} + +type lists struct { + itemsA []int + itemsB []int +} + +func parseInput(input io.Reader) (lists, error) { + const ( + listsNum = 2 + listAIdx = 0 + listBIdx = 1 + ) + + l := lists{ + itemsA: make([]int, 0), + itemsB: make([]int, 0), + } + + scanner := bufio.NewScanner(input) + for scanner.Scan() { + line := scanner.Text() + + parts := strings.Split(line, " ") + if len(parts) != listsNum { + return lists{}, fmt.Errorf("invalid input line: %s", line) + } + + // Parse parts[0] and parts[1] to integers and append them to l.itemsA and l.itemsB respectively. + a, err := strconv.Atoi(parts[listAIdx]) + if err != nil { + return lists{}, fmt.Errorf("failed to parse int: %w", err) + } + + b, err := strconv.Atoi(parts[listBIdx]) + if err != nil { + return lists{}, fmt.Errorf("failed to parse int: %w", err) + } + + l.itemsA = append(l.itemsA, a) + + l.itemsB = append(l.itemsB, b) + } + + if scanner.Err() != nil { + return lists{}, fmt.Errorf("scanner error: %w", scanner.Err()) + } + + return l, nil +} diff --git a/internal/puzzles/solutions/2024/day01/solution_test.go b/internal/puzzles/solutions/2024/day01/solution_test.go new file mode 100755 index 00000000..af9659e7 --- /dev/null +++ b/internal/puzzles/solutions/2024/day01/solution_test.go @@ -0,0 +1,117 @@ +package day01 + +import ( + "errors" + "io" + "path/filepath" + "testing" + "testing/iotest" + + "github.com/stretchr/testify/assert" + + "github.com/obalunenko/advent-of-code/internal/puzzles/common/utils" +) + +func Test_solution_Year(t *testing.T) { + var s solution + + want := "2024" + got := s.Year() + + assert.Equal(t, want, got) +} + +func Test_solution_Day(t *testing.T) { + var s solution + + want := "1" + got := s.Day() + + assert.Equal(t, want, got) +} + +func Test_solution_Part1(t *testing.T) { + var s solution + + type args struct { + input io.Reader + } + + tests := []struct { + name string + args args + want string + wantErr assert.ErrorAssertionFunc + }{ + { + name: "test example from description", + args: args{ + input: utils.ReaderFromFile(t, filepath.Join("testdata", "input.txt")), + }, + want: "11", + wantErr: assert.NoError, + }, + { + name: "", + args: args{ + input: iotest.ErrReader(errors.New("custom error")), + }, + want: "", + wantErr: assert.Error, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := s.Part1(tt.args.input) + if !tt.wantErr(t, err) { + return + } + + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_solution_Part2(t *testing.T) { + var s solution + + type args struct { + input io.Reader + } + + tests := []struct { + name string + args args + want string + wantErr assert.ErrorAssertionFunc + }{ + { + name: "test example from description", + args: args{ + input: utils.ReaderFromFile(t, filepath.Join("testdata", "input.txt")), + }, + want: "31", + wantErr: assert.NoError, + }, + { + name: "", + args: args{ + input: iotest.ErrReader(errors.New("custom error")), + }, + want: "", + wantErr: assert.Error, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := s.Part2(tt.args.input) + if !tt.wantErr(t, err) { + return + } + + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/puzzles/solutions/2024/day01/spec.md b/internal/puzzles/solutions/2024/day01/spec.md new file mode 100755 index 00000000..2545ebca --- /dev/null +++ b/internal/puzzles/solutions/2024/day01/spec.md @@ -0,0 +1,104 @@ +# Puzzle https://adventofcode.com/2024/day/1 + +# --- Day 1: Historian Hysteria --- + +## --- Part One --- + +The Chief Historian is always present for the big Christmas sleigh launch, but nobody has seen him in months! +Last anyone heard, he was visiting locations that are historically significant to the North Pole; +a group of Senior Historians has asked you to accompany them as they check the places they think he was most likely to visit. + +As each location is checked, they will mark it on their list with a star. They figure the Chief Historian must be +in one of the first fifty places they'll look, so in order to save Christmas, you need to help them get fifty stars on +their list before Santa takes off on December 25th. + +Collect stars by solving puzzles. Two puzzles will be made available on each day in the Advent calendar; +the second puzzle is unlocked when you complete the first. Each puzzle grants one star. Good luck! + +You haven't even left yet and the group of Elvish Senior Historians has already hit a problem: their list of locations +to check is currently empty. Eventually, someone decides that the best place to check first would be the +Chief Historian's office. + +Upon pouring into the office, everyone confirms that the Chief Historian is indeed nowhere to be found. +Instead, the Elves discover an assortment of notes and lists of historically significant locations! T +his seems to be the planning the Chief Historian was doing before he left. +Perhaps these notes can be used to determine which locations to search? + +Throughout the Chief's office, the historically significant locations are listed not by name but by a unique number +called the location ID. To make sure they don't miss anything, The Historians split into two groups, +each searching the office and trying to create their own complete list of location IDs. + +There's just one problem: by holding the two lists up side by side (your puzzle input), it quickly becomes clear that +the lists aren't very similar. Maybe you can help The Historians reconcile their lists? + +For example: + +```text +3 4 +4 3 +2 5 +1 3 +3 9 +3 3 +``` + +Maybe the lists are only off by a small amount! To find out, pair up the numbers and measure how far apart they are. +Pair up the smallest number in the left list with the smallest number in the right list, then the second-smallest left +number with the second-smallest right number, and so on. + +Within each pair, figure out how far apart the two numbers are; you'll need to add up all of those distances. +For example, if you pair up a 3 from the left list with a 7 from the right list, the distance apart is 4; if you pair +up a 9 with a 3, the distance apart is 6. + +In the example list above, the pairs and distances would be as follows: + +- The smallest number in the left list is 1, and the smallest number in the right list is 3. The distance between them is 2. +- The second-smallest number in the left list is 2, and the second-smallest number in the right list is another 3. The distance between them is 1. +- The third-smallest number in both lists is 3, so the distance between them is 0. +- The next numbers to pair up are 3 and 4, a distance of 1. +- The fifth-smallest numbers in each list are 3 and 5, a distance of 2. +- Finally, the largest number in the left list is 4, while the largest number in the right list is 9; these are a distance 5 apart. + +To find the total distance between the left list and the right list, add up the distances between all of the pairs you +found. In the example above, this is `2 + 1 + 0 + 1 + 2 + 5`, a total distance of `11`! + +Your actual left and right lists contain many location IDs. What is the total distance between your lists? + +## --- Part Two --- + +Your analysis only confirmed what everyone feared: the two lists of location IDs are indeed very different. + +Or are they? + +The Historians can't agree on which group made the mistakes or how to read most of the Chief's handwriting, +but in the commotion you notice an interesting detail: a lot of location IDs appear in both lists! Maybe the other +numbers aren't location IDs at all but rather misinterpreted handwriting. + +This time, you'll need to figure out exactly how often each number from the left list appears in the right list. +Calculate a total similarity score by adding up each number in the left list after multiplying it by the number of +times that number appears in the right list. + +Here are the same example lists again: + +```text +3 4 +4 3 +2 5 +1 3 +3 9 +3 3 +``` + +For these example lists, here is the process of finding the similarity score: + +- The first number in the left list is 3. It appears in the right list three times, so the similarity score increases by 3 * 3 = 9. +- The second number in the left list is 4. It appears in the right list once, so the similarity score increases by 4 * 1 = 4. +- The third number in the left list is 2. It does not appear in the right list, so the similarity score does not increase (2 * 0 = 0). +- The fourth number, 1, also does not appear in the right list. +- The fifth number, 3, appears in the right list three times; the similarity score increases by 9. +- The last number, 3, appears in the right list three times; the similarity score again increases by 9. + +So, for these example lists, the similarity score at the end of this process is `31` `(9 + 4 + 0 + 0 + 9 + 9)`. + +Once again consider your left and right lists. What is their similarity score? + diff --git a/internal/puzzles/solutions/2024/day01/testdata/input.txt b/internal/puzzles/solutions/2024/day01/testdata/input.txt new file mode 100644 index 00000000..b8af9ad2 --- /dev/null +++ b/internal/puzzles/solutions/2024/day01/testdata/input.txt @@ -0,0 +1,6 @@ +3 4 +4 3 +2 5 +1 3 +3 9 +3 3 diff --git a/internal/puzzles/solutions/register_2024.go b/internal/puzzles/solutions/register_2024.go new file mode 100644 index 00000000..2bf6def2 --- /dev/null +++ b/internal/puzzles/solutions/register_2024.go @@ -0,0 +1,9 @@ +package solutions + +import ( + /* + 2024 solutions. + */ + // register day01 solution. + _ "github.com/obalunenko/advent-of-code/internal/puzzles/solutions/2024/day01" +) diff --git a/internal/puzzles/year_string.go b/internal/puzzles/year_string.go index ed6a37e5..8d9cda4f 100644 --- a/internal/puzzles/year_string.go +++ b/internal/puzzles/year_string.go @@ -18,12 +18,13 @@ func _() { _ = x[Year2021-7] _ = x[Year2022-8] _ = x[Year2023-9] - _ = x[yearSentinel-10] + _ = x[Year2024-10] + _ = x[yearSentinel-11] } -const _Year_name = "yearUnknown201520162017201820192020202120222023yearSentinel" +const _Year_name = "yearUnknown2015201620172018201920202021202220232024yearSentinel" -var _Year_index = [...]uint8{0, 11, 15, 19, 23, 27, 31, 35, 39, 43, 47, 59} +var _Year_index = [...]uint8{0, 11, 15, 19, 23, 27, 31, 35, 39, 43, 47, 51, 63} func (i Year) String() string { if i < 0 || i >= Year(len(_Year_index)-1) { diff --git a/tests/regression_2024_test.go b/tests/regression_2024_test.go new file mode 100644 index 00000000..09341465 --- /dev/null +++ b/tests/regression_2024_test.go @@ -0,0 +1,364 @@ +package tests_test + +import ( + "testing" + + "github.com/obalunenko/advent-of-code/internal/puzzles" +) + +func testcases2024(tb testing.TB) []testcase { + year := puzzles.Year2024 + + return []testcase{ + { + name: tcName(tb, year, puzzles.Day01), + args: args{ + year: year.String(), + name: puzzles.Day01.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day01.String(), + Part1: "936063", + Part2: "23150395", + }, + wantErr: false, + }, + { + name: tcName(tb, year, puzzles.Day02), + args: args{ + year: year.String(), + name: puzzles.Day02.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day02.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day03), + args: args{ + year: year.String(), + name: puzzles.Day03.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day03.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day04), + args: args{ + year: year.String(), + name: puzzles.Day04.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day04.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day05), + args: args{ + year: year.String(), + name: puzzles.Day05.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day05.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day06), + args: args{ + year: year.String(), + name: puzzles.Day06.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day06.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day07), + args: args{ + year: year.String(), + name: puzzles.Day07.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day07.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day08), + args: args{ + year: year.String(), + name: puzzles.Day08.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day08.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day09), + args: args{ + year: year.String(), + name: puzzles.Day09.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day09.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day10), + args: args{ + year: year.String(), + name: puzzles.Day10.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day10.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day11), + args: args{ + year: year.String(), + name: puzzles.Day11.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day11.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day12), + args: args{ + year: year.String(), + name: puzzles.Day12.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day12.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day13), + args: args{ + year: year.String(), + name: puzzles.Day13.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day13.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day14), + args: args{ + year: year.String(), + name: puzzles.Day14.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day14.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day15), + args: args{ + year: year.String(), + name: puzzles.Day15.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day15.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day16), + args: args{ + year: year.String(), + name: puzzles.Day16.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day16.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day17), + args: args{ + year: year.String(), + name: puzzles.Day17.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day17.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day18), + args: args{ + year: year.String(), + name: puzzles.Day18.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day18.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day19), + args: args{ + year: year.String(), + name: puzzles.Day19.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day19.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day20), + args: args{ + year: year.String(), + name: puzzles.Day20.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day20.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day21), + args: args{ + year: year.String(), + name: puzzles.Day21.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day21.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day22), + args: args{ + year: year.String(), + name: puzzles.Day22.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day22.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day23), + args: args{ + year: year.String(), + name: puzzles.Day23.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day23.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day24), + args: args{ + year: year.String(), + name: puzzles.Day24.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day24.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day25), + args: args{ + year: year.String(), + name: puzzles.Day25.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day25.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + } +} diff --git a/tests/regression_test.go b/tests/regression_test.go index dc025d71..9d3f1088 100644 --- a/tests/regression_test.go +++ b/tests/regression_test.go @@ -55,6 +55,7 @@ func TestRun(t *testing.T) { tests = append(tests, testcases2021(t)...) tests = append(tests, testcases2022(t)...) tests = append(tests, testcases2023(t)...) + tests = append(tests, testcases2024(t)...) for i := range tests { tt := tests[i]