-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
55376b4
commit 2053cce
Showing
6 changed files
with
388 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
// Package day02 contains solution for https://adventofcode.com/2021/day/2 puzzle. | ||
package day02 | ||
|
||
import ( | ||
"bufio" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"regexp" | ||
"strconv" | ||
|
||
"github.com/obalunenko/advent-of-code/internal/puzzles" | ||
) | ||
|
||
func init() { | ||
puzzles.Register(solution{}) | ||
} | ||
|
||
type solution struct{} | ||
|
||
func (s solution) Year() string { | ||
return puzzles.Year2021.String() | ||
} | ||
|
||
func (s solution) Day() string { | ||
return puzzles.Day02.String() | ||
} | ||
|
||
func (s solution) Part1(input io.Reader) (string, error) { | ||
subm := newSubmarine() | ||
|
||
return submarineDive(input, &subm) | ||
} | ||
|
||
func (s solution) Part2(input io.Reader) (string, error) { | ||
subm := newSubmarineWithAim() | ||
|
||
return submarineDive(input, &subm) | ||
} | ||
|
||
func submarineDive(input io.Reader, subm submarineMover) (string, error) { | ||
scanner := bufio.NewScanner(input) | ||
|
||
for scanner.Scan() { | ||
line := scanner.Text() | ||
|
||
act, err := parseAction(line) | ||
if err != nil { | ||
return "", fmt.Errorf("parse action: %w", err) | ||
} | ||
|
||
if err = subm.move(act); err != nil { | ||
return "", fmt.Errorf("submarine move: %w", err) | ||
} | ||
} | ||
|
||
if err := scanner.Err(); err != nil { | ||
return "", fmt.Errorf("scanner error: %w", err) | ||
} | ||
|
||
res := subm.position().x * subm.position().y | ||
|
||
return strconv.Itoa(res), nil | ||
} | ||
|
||
type action struct { | ||
move move | ||
steps int | ||
} | ||
|
||
var moveRe = regexp.MustCompile(`(?s)(\w+)\s(\d+)`) | ||
|
||
const ( | ||
_ = iota | ||
movePos | ||
moveNumPos | ||
|
||
totalMatches = 3 | ||
) | ||
|
||
var errInvalidFormat = errors.New("invalid action format") | ||
|
||
func parseAction(s string) (action, error) { | ||
matches := moveRe.FindStringSubmatch(s) | ||
if len(matches) != totalMatches { | ||
return action{}, fmt.Errorf("[%s]: %w", s, errInvalidFormat) | ||
} | ||
|
||
m, err := parseMove(matches[movePos]) | ||
if err != nil { | ||
return action{}, fmt.Errorf("parse move: %w", err) | ||
} | ||
|
||
n, err := strconv.Atoi(matches[moveNumPos]) | ||
if err != nil { | ||
return action{}, fmt.Errorf("parse steps num: %w", err) | ||
} | ||
|
||
return action{ | ||
move: m, | ||
steps: n, | ||
}, nil | ||
} | ||
|
||
type position struct { | ||
x int // horizontal position | ||
y int // depth | ||
} | ||
|
||
type move string | ||
|
||
const ( | ||
moveUp = "up" | ||
moveDown = "down" | ||
moveForward = "forward" | ||
) | ||
|
||
func parseMove(s string) (move, error) { | ||
var m move | ||
|
||
switch s { | ||
case moveUp: | ||
m = moveUp | ||
case moveForward: | ||
m = moveForward | ||
case moveDown: | ||
m = moveDown | ||
default: | ||
return "", fmt.Errorf("[%s]: %w", s, errInvalidMove) | ||
} | ||
|
||
return m, nil | ||
} | ||
|
||
type submarineMover interface { | ||
move(act action) error | ||
position() position | ||
} | ||
|
||
type submarine struct { | ||
pos position | ||
} | ||
|
||
func (s *submarine) position() position { | ||
return s.pos | ||
} | ||
|
||
func newSubmarine() submarine { | ||
return submarine{ | ||
pos: position{ | ||
x: 0, | ||
y: 0, | ||
}, | ||
} | ||
} | ||
|
||
var errInvalidMove = errors.New("invalid move") | ||
|
||
func (s *submarine) move(act action) error { | ||
switch act.move { | ||
case moveUp: | ||
s.pos.y -= act.steps | ||
case moveForward: | ||
s.pos.x += act.steps | ||
case moveDown: | ||
s.pos.y += act.steps | ||
default: | ||
return errInvalidMove | ||
} | ||
|
||
return nil | ||
} | ||
|
||
type submarineWithAim struct { | ||
submarine | ||
aim int | ||
} | ||
|
||
func newSubmarineWithAim() submarineWithAim { | ||
return submarineWithAim{ | ||
submarine: newSubmarine(), | ||
aim: 0, | ||
} | ||
} | ||
|
||
func (s *submarineWithAim) move(act action) error { | ||
switch act.move { | ||
case moveUp: | ||
s.aim -= act.steps | ||
case moveForward: | ||
s.pos.x += act.steps | ||
s.pos.y += s.aim * act.steps | ||
case moveDown: | ||
s.aim += act.steps | ||
default: | ||
return errInvalidMove | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
package day02 | ||
|
||
import ( | ||
"io" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func Test_solution_Year(t *testing.T) { | ||
var s solution | ||
|
||
want := "2021" | ||
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 bool | ||
}{ | ||
{ | ||
name: "test example from description", | ||
args: args{ | ||
input: strings.NewReader("forward 5\ndown 5\nforward 8\nup 3\ndown 8\nforward 2\n"), | ||
}, | ||
want: "150", | ||
wantErr: false, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
got, err := s.Part1(tt.args.input) | ||
if tt.wantErr { | ||
assert.Error(t, err) | ||
|
||
return | ||
} | ||
|
||
assert.NoError(t, err) | ||
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 bool | ||
}{ | ||
{ | ||
name: "test example from description", | ||
args: args{ | ||
input: strings.NewReader("forward 5\ndown 5\nforward 8\nup 3\ndown 8\nforward 2\n"), | ||
}, | ||
want: "900", | ||
wantErr: false, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
got, err := s.Part2(tt.args.input) | ||
if tt.wantErr { | ||
assert.Error(t, err) | ||
|
||
return | ||
} | ||
|
||
assert.NoError(t, err) | ||
assert.Equal(t, tt.want, got) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
# --- Day 2: Dive! --- | ||
|
||
## --- Part One --- | ||
|
||
Now, you need to figure out how to pilot this thing. | ||
|
||
It seems like the submarine can take a series of commands like `forward 1`, `down 2`, or `up 3`: | ||
|
||
- `forward X` increases the horizontal position by `X` units. | ||
- `down X` increases the depth by `X` units. | ||
- `up X` decreases the depth by `X` units. | ||
- | ||
Note that since you're on a submarine, `down` and `up` affect your `depth`, and so they have the | ||
opposite result of what you might expect. | ||
|
||
The submarine seems to already have a planned course (your puzzle input). | ||
You should probably figure out where it's going. | ||
|
||
### For example: | ||
|
||
```text | ||
forward 5 | ||
down 5 | ||
forward 8 | ||
up 3 | ||
down 8 | ||
forward 2 | ||
``` | ||
|
||
Your horizontal position and depth both start at `0`. The steps above would then modify them as follows: | ||
|
||
- `forward 5` adds `5` to your horizontal position, a total of `5`. | ||
- `down 5` adds `5` to your depth, resulting in a value of `5`. | ||
- `forward 8` adds `8` to your horizontal position, a total of `13`. | ||
- `up 3` decreases your depth by `3`, resulting in a value of `2`. | ||
- `down 8` adds `8` to your depth, resulting in a value of `10`. | ||
- `forward 2` adds `2` to your horizontal position, a total of `15`. | ||
|
||
After following these instructions, you would have a horizontal position of `15` and a depth of `10`. | ||
(Multiplying these together produces `150`.) | ||
|
||
Calculate the horizontal position and depth you would have after following the planned course. | ||
What do you get if you multiply your final horizontal position by your final depth? | ||
|
||
|
||
## --- Part Two --- | ||
|
||
Based on your calculations, the planned course doesn't seem to make any sense. | ||
You find the submarine manual and discover that the process is actually slightly more complicated. | ||
|
||
In addition to horizontal position and depth, you'll also need to track a third value, `aim`, which also starts at `0`. | ||
The commands also mean something entirely different than you first thought: | ||
|
||
- `down X` **increases** your aim by `X` units. | ||
- `up X` **decreases** your aim by `X` units. | ||
- `forward X` does two things: | ||
- It **increases** your `horizontal` position by `X` units. | ||
- It **increases** your `depth` by your aim **multiplied by** `X`. | ||
|
||
Again note that since you're on a submarine, down and up do the opposite of what | ||
you might expect: "down" means aiming in the positive direction. | ||
|
||
Now, the above example does something different: | ||
|
||
- `forward 5` adds `5` to your horizontal position, a total of `5`. | ||
Because your aim is `0`, your depth does not change. | ||
- `down 5` adds `5` to your aim, resulting in a value of `5`. | ||
- `forward 8` adds `8` to your horizontal position, a total of `13`. | ||
Because your aim is `5`, your depth increases by `8*5=40`. | ||
- `up 3` decreases your aim by `3`, resulting in a value of `2`. | ||
- `down 8` adds `8` to your aim, resulting in a value of `10`. | ||
- `forward 2` adds `2` to your horizontal position, a total of `15`. | ||
Because your aim is `10`, your depth increases by `2*10=20` to a total of `60`. | ||
|
||
After following these new instructions, you would have a horizontal position of `15` and a depth of `60`. | ||
(Multiplying these produces `900`.) | ||
|
||
Using this new interpretation of the commands, calculate the horizontal position and depth you would have after | ||
following the planned course. What do you get if you multiply your final horizontal position by your final depth? |
Oops, something went wrong.