Skip to content

Commit 2053cce

Browse files
authored
feat: add 2021/day02 puzzle specs and tests (#59)
* feat: add 2021/day02 puzzle specs and tests * feat: Implement 2021/day02 part1 * tests: Update regression tests * feat: Update 2021/day02 spec and tests for part2 * feat: Implement 2021/day02 part2 * tests: Add regression test for 2021/day02 * docs: Mark 2021/day02 as solved * style: Fix sonar code smells
1 parent 55376b4 commit 2053cce

File tree

6 files changed

+388
-4
lines changed

6 files changed

+388
-4
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ This repository contains solutions for puzzles and cli tool to run solutions to
220220
<summary>2021</summary>
221221

222222
- [x] [Day 1: Sonar Sweep](https://adventofcode.com/2021/day/1)
223-
- [ ] [Day 2: Dive!](https://adventofcode.com/2021/day/2)
223+
- [x] [Day 2: Dive!](https://adventofcode.com/2021/day/2)
224224
- [ ] [Day 3: Binary Diagnostic](https://adventofcode.com/2021/day/3)
225225
- [ ] [Day 4: ?](https://adventofcode.com/2021/day/4)
226226
- [ ] [Day 5: ?](https://adventofcode.com/2021/day/5)
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
// Package day02 contains solution for https://adventofcode.com/2021/day/2 puzzle.
2+
package day02
3+
4+
import (
5+
"bufio"
6+
"errors"
7+
"fmt"
8+
"io"
9+
"regexp"
10+
"strconv"
11+
12+
"github.com/obalunenko/advent-of-code/internal/puzzles"
13+
)
14+
15+
func init() {
16+
puzzles.Register(solution{})
17+
}
18+
19+
type solution struct{}
20+
21+
func (s solution) Year() string {
22+
return puzzles.Year2021.String()
23+
}
24+
25+
func (s solution) Day() string {
26+
return puzzles.Day02.String()
27+
}
28+
29+
func (s solution) Part1(input io.Reader) (string, error) {
30+
subm := newSubmarine()
31+
32+
return submarineDive(input, &subm)
33+
}
34+
35+
func (s solution) Part2(input io.Reader) (string, error) {
36+
subm := newSubmarineWithAim()
37+
38+
return submarineDive(input, &subm)
39+
}
40+
41+
func submarineDive(input io.Reader, subm submarineMover) (string, error) {
42+
scanner := bufio.NewScanner(input)
43+
44+
for scanner.Scan() {
45+
line := scanner.Text()
46+
47+
act, err := parseAction(line)
48+
if err != nil {
49+
return "", fmt.Errorf("parse action: %w", err)
50+
}
51+
52+
if err = subm.move(act); err != nil {
53+
return "", fmt.Errorf("submarine move: %w", err)
54+
}
55+
}
56+
57+
if err := scanner.Err(); err != nil {
58+
return "", fmt.Errorf("scanner error: %w", err)
59+
}
60+
61+
res := subm.position().x * subm.position().y
62+
63+
return strconv.Itoa(res), nil
64+
}
65+
66+
type action struct {
67+
move move
68+
steps int
69+
}
70+
71+
var moveRe = regexp.MustCompile(`(?s)(\w+)\s(\d+)`)
72+
73+
const (
74+
_ = iota
75+
movePos
76+
moveNumPos
77+
78+
totalMatches = 3
79+
)
80+
81+
var errInvalidFormat = errors.New("invalid action format")
82+
83+
func parseAction(s string) (action, error) {
84+
matches := moveRe.FindStringSubmatch(s)
85+
if len(matches) != totalMatches {
86+
return action{}, fmt.Errorf("[%s]: %w", s, errInvalidFormat)
87+
}
88+
89+
m, err := parseMove(matches[movePos])
90+
if err != nil {
91+
return action{}, fmt.Errorf("parse move: %w", err)
92+
}
93+
94+
n, err := strconv.Atoi(matches[moveNumPos])
95+
if err != nil {
96+
return action{}, fmt.Errorf("parse steps num: %w", err)
97+
}
98+
99+
return action{
100+
move: m,
101+
steps: n,
102+
}, nil
103+
}
104+
105+
type position struct {
106+
x int // horizontal position
107+
y int // depth
108+
}
109+
110+
type move string
111+
112+
const (
113+
moveUp = "up"
114+
moveDown = "down"
115+
moveForward = "forward"
116+
)
117+
118+
func parseMove(s string) (move, error) {
119+
var m move
120+
121+
switch s {
122+
case moveUp:
123+
m = moveUp
124+
case moveForward:
125+
m = moveForward
126+
case moveDown:
127+
m = moveDown
128+
default:
129+
return "", fmt.Errorf("[%s]: %w", s, errInvalidMove)
130+
}
131+
132+
return m, nil
133+
}
134+
135+
type submarineMover interface {
136+
move(act action) error
137+
position() position
138+
}
139+
140+
type submarine struct {
141+
pos position
142+
}
143+
144+
func (s *submarine) position() position {
145+
return s.pos
146+
}
147+
148+
func newSubmarine() submarine {
149+
return submarine{
150+
pos: position{
151+
x: 0,
152+
y: 0,
153+
},
154+
}
155+
}
156+
157+
var errInvalidMove = errors.New("invalid move")
158+
159+
func (s *submarine) move(act action) error {
160+
switch act.move {
161+
case moveUp:
162+
s.pos.y -= act.steps
163+
case moveForward:
164+
s.pos.x += act.steps
165+
case moveDown:
166+
s.pos.y += act.steps
167+
default:
168+
return errInvalidMove
169+
}
170+
171+
return nil
172+
}
173+
174+
type submarineWithAim struct {
175+
submarine
176+
aim int
177+
}
178+
179+
func newSubmarineWithAim() submarineWithAim {
180+
return submarineWithAim{
181+
submarine: newSubmarine(),
182+
aim: 0,
183+
}
184+
}
185+
186+
func (s *submarineWithAim) move(act action) error {
187+
switch act.move {
188+
case moveUp:
189+
s.aim -= act.steps
190+
case moveForward:
191+
s.pos.x += act.steps
192+
s.pos.y += s.aim * act.steps
193+
case moveDown:
194+
s.aim += act.steps
195+
default:
196+
return errInvalidMove
197+
}
198+
199+
return nil
200+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package day02
2+
3+
import (
4+
"io"
5+
"strings"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func Test_solution_Year(t *testing.T) {
12+
var s solution
13+
14+
want := "2021"
15+
got := s.Year()
16+
17+
assert.Equal(t, want, got)
18+
}
19+
20+
func Test_solution_Day(t *testing.T) {
21+
var s solution
22+
23+
want := "2"
24+
got := s.Day()
25+
26+
assert.Equal(t, want, got)
27+
}
28+
29+
func Test_solution_Part1(t *testing.T) {
30+
var s solution
31+
32+
type args struct {
33+
input io.Reader
34+
}
35+
36+
tests := []struct {
37+
name string
38+
args args
39+
want string
40+
wantErr bool
41+
}{
42+
{
43+
name: "test example from description",
44+
args: args{
45+
input: strings.NewReader("forward 5\ndown 5\nforward 8\nup 3\ndown 8\nforward 2\n"),
46+
},
47+
want: "150",
48+
wantErr: false,
49+
},
50+
}
51+
52+
for _, tt := range tests {
53+
t.Run(tt.name, func(t *testing.T) {
54+
got, err := s.Part1(tt.args.input)
55+
if tt.wantErr {
56+
assert.Error(t, err)
57+
58+
return
59+
}
60+
61+
assert.NoError(t, err)
62+
assert.Equal(t, tt.want, got)
63+
})
64+
}
65+
}
66+
67+
func Test_solution_Part2(t *testing.T) {
68+
var s solution
69+
70+
type args struct {
71+
input io.Reader
72+
}
73+
74+
tests := []struct {
75+
name string
76+
args args
77+
want string
78+
wantErr bool
79+
}{
80+
{
81+
name: "test example from description",
82+
args: args{
83+
input: strings.NewReader("forward 5\ndown 5\nforward 8\nup 3\ndown 8\nforward 2\n"),
84+
},
85+
want: "900",
86+
wantErr: false,
87+
},
88+
}
89+
90+
for _, tt := range tests {
91+
t.Run(tt.name, func(t *testing.T) {
92+
got, err := s.Part2(tt.args.input)
93+
if tt.wantErr {
94+
assert.Error(t, err)
95+
96+
return
97+
}
98+
99+
assert.NoError(t, err)
100+
assert.Equal(t, tt.want, got)
101+
})
102+
}
103+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# --- Day 2: Dive! ---
2+
3+
## --- Part One ---
4+
5+
Now, you need to figure out how to pilot this thing.
6+
7+
It seems like the submarine can take a series of commands like `forward 1`, `down 2`, or `up 3`:
8+
9+
- `forward X` increases the horizontal position by `X` units.
10+
- `down X` increases the depth by `X` units.
11+
- `up X` decreases the depth by `X` units.
12+
-
13+
Note that since you're on a submarine, `down` and `up` affect your `depth`, and so they have the
14+
opposite result of what you might expect.
15+
16+
The submarine seems to already have a planned course (your puzzle input).
17+
You should probably figure out where it's going.
18+
19+
### For example:
20+
21+
```text
22+
forward 5
23+
down 5
24+
forward 8
25+
up 3
26+
down 8
27+
forward 2
28+
```
29+
30+
Your horizontal position and depth both start at `0`. The steps above would then modify them as follows:
31+
32+
- `forward 5` adds `5` to your horizontal position, a total of `5`.
33+
- `down 5` adds `5` to your depth, resulting in a value of `5`.
34+
- `forward 8` adds `8` to your horizontal position, a total of `13`.
35+
- `up 3` decreases your depth by `3`, resulting in a value of `2`.
36+
- `down 8` adds `8` to your depth, resulting in a value of `10`.
37+
- `forward 2` adds `2` to your horizontal position, a total of `15`.
38+
39+
After following these instructions, you would have a horizontal position of `15` and a depth of `10`.
40+
(Multiplying these together produces `150`.)
41+
42+
Calculate the horizontal position and depth you would have after following the planned course.
43+
What do you get if you multiply your final horizontal position by your final depth?
44+
45+
46+
## --- Part Two ---
47+
48+
Based on your calculations, the planned course doesn't seem to make any sense.
49+
You find the submarine manual and discover that the process is actually slightly more complicated.
50+
51+
In addition to horizontal position and depth, you'll also need to track a third value, `aim`, which also starts at `0`.
52+
The commands also mean something entirely different than you first thought:
53+
54+
- `down X` **increases** your aim by `X` units.
55+
- `up X` **decreases** your aim by `X` units.
56+
- `forward X` does two things:
57+
- It **increases** your `horizontal` position by `X` units.
58+
- It **increases** your `depth` by your aim **multiplied by** `X`.
59+
60+
Again note that since you're on a submarine, down and up do the opposite of what
61+
you might expect: "down" means aiming in the positive direction.
62+
63+
Now, the above example does something different:
64+
65+
- `forward 5` adds `5` to your horizontal position, a total of `5`.
66+
Because your aim is `0`, your depth does not change.
67+
- `down 5` adds `5` to your aim, resulting in a value of `5`.
68+
- `forward 8` adds `8` to your horizontal position, a total of `13`.
69+
Because your aim is `5`, your depth increases by `8*5=40`.
70+
- `up 3` decreases your aim by `3`, resulting in a value of `2`.
71+
- `down 8` adds `8` to your aim, resulting in a value of `10`.
72+
- `forward 2` adds `2` to your horizontal position, a total of `15`.
73+
Because your aim is `10`, your depth increases by `2*10=20` to a total of `60`.
74+
75+
After following these new instructions, you would have a horizontal position of `15` and a depth of `60`.
76+
(Multiplying these produces `900`.)
77+
78+
Using this new interpretation of the commands, calculate the horizontal position and depth you would have after
79+
following the planned course. What do you get if you multiply your final horizontal position by your final depth?

0 commit comments

Comments
 (0)