diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index 23ebdd60..725c77ec 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -69,7 +69,7 @@ jobs: - name: Cover report if: success() run: | - bash <(curl -s https://codecov.io/bash) + bash <(curl -s https://codecov.io/bash) -f ./coverage/full.cov - name: Install GoReleaser uses: goreleaser/goreleaser-action@v2 diff --git a/.gitignore b/.gitignore index 37d76c98..e081f134 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,5 @@ bin/ dist/ release/ -.DS_Store \ No newline at end of file +.DS_Store +coverage/ \ No newline at end of file diff --git a/README.md b/README.md index cc015b71..51b47c27 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ This repository contains solutions for puzzles and cli tool to run solutions to
2016 - - [ ] [Day 1: No Time for a Taxicab](https://adventofcode.com/2016/day/1) + - [x] [Day 1: No Time for a Taxicab](https://adventofcode.com/2016/day/1) - [ ] [Day 2: Bathroom Security](https://adventofcode.com/2016/day/2) - [ ] [Day 3: Squares With Three Sides](https://adventofcode.com/2016/day/3) - [ ] [Day 4: Security Through Obscurity](https://adventofcode.com/2016/day/4) diff --git a/cmd/aoc-cli/main.go b/cmd/aoc-cli/main.go index 03706a5c..b92e0641 100644 --- a/cmd/aoc-cli/main.go +++ b/cmd/aoc-cli/main.go @@ -11,9 +11,8 @@ import ( log "github.com/sirupsen/logrus" "github.com/urfave/cli" - "github.com/obalunenko/advent-of-code/internal/input" "github.com/obalunenko/advent-of-code/internal/puzzles" - + "github.com/obalunenko/advent-of-code/internal/puzzles/input" // register all solutions. _ "github.com/obalunenko/advent-of-code/internal/puzzles/solutions" ) diff --git a/internal/input/content.go b/internal/puzzles/input/content.go similarity index 100% rename from internal/input/content.go rename to internal/puzzles/input/content.go diff --git a/internal/input/data/2015/day01.txt b/internal/puzzles/input/data/2015/day01.txt similarity index 100% rename from internal/input/data/2015/day01.txt rename to internal/puzzles/input/data/2015/day01.txt diff --git a/internal/puzzles/input/data/2016/day01.txt b/internal/puzzles/input/data/2016/day01.txt new file mode 100644 index 00000000..0c1b018b --- /dev/null +++ b/internal/puzzles/input/data/2016/day01.txt @@ -0,0 +1 @@ +R1, R3, L2, L5, L2, L1, R3, L4, R2, L2, L4, R2, L1, R1, L2, R3, L1, L4, R2, L5, R3, R4, L1, R2, L1, R3, L4, R5, L4, L5, R5, L3, R2, L3, L3, R1, R3, L4, R2, R5, L4, R1, L1, L1, R5, L2, R1, L2, R188, L5, L3, R5, R1, L2, L4, R3, R5, L3, R3, R45, L4, R4, R72, R2, R3, L1, R1, L1, L1, R192, L1, L1, L1, L4, R1, L2, L5, L3, R5, L3, R3, L4, L3, R1, R4, L2, R2, R3, L5, R3, L1, R1, R4, L2, L3, R1, R3, L4, L3, L4, L2, L2, R1, R3, L5, L1, R4, R2, L4, L1, R3, R3, R1, L5, L2, R4, R4, R2, R1, R5, R5, L4, L1, R5, R3, R4, R5, R3, L1, L2, L4, R1, R4, R5, L2, L3, R4, L4, R2, L2, L4, L2, R5, R1, R4, R3, R5, L4, L4, L5, L5, R3, R4, L1, L3, R2, L2, R1, L3, L5, R5, R5, R3, L4, L2, R4, R5, R1, R4, L3 \ No newline at end of file diff --git a/internal/input/data/2019/day01.txt b/internal/puzzles/input/data/2019/day01.txt similarity index 100% rename from internal/input/data/2019/day01.txt rename to internal/puzzles/input/data/2019/day01.txt diff --git a/internal/input/data/2019/day02.txt b/internal/puzzles/input/data/2019/day02.txt similarity index 100% rename from internal/input/data/2019/day02.txt rename to internal/puzzles/input/data/2019/day02.txt diff --git a/internal/input/data/2019/day03.txt b/internal/puzzles/input/data/2019/day03.txt similarity index 100% rename from internal/input/data/2019/day03.txt rename to internal/puzzles/input/data/2019/day03.txt diff --git a/internal/input/data/2019/day04.txt b/internal/puzzles/input/data/2019/day04.txt similarity index 100% rename from internal/input/data/2019/day04.txt rename to internal/puzzles/input/data/2019/day04.txt diff --git a/internal/input/data/2020/day01.txt b/internal/puzzles/input/data/2020/day01.txt similarity index 100% rename from internal/input/data/2020/day01.txt rename to internal/puzzles/input/data/2020/day01.txt diff --git a/internal/input/data/2020/day02.txt b/internal/puzzles/input/data/2020/day02.txt similarity index 100% rename from internal/input/data/2020/day02.txt rename to internal/puzzles/input/data/2020/day02.txt diff --git a/internal/puzzles/solutions/2016/day01/solution.go b/internal/puzzles/solutions/2016/day01/solution.go new file mode 100644 index 00000000..cfc42722 --- /dev/null +++ b/internal/puzzles/solutions/2016/day01/solution.go @@ -0,0 +1,387 @@ +package day01 + +import ( + "bytes" + "errors" + "fmt" + "io" + "regexp" + "strconv" + "strings" + "sync" + + "github.com/obalunenko/advent-of-code/internal/puzzles" +) + +const ( + puzzleName = "day01" + year = "2016" +) + +type solution struct { + year string + name string +} + +func (s solution) Name() string { + return s.name +} + +func (s solution) Year() string { + return s.year +} + +func init() { + puzzles.Register(solution{ + year: year, + name: puzzleName, + }) +} + +func (s solution) Part1(input io.Reader) (string, error) { + buf := new(bytes.Buffer) + if _, err := buf.ReadFrom(input); err != nil { + return "", fmt.Errorf("failed to read input: %w", err) + } + + c := newCab() + + go func() { + c.n.start() + }() + + cmds := strings.Split(buf.String(), ", ") + for _, cmd := range cmds { + t, s, err := splitCommand(cmd) + if err != nil { + return "", fmt.Errorf("split command: %w", err) + } + + if err = c.Move(t, s); err != nil { + return "", fmt.Errorf("move: %w", err) + } + } + + c.n.stop() + + l := c.n.Pos().manhattan() + + return strconv.Itoa(l), nil +} + +func (s solution) Part2(input io.Reader) (string, error) { + buf := new(bytes.Buffer) + if _, err := buf.ReadFrom(input); err != nil { + return "", fmt.Errorf("failed to read input: %w", err) + } + + c := newCab() + + go c.n.start() + + cmds := strings.Split(buf.String(), ", ") + for _, cmd := range cmds { + t, s, err := splitCommand(cmd) + if err != nil { + return "", fmt.Errorf("split command: %w", err) + } + + if err = c.Move(t, s); err != nil { + return "", fmt.Errorf("move: %w", err) + } + } + + c.n.stop() + + rl := c.n.revisitedList() + if len(rl) == 0 { + return "", errors.New("no revisited points") + } + + // get first + l := rl[0].manhattan() + + return strconv.Itoa(l), nil +} + +type turn string + +const ( + leftTurn = "L" + rightTurn = "R" +) + +var errInvalidTurn = errors.New("invalid turn value") + +func turnFromstring(s string) (turn, error) { + switch s { + case leftTurn: + return leftTurn, nil + case rightTurn: + return rightTurn, nil + default: + return "", errInvalidTurn + } +} + +type position struct { + x, y int +} + +func (p *position) addX(n int) { + p.x = p.x + n +} + +func (p *position) addY(n int) { + p.y = p.y + n +} + +func (p *position) subX(n int) { + p.x = p.x - n +} + +func (p *position) subY(n int) { + p.y = p.y - n +} + +func (p position) manhattan() int { + x, y := p.x, p.y + + if x < 0 { + x = -x + } + + if y < 0 { + y = -y + } + + return x + y +} + +type direction uint + +const ( + unknownDirection direction = iota + + northDirection + eastDirection + southDirection + westDirection + + sentinelDirection +) + +func (d direction) isValid() bool { + return d > unknownDirection && d < sentinelDirection +} + +func (d direction) strikeTo(t turn) direction { + switch t { + case rightTurn: + if d == westDirection { + return northDirection + } + + return d + 1 + case leftTurn: + if d == northDirection { + return westDirection + } + + return d - 1 + + default: + return unknownDirection + } +} + +type cab struct { + curDir direction + n navigator +} + +func newCab() cab { + return cab{ + curDir: northDirection, + n: newNavigator(), + } +} + +var errInvalidDirect = errors.New("invalid direction") + +const ( + step = 1 +) + +func (c *cab) Move(t turn, steps int) error { + c.curDir = c.curDir.strikeTo(t) + if !c.curDir.isValid() { + return errInvalidDirect + } + + switch c.curDir { + case northDirection: + c.n.moveNorth(steps) + case eastDirection: + c.n.moveEast(steps) + case southDirection: + c.n.moveSouth(steps) + case westDirection: + c.n.moveWest(steps) + } + + return nil +} + +func (n *navigator) moveNorth(steps int) { + for i := 0; i < steps; i++ { + n.mu.Lock() + n.pos.addY(step) + n.mu.Unlock() + + n.record <- n.Pos() + } +} + +func (n *navigator) moveEast(steps int) { + for i := 0; i < steps; i++ { + n.mu.Lock() + n.pos.addX(step) + n.mu.Unlock() + + n.record <- n.Pos() + } +} + +func (n *navigator) moveSouth(steps int) { + for i := 0; i < steps; i++ { + n.mu.Lock() + n.pos.subY(step) + n.mu.Unlock() + + n.record <- n.Pos() + } +} + +func (n *navigator) moveWest(steps int) { + for i := 0; i < steps; i++ { + n.mu.Lock() + n.pos.subX(step) + n.mu.Unlock() + + n.record <- n.Pos() + } +} + +func (c *cab) Track() track { + return c.n.track +} + +func (n navigator) Pos() position { + n.mu.Lock() + defer n.mu.Unlock() + + return n.pos +} + +// Example: L4, R5 +var re = regexp.MustCompile(`(?msi)(L|R)(\d+)`) + +const ( + fullMatchPos = iota + turnPos + stepsPos + + totalMatchesNum = 3 +) + +var errInvalidCMD = errors.New("invalid command") + +func splitCommand(cmd string) (turn, int, error) { + parts := re.FindStringSubmatch(cmd) + if len(parts) != totalMatchesNum { + return "", 0, errInvalidCMD + } + + t, err := turnFromstring(parts[turnPos]) + if err != nil { + return "", 0, fmt.Errorf("turnFromstring: %w", err) + } + s, err := strconv.Atoi(parts[stepsPos]) + if err != nil { + return "", 0, fmt.Errorf("invalid steps num: %w", err) + } + + return t, s, nil +} + +type navigator struct { + record chan position + pos position + track track + mu *sync.Mutex + wg *sync.WaitGroup + revisited []position +} + +func (n *navigator) recordTrack(p position) { + n.mu.Lock() + + defer func() { + n.mu.Unlock() + }() + + if n.track.isVisited(p) { + n.revisited = append(n.revisited, p) + } + + n.track.record(p) +} + +func (n *navigator) start() { + n.wg.Add(1) + + for p := range n.record { + n.recordTrack(p) + } + + n.wg.Done() +} + +func (n *navigator) stop() { + close(n.record) + + n.wg.Wait() +} + +func (n navigator) revisitedList() []position { + return n.revisited +} + +func newNavigator() navigator { + return navigator{ + record: make(chan position), + pos: position{ + x: 0, + y: 0, + }, + track: newTrack(), + mu: &sync.Mutex{}, + wg: &sync.WaitGroup{}, + revisited: []position{}, + } +} + +type track map[position]bool + +func newTrack() track { + return make(track) +} + +func (t track) record(p position) { + t[p] = true +} + +func (t track) isVisited(p position) bool { + return t[p] +} diff --git a/internal/puzzles/solutions/2016/day01/solution_test.go b/internal/puzzles/solutions/2016/day01/solution_test.go new file mode 100644 index 00000000..66d48584 --- /dev/null +++ b/internal/puzzles/solutions/2016/day01/solution_test.go @@ -0,0 +1,132 @@ +package day01 + +import ( + "io" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_solution_Part1(t *testing.T) { + type fields struct { + name string + } + + type args struct { + input io.Reader + } + + tests := []struct { + name string + fields fields + args args + want string + wantErr bool + }{ + { + name: "", + fields: fields{ + name: "", + }, + args: args{ + input: strings.NewReader("R2, L3"), + }, + want: "5", + wantErr: false, + }, + { + name: "", + fields: fields{ + name: "", + }, + args: args{ + input: strings.NewReader("R2, R2, R2"), + }, + want: "2", + wantErr: false, + }, + { + name: "", + fields: fields{ + name: "", + }, + args: args{ + input: strings.NewReader("R5, L5, R5, R3"), + }, + want: "12", + wantErr: false, + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + s := solution{ + name: tt.fields.name, + year: year, + } + + 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) { + type fields struct { + name string + } + + type args struct { + input io.Reader + } + + tests := []struct { + name string + fields fields + args args + want string + wantErr bool + }{ + { + name: "", + fields: fields{ + name: "", + }, + args: args{ + input: strings.NewReader("R8, R4, R4, R8"), + }, + want: "4", + wantErr: false, + }, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + s := solution{ + name: tt.fields.name, + } + + 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) + }) + } +} diff --git a/internal/puzzles/solutions/2016/day01/spec.md b/internal/puzzles/solutions/2016/day01/spec.md new file mode 100644 index 00000000..8bc64227 --- /dev/null +++ b/internal/puzzles/solutions/2016/day01/spec.md @@ -0,0 +1,39 @@ +# --- Day 1: No Time for a Taxicab --- + +## Part One + +Santa's sleigh uses a very high-precision clock to guide its movements, and the clock's oscillator +is regulated by stars. Unfortunately, the stars have been stolen... by the Easter Bunny. +To save Christmas, Santa needs you to retrieve all fifty stars by 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're airdropped near Easter Bunny Headquarters in a city somewhere. +"Near", unfortunately, is as close as you can get - the instructions on the Easter Bunny Recruiting +Document the Elves intercepted start here, and nobody had time to work them out further. + +The Document indicates that you should start at the given coordinates (where you just landed) and face North. +Then, follow the provided sequence: either turn left `(L)` or right `(R)` 90 degrees, +then walk forward the given number of blocks, ending at a new intersection. + +There's no time to follow such ridiculous instructions on foot, though, +so you take a moment and work out the destination. Given that you can only walk on the street grid of the city, +how far is the shortest path to the destination? + +For example: + +- Following `R2, L3` leaves you `2` blocks East and `3` blocks North, or `5` blocks away. +- `R2, R2, R2` leaves you `2` blocks due South of your starting position, which is `2` blocks away. +- `R5, L5, R5, R3` leaves you `12` blocks away. +How many blocks away is Easter Bunny HQ? + +## Part Two + +Then, you notice the instructions continue on the back of the Recruiting Document. Easter Bunny HQ is actually at the first +location you visit twice. + +For example, if your instructions are `R8, R4, R4, R8`, the first location you visit twice is `4` blocks away, due `East`. + +How many blocks away is the first location you visit twice? \ No newline at end of file diff --git a/internal/puzzles/solutions/register.go b/internal/puzzles/solutions/register.go index cc9c3d98..f2f7cc0e 100644 --- a/internal/puzzles/solutions/register.go +++ b/internal/puzzles/solutions/register.go @@ -4,7 +4,9 @@ import ( // 2015 solutions. // register day01 solution. _ "github.com/obalunenko/advent-of-code/internal/puzzles/solutions/2015/day01" - + // 2016 solutions. + // register day01 solution. + _ "github.com/obalunenko/advent-of-code/internal/puzzles/solutions/2016/day01" // 2019 solutions. // register day01 solution. _ "github.com/obalunenko/advent-of-code/internal/puzzles/solutions/2019/day01" @@ -14,7 +16,6 @@ import ( _ "github.com/obalunenko/advent-of-code/internal/puzzles/solutions/2019/day03" // register day04 solution. _ "github.com/obalunenko/advent-of-code/internal/puzzles/solutions/2019/day04" - // 2020 solutions. // register day01 solution. _ "github.com/obalunenko/advent-of-code/internal/puzzles/solutions/2020/day01" diff --git a/scripts/coverage.sh b/scripts/coverage.sh index e6a7b773..80b5e8a5 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -1,25 +1,28 @@ -#!/usr/bin/env bash +#!/bin/sh -set -Eeuo pipefail +set -eu -function cleanup() { - trap - SIGINT SIGTERM ERR EXIT - echo "cleanup running" - rm -rf coverage.out.tmp -} +SCRIPT_NAME="$(basename "$0")" -trap cleanup SIGINT SIGTERM ERR EXIT +echo "${SCRIPT_NAME} is running... " -SCRIPT_NAME="$(basename "$(test -L "$0" && readlink "$0" || echo "$0")")" +rm -rf coverage +mkdir -p coverage -echo "${SCRIPT_NAME} is running... " +# go test --count=1 -tags=integration_test -coverprofile ./coverage/integration.cov -covermode=atomic ./... +go test --count=1 -coverprofile ./coverage/unit.cov -covermode=atomic ./... + + +{ +echo "mode: atomic" +tail -q -n +2 ./coverage/*.cov +} >> ./coverage/full.cov +gocov convert ./coverage/full.cov > ./coverage/full.json +gocov report ./coverage/full.json +gocov-html ./coverage/full.json >./coverage/full.html +# open ./coverage/full.html -go test -race -coverpkg=./... -coverprofile coverage.out.tmp ./... +# go tool cover -html=./coverage/full.cov -# shellcheck disable=SC2002 -cat coverage.out.tmp | grep -v "cmd/" >coverage.out -gocov convert coverage.out >coverage.out.json -gocov report coverage.out.json -gocov-html coverage.out.json > coverage.out.html -go tool cover -html=coverage.out +echo "${SCRIPT_NAME} done." diff --git a/sonar-project.properties b/sonar-project.properties index c4eadac9..6ef8db72 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -15,5 +15,5 @@ sonar.exclusions=**/*_test.go,**/vendor/** sonar.tests=. sonar.test.inclusions=**/*_test.go sonar.go.tests.reportPaths=tests.out -sonar.go.coverage.reportPaths=coverage.out +sonar.go.coverage.reportPaths=./coverage/full.cov sonar.go.golangci-lint.reportPaths=linters.out \ No newline at end of file