diff --git a/internal/puzzles/solutions/2024/day02/solution.go b/internal/puzzles/solutions/2024/day02/solution.go new file mode 100755 index 00000000..97c23d0b --- /dev/null +++ b/internal/puzzles/solutions/2024/day02/solution.go @@ -0,0 +1,184 @@ +// Package day02 contains solution for https://adventofcode.com/2024/day/2 puzzle. +package day02 + +import ( + "bufio" + "bytes" + "fmt" + "io" + "strconv" + + "github.com/obalunenko/advent-of-code/internal/puzzles" + "github.com/obalunenko/advent-of-code/internal/puzzles/common/utils" +) + +func init() { + puzzles.Register(solution{}) +} + +type solution struct{} + +func (s solution) Year() string { + return puzzles.Year2024.String() +} + +func (s solution) Day() string { + return puzzles.Day02.String() +} + +func (s solution) Part1(input io.Reader) (string, error) { + scanner := bufio.NewScanner(input) + + isSafe := func(line []int) bool { + var asc, desc bool + + for i, val := range line { + if i == 0 { + continue + } + + if line[i-1] < val { + if desc { + return false + } + + asc = true + } + + if line[i-1] > val { + if asc { + return false + } + + desc = true + } + + diff := val - line[i-1] + if diff < 0 { + diff = -diff + } + + if diff < 1 || diff > 3 { + return false + } + } + + return true + } + + var safeCount int + + for scanner.Scan() { + line := scanner.Bytes() + + numbers, err := utils.ParseInts(bytes.NewReader(line), " ") + if err != nil { + return "", fmt.Errorf("failed to parse input line: %w", err) + } + + if isSafe(numbers) { + safeCount++ + } + } + + if err := scanner.Err(); err != nil { + return "", fmt.Errorf("failed to read input: %w", err) + } + + return strconv.Itoa(safeCount), nil +} + +func (s solution) Part2(input io.Reader) (string, error) { + scanner := bufio.NewScanner(input) + + isSafe := func(line []int) bool { + var asc, desc bool + + removedCount := 0 + + mayRemove := func(i int) bool { + if removedCount != 0 { + return false + } + + removedCount++ + + return true + } + + for i := range line { + if i == len(line)-1 { + + } + + if line[i-1] < val { + if desc { + return false + } + + asc = true + } + + if line[i-1] > val { + if asc { + return false + } + + desc = true + } + + diff := val - line[i-1] + if diff < 0 { + diff = -diff + } + + if diff < 1 || diff > 3 { + if !mayRemove(i) { + diff = line[i-1] - line[i+1] + if diff < 0 { + diff = -diff + } + + if diff < 1 || diff > 3 { + return false + } + + continue + } + + return false + } + } + + return true + } + + var safeCount int + + for scanner.Scan() { + line := scanner.Bytes() + + numbers, err := utils.ParseInts(bytes.NewReader(line), " ") + if err != nil { + return "", fmt.Errorf("failed to parse input line: %w", err) + } + + if isSafe(numbers) { + safeCount++ + } + } + + if err := scanner.Err(); err != nil { + return "", fmt.Errorf("failed to read input: %w", err) + } + + return strconv.Itoa(safeCount), nil +} + +func removeIndex(s []int, index int) []int { + ret := make([]int, 0, len(s)-1) + + ret = append(ret, s[:index]...) + + return append(ret, s[index+1:]...) +} diff --git a/internal/puzzles/solutions/2024/day02/solution_test.go b/internal/puzzles/solutions/2024/day02/solution_test.go new file mode 100755 index 00000000..20262adb --- /dev/null +++ b/internal/puzzles/solutions/2024/day02/solution_test.go @@ -0,0 +1,117 @@ +package day02 + +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 := "2" + 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: "2", + 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: "4", + 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/day02/spec.md b/internal/puzzles/solutions/2024/day02/spec.md new file mode 100755 index 00000000..bc326f3b --- /dev/null +++ b/internal/puzzles/solutions/2024/day02/spec.md @@ -0,0 +1,73 @@ +# Puzzle https://adventofcode.com/2024/day/2 + +# --- Day 2: Red-Nosed Reports --- + +## --- Part One --- + +Fortunately, the first location The Historians want to search isn't a long walk from the Chief Historian's office. + +While the Red-Nosed Reindeer nuclear fusion/fission plant appears to contain no sign of the Chief Historian, +the engineers there run up to you as soon as they see you. Apparently, they still talk about the time Rudolph was saved +through molecular synthesis from a single electron. + +They're quick to add that - since you're already here - they'd really appreciate your help analyzing some unusual data +from the Red-Nosed reactor. You turn to check if The Historians are waiting for you, but they seem to have already +divided into groups that are currently searching every corner of the facility. You offer to help with the unusual data. + +The unusual data (your puzzle input) consists of many reports, one report per line. Each report is a list of numbers +called levels that are separated by spaces. For example: + +```text +7 6 4 2 1 +1 2 7 8 9 +9 7 6 2 1 +1 3 2 4 5 +8 6 4 4 1 +1 3 6 7 9 +``` + +This example data contains six reports each containing five levels. + +The engineers are trying to figure out which reports are safe. The Red-Nosed reactor safety systems can only tolerate +levels that are either gradually increasing or gradually decreasing. So, a report only counts as safe if both of the +following are true: + +The levels are either all increasing or all decreasing. +Any two adjacent levels differ by at least one and at most three. +In the example above, the reports can be found safe or unsafe by checking those rules: + +- `7 6 4 2 1`: Safe because the levels are all decreasing by 1 or 2. +- `1 2 7 8 9`: Unsafe because 2 7 is an increase of 5. +- `9 7 6 2 1`: Unsafe because 6 2 is a decrease of 4. +- `1 3 2 4 5`: Unsafe because 1 3 is increasing but 3 2 is decreasing. +- `8 6 4 4 1`: Unsafe because 4 4 is neither an increase or a decrease. +- `1 3 6 7 9`: Safe because the levels are all increasing by 1, 2, or 3. +- +So, in this example, 2 reports are safe. + +Analyze the unusual data from the engineers. How many reports are safe? + +## --- Part Two --- + +The engineers are surprised by the low number of safe reports until they realize they forgot to tell you about the +Problem Dampener. + +The Problem Dampener is a reactor-mounted module that lets the reactor safety systems tolerate a single bad level in +what would otherwise be a safe report. It's like the bad level never happened! + +Now, the same rules apply as before, except if removing a single level from an unsafe report would make it safe, the +report instead counts as safe. + +More of the above example's reports are now safe: + +- `7 6 4 2 1`: Safe without removing any level. +- `1 2 7 8 9`: Unsafe regardless of which level is removed. +- `9 7 6 2 1`: Unsafe regardless of which level is removed. +- `1 3 2 4 5`: Safe by removing the second level, 3. +- `8 6 4 4 1`: Safe by removing the third level, 4. +- `1 3 6 7 9`: Safe without removing any level. +Thanks to the Problem Dampener, 4 reports are actually safe! + +Update your analysis by handling situations where the Problem Dampener can remove a single level from unsafe reports. +How many reports are now safe? + diff --git a/internal/puzzles/solutions/2024/day02/testdata/input.txt b/internal/puzzles/solutions/2024/day02/testdata/input.txt new file mode 100644 index 00000000..82cd6799 --- /dev/null +++ b/internal/puzzles/solutions/2024/day02/testdata/input.txt @@ -0,0 +1,6 @@ +7 6 4 2 1 +1 2 7 8 9 +9 7 6 2 1 +1 3 2 4 5 +8 6 4 4 1 +1 3 6 7 9 \ No newline at end of file diff --git a/internal/puzzles/solutions/register_2024.go b/internal/puzzles/solutions/register_2024.go index 2bf6def2..a57a3468 100644 --- a/internal/puzzles/solutions/register_2024.go +++ b/internal/puzzles/solutions/register_2024.go @@ -6,4 +6,6 @@ import ( */ // register day01 solution. _ "github.com/obalunenko/advent-of-code/internal/puzzles/solutions/2024/day01" + // register day02 solution. + _ "github.com/obalunenko/advent-of-code/internal/puzzles/solutions/2024/day02" ) diff --git a/internal/puzzles/solutions/templates/solution.go.tmpl b/internal/puzzles/solutions/templates/solution.go.tmpl index c4b7fce1..2880f1d6 100644 --- a/internal/puzzles/solutions/templates/solution.go.tmpl +++ b/internal/puzzles/solutions/templates/solution.go.tmpl @@ -21,10 +21,10 @@ func (s solution) Day() string { return puzzles.Day{{ .DayStr }}.String() } -func (s solution) Part1(input io.Reader) (string, error) { +func (s solution) Part1(_ io.Reader) (string, error) { return "", puzzles.ErrNotImplemented } -func (s solution) Part2(input io.Reader) (string, error) { +func (s solution) Part2(_ io.Reader) (string, error) { return "", puzzles.ErrNotImplemented } diff --git a/internal/puzzles/solutions/templates/solution_test.go.tmpl b/internal/puzzles/solutions/templates/solution_test.go.tmpl index e1fddad1..adf17eac 100644 --- a/internal/puzzles/solutions/templates/solution_test.go.tmpl +++ b/internal/puzzles/solutions/templates/solution_test.go.tmpl @@ -48,11 +48,11 @@ func Test_solution_Part1(t *testing.T) { args: args{ input: utils.ReaderFromFile(t, filepath.Join("testdata", "input.txt")), }, - want: "8", + want: "", wantErr: assert.NoError, }, { - name: "", + name: "error case", args: args{ input: iotest.ErrReader(errors.New("custom error")), }, @@ -87,7 +87,7 @@ func Test_solution_Part2(t *testing.T) { wantErr assert.ErrorAssertionFunc }{ { - name: "", + name: "test example from description", args: args{ input: utils.ReaderFromFile(t, filepath.Join("testdata", "input.txt")), }, @@ -95,7 +95,7 @@ func Test_solution_Part2(t *testing.T) { wantErr: assert.NoError, }, { - name: "", + name: "error case", args: args{ input: iotest.ErrReader(errors.New("custom error")), }, diff --git a/tests/regression_2024_test.go b/tests/regression_2024_test.go index 09341465..b985391d 100644 --- a/tests/regression_2024_test.go +++ b/tests/regression_2024_test.go @@ -33,10 +33,10 @@ func testcases2024(tb testing.TB) []testcase { want: puzzles.Result{ Year: year.String(), Name: puzzles.Day02.String(), - Part1: "", + Part1: "334", Part2: "", }, - wantErr: true, + wantErr: false, }, { name: tcName(tb, year, puzzles.Day03),