From 03111b37549c2dc799a3a5979efc368b842ca639 Mon Sep 17 00:00:00 2001 From: Oleg Balunenko Date: Tue, 17 Dec 2024 03:19:52 +0400 Subject: [PATCH 1/8] feat: Generate boilerplate for 2024/day02 --- .../puzzles/solutions/2024/day02/solution.go | 30 +++++ .../solutions/2024/day02/solution_test.go | 117 ++++++++++++++++++ internal/puzzles/solutions/2024/day02/spec.md | 53 ++++++++ .../solutions/2024/day02/testdata/input.txt | 6 + internal/puzzles/solutions/register_2024.go | 2 + .../solutions/templates/solution_test.go.tmpl | 2 +- 6 files changed, 209 insertions(+), 1 deletion(-) create mode 100755 internal/puzzles/solutions/2024/day02/solution.go create mode 100755 internal/puzzles/solutions/2024/day02/solution_test.go create mode 100755 internal/puzzles/solutions/2024/day02/spec.md create mode 100644 internal/puzzles/solutions/2024/day02/testdata/input.txt diff --git a/internal/puzzles/solutions/2024/day02/solution.go b/internal/puzzles/solutions/2024/day02/solution.go new file mode 100755 index 00000000..32fcd120 --- /dev/null +++ b/internal/puzzles/solutions/2024/day02/solution.go @@ -0,0 +1,30 @@ +// Package day02 contains solution for https://adventofcode.com/2024/day/2 puzzle. +package day02 + +import ( + "io" + + "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.Day02.String() +} + +func (s solution) Part1(input io.Reader) (string, error) { + return "", puzzles.ErrNotImplemented +} + +func (s solution) Part2(input io.Reader) (string, error) { + return "", puzzles.ErrNotImplemented +} 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..60b62148 --- /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: "", + args: args{ + input: utils.ReaderFromFile(t, filepath.Join("testdata", "input.txt")), + }, + want: "", + 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..19ef4e57 --- /dev/null +++ b/internal/puzzles/solutions/2024/day02/spec.md @@ -0,0 +1,53 @@ +# 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 --- + + + 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_test.go.tmpl b/internal/puzzles/solutions/templates/solution_test.go.tmpl index e1fddad1..c08779cb 100644 --- a/internal/puzzles/solutions/templates/solution_test.go.tmpl +++ b/internal/puzzles/solutions/templates/solution_test.go.tmpl @@ -48,7 +48,7 @@ func Test_solution_Part1(t *testing.T) { args: args{ input: utils.ReaderFromFile(t, filepath.Join("testdata", "input.txt")), }, - want: "8", + want: "", wantErr: assert.NoError, }, { From 6451ed4e80f909a154309285e0f2553616219b9d Mon Sep 17 00:00:00 2001 From: Oleg Balunenko Date: Wed, 18 Dec 2024 05:12:54 +0400 Subject: [PATCH 2/8] fix: Boilerplate template --- internal/puzzles/solutions/templates/solution.go.tmpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 } From acdda100ca1a0d586a37723671b583148e67b7b5 Mon Sep 17 00:00:00 2001 From: Oleg Balunenko Date: Wed, 18 Dec 2024 05:13:09 +0400 Subject: [PATCH 3/8] feat: Implement 2024 day 2 part 1 --- .../puzzles/solutions/2024/day02/solution.go | 67 ++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/internal/puzzles/solutions/2024/day02/solution.go b/internal/puzzles/solutions/2024/day02/solution.go index 32fcd120..c7fde34f 100755 --- a/internal/puzzles/solutions/2024/day02/solution.go +++ b/internal/puzzles/solutions/2024/day02/solution.go @@ -2,9 +2,14 @@ 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() { @@ -22,9 +27,67 @@ func (s solution) Day() string { } func (s solution) Part1(input io.Reader) (string, error) { - return "", puzzles.ErrNotImplemented + 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) { +func (s solution) Part2(_ io.Reader) (string, error) { return "", puzzles.ErrNotImplemented } From fbea0d43852f5332da2443ef40c6fc9d00f0d2ba Mon Sep 17 00:00:00 2001 From: Oleg Balunenko Date: Wed, 18 Dec 2024 05:14:08 +0400 Subject: [PATCH 4/8] chore: Update regression tests --- tests/regression_2024_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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), From f135a09f413a7665eace901764638209be33a610 Mon Sep 17 00:00:00 2001 From: Oleg Balunenko Date: Wed, 18 Dec 2024 05:16:26 +0400 Subject: [PATCH 5/8] chore: Update boilerplate template --- internal/puzzles/solutions/templates/solution_test.go.tmpl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/puzzles/solutions/templates/solution_test.go.tmpl b/internal/puzzles/solutions/templates/solution_test.go.tmpl index c08779cb..adf17eac 100644 --- a/internal/puzzles/solutions/templates/solution_test.go.tmpl +++ b/internal/puzzles/solutions/templates/solution_test.go.tmpl @@ -52,7 +52,7 @@ func Test_solution_Part1(t *testing.T) { 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")), }, From 60571f987855837eda9462e30b87cc2fd13d0846 Mon Sep 17 00:00:00 2001 From: Oleg Balunenko Date: Wed, 18 Dec 2024 05:16:43 +0400 Subject: [PATCH 6/8] doc: Add part 2 docs --- internal/puzzles/solutions/2024/day02/spec.md | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/internal/puzzles/solutions/2024/day02/spec.md b/internal/puzzles/solutions/2024/day02/spec.md index 19ef4e57..bc326f3b 100755 --- a/internal/puzzles/solutions/2024/day02/spec.md +++ b/internal/puzzles/solutions/2024/day02/spec.md @@ -49,5 +49,25 @@ 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? From 72bf3d42ae440c5594b828621ab20428b6da75bc Mon Sep 17 00:00:00 2001 From: Oleg Balunenko Date: Wed, 18 Dec 2024 05:16:53 +0400 Subject: [PATCH 7/8] chore: Update tests for part 2 --- internal/puzzles/solutions/2024/day02/solution_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/puzzles/solutions/2024/day02/solution_test.go b/internal/puzzles/solutions/2024/day02/solution_test.go index 60b62148..20262adb 100755 --- a/internal/puzzles/solutions/2024/day02/solution_test.go +++ b/internal/puzzles/solutions/2024/day02/solution_test.go @@ -87,11 +87,11 @@ 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")), }, - want: "", + want: "4", wantErr: assert.NoError, }, { From 47c7f8718cfc63cfa94fa912a49585c356ff8a6d Mon Sep 17 00:00:00 2001 From: Oleg Balunenko Date: Mon, 17 Feb 2025 15:15:48 +0400 Subject: [PATCH 8/8] feat: Implement part 2 --- .../puzzles/solutions/2024/day02/solution.go | 95 ++++++++++++++++++- 1 file changed, 93 insertions(+), 2 deletions(-) diff --git a/internal/puzzles/solutions/2024/day02/solution.go b/internal/puzzles/solutions/2024/day02/solution.go index c7fde34f..97c23d0b 100755 --- a/internal/puzzles/solutions/2024/day02/solution.go +++ b/internal/puzzles/solutions/2024/day02/solution.go @@ -88,6 +88,97 @@ func (s solution) Part1(input io.Reader) (string, error) { return strconv.Itoa(safeCount), nil } -func (s solution) Part2(_ io.Reader) (string, error) { - return "", puzzles.ErrNotImplemented +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:]...) }