From d69fb2899ed81855fd9ea8a695a5bb0affe67686 Mon Sep 17 00:00:00 2001 From: Oleg Balunenko Date: Fri, 1 Dec 2023 20:30:51 +0400 Subject: [PATCH] feat: Implemented solution for puzzle 2023/day01 (#284) * feat: Add constant for 2023 year * feat: Add puzzle description and skeleton * feat: Register puzzle 2023/day01 * feat: Add regression tests for 2023 year puzzles * chore: Add workflow configuration for 2023 * feat: Implement part 1 * feat: Implement part 2 * chore: Add regression tests * refactor: Reduce cyclo complexity * refactor: Remove redundant if * refactor: Simplify code --- .github/workflows/readme-stars.yml | 14 + README.md | 2 + .../go-tools-docker-compose.yml | 7 + internal/puzzles/constants.go | 1 + .../puzzles/solutions/2023/day01/solution.go | 171 ++++++++ .../solutions/2023/day01/solution_test.go | 115 ++++++ internal/puzzles/solutions/2023/day01/spec.md | 64 +++ internal/puzzles/solutions/register_2023.go | 9 + internal/puzzles/year_string.go | 7 +- tests/regression_2023_test.go | 364 ++++++++++++++++++ tests/regression_test.go | 1 + 11 files changed, 752 insertions(+), 3 deletions(-) create mode 100644 internal/puzzles/solutions/2023/day01/solution.go create mode 100644 internal/puzzles/solutions/2023/day01/solution_test.go create mode 100644 internal/puzzles/solutions/2023/day01/spec.md create mode 100644 internal/puzzles/solutions/register_2023.go create mode 100644 tests/regression_2023_test.go diff --git a/.github/workflows/readme-stars.yml b/.github/workflows/readme-stars.yml index aa47370b..29ea711d 100644 --- a/.github/workflows/readme-stars.yml +++ b/.github/workflows/readme-stars.yml @@ -24,6 +24,20 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Update 2023 year + uses: k2bd/advent-readme-stars@v1.0.3 + env: + YEAR: 2023 + with: + userId: ${{env.USER_ID}} + leaderboardId: ${{env.BOARD_ID}} + sessionCookie: ${{env.SESSION}} + readmeLocation: ${{env.README}} + headerPrefix: ${{env.HEADER_PFX}} + year: ${{env.YEAR}} + tableMarker: + starSymbol: ${{env.STAR_SYMBOL}} + - name: Update 2022 year uses: k2bd/advent-readme-stars@v1.0.3 env: diff --git a/README.md b/README.md index afd83b12..95f63ee1 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ This repository contains solutions for puzzles and cli tool to run solutions to ## Implemented solutions + + ### 2022 Results diff --git a/deployments/docker-compose/go-tools-docker-compose.yml b/deployments/docker-compose/go-tools-docker-compose.yml index f5456302..f0e7e08e 100755 --- a/deployments/docker-compose/go-tools-docker-compose.yml +++ b/deployments/docker-compose/go-tools-docker-compose.yml @@ -30,6 +30,13 @@ services: service: tools entrypoint: /bin/sh -c 'git config --global --add safe.directory /app && ./scripts/tests/run.sh' + run-tests-regression: + extends: + service: tools + entrypoint: /bin/sh -c 'git config --global --add safe.directory /app && ./scripts/tests/run-regression.sh' + environment: + AOC_SESSION: ${AOC_SESSION} + run-tests-coverage: extends: service: tools diff --git a/internal/puzzles/constants.go b/internal/puzzles/constants.go index 2affe5e3..f9e9f02e 100644 --- a/internal/puzzles/constants.go +++ b/internal/puzzles/constants.go @@ -53,6 +53,7 @@ const ( Year2020 // 2020 Year2021 // 2021 Year2022 // 2022 + Year2023 // 2023 yearSentinel ) diff --git a/internal/puzzles/solutions/2023/day01/solution.go b/internal/puzzles/solutions/2023/day01/solution.go new file mode 100644 index 00000000..52405f94 --- /dev/null +++ b/internal/puzzles/solutions/2023/day01/solution.go @@ -0,0 +1,171 @@ +// Package day01 contains solution for https://adventofcode.com/2023/day/1 puzzle. +package day01 + +import ( + "bufio" + "fmt" + "io" + "strconv" + "strings" + "unicode" + + "github.com/obalunenko/advent-of-code/internal/puzzles" +) + +func init() { + puzzles.Register(solution{}) +} + +type solution struct{} + +func (s solution) Year() string { + return puzzles.Year2023.String() +} + +func (s solution) Day() string { + return puzzles.Day01.String() +} + +func (s solution) Part1(input io.Reader) (string, error) { + sum, err := calibrate(input, nil) + if err != nil { + return "", fmt.Errorf("calibrating: %w", err) + } + + return strconv.Itoa(sum), nil +} + +func (s solution) Part2(input io.Reader) (string, error) { + sum, err := calibrate(input, digitsDict) + if err != nil { + return "", fmt.Errorf("calibrating: %w", err) + } + + return strconv.Itoa(sum), nil +} + +const ( + one = "one" + two = "two" + three = "three" + four = "four" + five = "five" + six = "six" + seven = "seven" + eight = "eight" + nine = "nine" +) + +var digitsDict = map[string]int{ + one: 1, + two: 2, + three: 3, + four: 4, + five: 5, + six: 6, + seven: 7, + eight: 8, + nine: 9, +} + +func calibrate(input io.Reader, dictionary map[string]int) (int, error) { + scanner := bufio.NewScanner(input) + + var values []int + + for scanner.Scan() { + line := scanner.Text() + + value, err := extractNumberFromLine(line, dictionary) + if err != nil { + return 0, fmt.Errorf("extracting number from line %q: %w", line, err) + } + + values = append(values, value) + } + + if err := scanner.Err(); err != nil { + return 0, fmt.Errorf("reading input: %w", err) + } + + var sum int + + for _, v := range values { + sum += v + } + + return sum, nil +} + +func extractNumberFromLine(line string, dict map[string]int) (int, error) { + first, last := -1, -1 + + var word string + + for _, c := range line { + var ( + d int + ok bool + err error + ) + + switch { + case unicode.IsDigit(c): + word = " " + + d, err = strconv.Atoi(string(c)) + if err != nil { + return 0, fmt.Errorf("failed to convert %q to int: %w", string(c), err) + } + + ok = true + case unicode.IsLetter(c): + word += string(c) + + d, ok = getDigitFromWord(word, dict) + default: + word = "" + } + + if !ok { + continue + } + + word = word[len(word)-1:] + + if first == -1 { + first = d + } else { + last = d + } + } + + if last == -1 { + last = first + } + + value, err := strconv.Atoi(strconv.Itoa(first) + strconv.Itoa(last)) + if err != nil { + return 0, fmt.Errorf("failed to convert %d%d to int: %w", first, last, err) + } + + return value, nil +} + +func getDigitFromWord(word string, dict map[string]int) (int, bool) { + if word == "" { + return -1, false + } + + if len(dict) == 0 { + return -1, false + } + + for s, i := range digitsDict { + if strings.Contains(word, s) { + return i, true + } + } + + return -1, false +} diff --git a/internal/puzzles/solutions/2023/day01/solution_test.go b/internal/puzzles/solutions/2023/day01/solution_test.go new file mode 100644 index 00000000..ed0f2b65 --- /dev/null +++ b/internal/puzzles/solutions/2023/day01/solution_test.go @@ -0,0 +1,115 @@ +package day01 + +import ( + "errors" + "io" + "strings" + "testing" + "testing/iotest" + + "github.com/stretchr/testify/assert" +) + +func Test_solution_Year(t *testing.T) { + var s solution + + want := "2023" + got := s.Year() + + assert.Equal(t, want, got) +} + +func Test_solution_Day(t *testing.T) { + var s solution + + want := "1" + 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: strings.NewReader("1abc2\npqr3stu8vwx\na1b2c3d4e5f\ntreb7uchet"), + }, + want: "142", + 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: strings.NewReader("two1nine\neightwothree\nabcone2threexyz\nxtwone3four\n4nineeightseven2\nzoneight234\n7pqrstsixteen"), + }, + want: "281", + 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/2023/day01/spec.md b/internal/puzzles/solutions/2023/day01/spec.md new file mode 100644 index 00000000..3b9d8a47 --- /dev/null +++ b/internal/puzzles/solutions/2023/day01/spec.md @@ -0,0 +1,64 @@ +# --- Day 1: Trebuchet?! --- + +## --- Part One --- + +Something is wrong with global snow production, and you've been selected to take a look. +The Elves have even given you a map; on it, they've used stars to mark the top fifty locations that are likely +to be having problems. + +You've been doing this long enough to know that to restore snow operations, you need to check 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 try to ask why they can't just use a weather machine ("not powerful enough") and where they're even +sending you ("the sky") and why your map looks mostly blank ("you sure ask a lot of questions") and hang on did you +just say the sky ("of course, where do you think snow comes from") when you realize that the Elves are already loading +you into a trebuchet ("please hold still, we need to strap you in"). + +As they're making the final adjustments, they discover that their calibration document (your puzzle input) has been +amended by a very young Elf who was apparently just excited to show off her art skills. Consequently, +the Elves are having trouble reading the values on the document. + +The newly-improved calibration document consists of lines of text; each line originally contained a specific +calibration value that the Elves now need to recover. On each line, the calibration value can be found by combining +the first digit and the last digit (in that order) to form a single two-digit number. + +For example: + +```text +1abc2 +pqr3stu8vwx +a1b2c3d4e5f +treb7uchet +``` + +In this example, the calibration values of these four lines are `12`, `38`, `15`, and `77`. +Adding these together produces `142`. + +Consider your entire calibration document. What is the sum of all of the calibration values? + +## --- Part Two --- + +Your calculation isn't quite right. It looks like some of the digits are actually spelled out with letters: +`one`, `two`, `three`, `four`, `five`, `six`, `seven`, `eight`, and `nine` also count as valid "digits". + +Equipped with this new information, you now need to find the real first and last digit on each line. + +For example: + +```text +two1nine +eightwothree +abcone2threexyz +xtwone3four +4nineeightseven2 +zoneight234 +7pqrstsixteen +``` + +In this example, the calibration values are `29`, `83`, `13`, `24`, `42`, `14`, and `76`. +Adding these together produces `281`. + +What is the sum of all of the calibration values? diff --git a/internal/puzzles/solutions/register_2023.go b/internal/puzzles/solutions/register_2023.go new file mode 100644 index 00000000..8fa0e3d0 --- /dev/null +++ b/internal/puzzles/solutions/register_2023.go @@ -0,0 +1,9 @@ +package solutions + +import ( + /* + 2023 solutions. + */ + // register day01 solution. + _ "github.com/obalunenko/advent-of-code/internal/puzzles/solutions/2023/day01" +) diff --git a/internal/puzzles/year_string.go b/internal/puzzles/year_string.go index 3f8c669d..ed6a37e5 100644 --- a/internal/puzzles/year_string.go +++ b/internal/puzzles/year_string.go @@ -17,12 +17,13 @@ func _() { _ = x[Year2020-6] _ = x[Year2021-7] _ = x[Year2022-8] - _ = x[yearSentinel-9] + _ = x[Year2023-9] + _ = x[yearSentinel-10] } -const _Year_name = "yearUnknown20152016201720182019202020212022yearSentinel" +const _Year_name = "yearUnknown201520162017201820192020202120222023yearSentinel" -var _Year_index = [...]uint8{0, 11, 15, 19, 23, 27, 31, 35, 39, 43, 55} +var _Year_index = [...]uint8{0, 11, 15, 19, 23, 27, 31, 35, 39, 43, 47, 59} func (i Year) String() string { if i < 0 || i >= Year(len(_Year_index)-1) { diff --git a/tests/regression_2023_test.go b/tests/regression_2023_test.go new file mode 100644 index 00000000..7cd5b5c5 --- /dev/null +++ b/tests/regression_2023_test.go @@ -0,0 +1,364 @@ +package tests_test + +import ( + "testing" + + "github.com/obalunenko/advent-of-code/internal/puzzles" +) + +func testcases2023(tb testing.TB) []testcase { + year := puzzles.Year2023 + + return []testcase{ + { + name: tcName(tb, year, puzzles.Day01), + args: args{ + year: year.String(), + name: puzzles.Day01.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day01.String(), + Part1: "54605", + Part2: "55429", + }, + wantErr: false, + }, + { + name: tcName(tb, year, puzzles.Day02), + args: args{ + year: year.String(), + name: puzzles.Day02.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day02.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day03), + args: args{ + year: year.String(), + name: puzzles.Day03.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day03.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day04), + args: args{ + year: year.String(), + name: puzzles.Day04.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day04.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day05), + args: args{ + year: year.String(), + name: puzzles.Day05.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day05.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day06), + args: args{ + year: year.String(), + name: puzzles.Day06.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day06.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day07), + args: args{ + year: year.String(), + name: puzzles.Day07.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day07.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day08), + args: args{ + year: year.String(), + name: puzzles.Day08.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day08.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day09), + args: args{ + year: year.String(), + name: puzzles.Day09.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day09.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day10), + args: args{ + year: year.String(), + name: puzzles.Day10.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day10.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day11), + args: args{ + year: year.String(), + name: puzzles.Day11.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day11.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day12), + args: args{ + year: year.String(), + name: puzzles.Day12.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day12.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day13), + args: args{ + year: year.String(), + name: puzzles.Day13.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day13.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day14), + args: args{ + year: year.String(), + name: puzzles.Day14.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day14.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day15), + args: args{ + year: year.String(), + name: puzzles.Day15.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day15.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day16), + args: args{ + year: year.String(), + name: puzzles.Day16.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day16.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day17), + args: args{ + year: year.String(), + name: puzzles.Day17.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day17.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day18), + args: args{ + year: year.String(), + name: puzzles.Day18.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day18.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day19), + args: args{ + year: year.String(), + name: puzzles.Day19.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day19.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day20), + args: args{ + year: year.String(), + name: puzzles.Day20.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day20.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day21), + args: args{ + year: year.String(), + name: puzzles.Day21.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day21.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day22), + args: args{ + year: year.String(), + name: puzzles.Day22.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day22.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day23), + args: args{ + year: year.String(), + name: puzzles.Day23.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day23.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day24), + args: args{ + year: year.String(), + name: puzzles.Day24.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day24.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + { + name: tcName(tb, year, puzzles.Day25), + args: args{ + year: year.String(), + name: puzzles.Day25.String(), + }, + want: puzzles.Result{ + Year: year.String(), + Name: puzzles.Day25.String(), + Part1: "", + Part2: "", + }, + wantErr: true, + }, + } +} diff --git a/tests/regression_test.go b/tests/regression_test.go index db3068e5..dc025d71 100644 --- a/tests/regression_test.go +++ b/tests/regression_test.go @@ -54,6 +54,7 @@ func TestRun(t *testing.T) { tests = append(tests, testcases2020(t)...) tests = append(tests, testcases2021(t)...) tests = append(tests, testcases2022(t)...) + tests = append(tests, testcases2023(t)...) for i := range tests { tt := tests[i]