diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index b27f349af..68d23b78f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -34,8 +34,8 @@ jobs: # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://git.io/codeql-language-support language: [ 'go' ] - os: [ 'ubuntu-22.04' ] - go-version: [1.21] + os: [ 'ubuntu-24.04' ] + go-version: [1.24] steps: - name: Checkout uses: actions/checkout@v4 @@ -43,7 +43,7 @@ jobs: fetch-depth: 0 - name: Set up go - uses: actions/setup-go@v4.1.0 + uses: actions/setup-go@v5.4.0 with: go-version: ${{ matrix.go-version }} - run: | @@ -52,7 +52,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -65,4 +65,4 @@ jobs: make build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/dockerfiles.yml b/.github/workflows/dockerfiles.yml index 70008dc36..03920a63d 100644 --- a/.github/workflows/dockerfiles.yml +++ b/.github/workflows/dockerfiles.yml @@ -24,7 +24,7 @@ jobs: [ "./build/docker/go-tools/Dockerfile", ] - runs-on: "ubuntu-22.04" + runs-on: "ubuntu-24.04" name: Lint ${{ matrix.dockerfile }} steps: - name: Checkout diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 6fbc8b304..992403ec4 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -40,8 +40,8 @@ jobs: fail-fast: false max-parallel: 3 matrix: - os: [ 'ubuntu-22.04' ] - go-version: [1.21] + os: [ 'ubuntu-24.04' ] + go-version: [1.24] runs-on: ${{ matrix.os }} name: Build steps: @@ -51,7 +51,7 @@ jobs: fetch-depth: 0 - name: Set up go - uses: actions/setup-go@v4.1.0 + uses: actions/setup-go@v5.4.0 with: go-version: ${{ matrix.go-version }} @@ -60,7 +60,7 @@ jobs: shell: bash - name: Set up cache - uses: actions/cache@v3 + uses: actions/cache@v4 env: cache-name: cache-vendored-tools-${{ secrets.CACHE_VERSION }} with: @@ -83,7 +83,7 @@ jobs: shell: bash - name: Install GoReleaser - uses: goreleaser/goreleaser-action@v5 + uses: goreleaser/goreleaser-action@v6 with: install-only: true @@ -97,8 +97,8 @@ jobs: fail-fast: false max-parallel: 2 matrix: - os: [ 'ubuntu-22.04' ] - go-version: [1.21] + os: [ 'ubuntu-24.04' ] + go-version: [1.24] runs-on: ${{ matrix.os }} name: Run Tests steps: @@ -108,7 +108,7 @@ jobs: fetch-depth: 0 - name: Set up go - uses: actions/setup-go@v4.1.0 + uses: actions/setup-go@v5.4.0 with: go-version: ${{ matrix.go-version }} @@ -117,7 +117,7 @@ jobs: shell: bash - name: Set up cache - uses: actions/cache@v3 + uses: actions/cache@v4 env: cache-name: cache-vendored-tools-${{ secrets.CACHE_VERSION }} with: @@ -144,8 +144,8 @@ jobs: fail-fast: false max-parallel: 2 matrix: - os: [ 'ubuntu-22.04' ] - go-version: [1.21] + os: [ 'ubuntu-24.04' ] + go-version: [1.24] runs-on: ${{ matrix.os }} name: Run linters steps: @@ -155,7 +155,7 @@ jobs: fetch-depth: 0 - name: Set up go - uses: actions/setup-go@v4.1.0 + uses: actions/setup-go@v5.4.0 with: go-version: ${{ matrix.go-version }} - run: | @@ -173,7 +173,7 @@ jobs: shell: bash - name: Set up cache - uses: actions/cache@v3 + uses: actions/cache@v4 env: cache-name: cache-vendored-tools-${{ secrets.CACHE_VERSION }} with: @@ -201,8 +201,8 @@ jobs: fail-fast: true max-parallel: 1 matrix: - os: [ 'ubuntu-22.04' ] - go-version: [1.21] + os: [ 'ubuntu-24.04' ] + go-version: [1.24] runs-on: ${{ matrix.os }} name: Quality reports steps: @@ -212,7 +212,7 @@ jobs: fetch-depth: 0 - name: Set up go - uses: actions/setup-go@v4.1.0 + uses: actions/setup-go@v5.4.0 with: go-version: ${{ matrix.go-version }} - run: | @@ -220,7 +220,7 @@ jobs: shell: bash - name: Set up cache - uses: actions/cache@v3 + uses: actions/cache@v4 env: cache-name: cache-vendored-tools-${{ secrets.CACHE_VERSION }} with: @@ -254,7 +254,7 @@ jobs: shell: bash - name: SonarCloud report upload - uses: sonarsource/sonarcloud-github-action@v1.9 + uses: sonarsource/sonarcloud-github-action@v5.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/readme-stars.yml b/.github/workflows/readme-stars.yml index 29ea711d1..2da9ee120 100644 --- a/.github/workflows/readme-stars.yml +++ b/.github/workflows/readme-stars.yml @@ -11,7 +11,7 @@ on: jobs: update-readme: - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 env: USER_ID: ${{ secrets.AOC_USER_ID }} BOARD_ID: ${{ secrets.AOC_BOARD_ID}} @@ -24,6 +24,20 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Update 2024 year + uses: k2bd/advent-readme-stars@v1.0.3 + env: + YEAR: 2024 + 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 2023 year uses: k2bd/advent-readme-stars@v1.0.3 env: @@ -151,6 +165,6 @@ jobs: starSymbol: ${{env.STAR_SYMBOL}} - name: Commit changes - uses: stefanzweifel/git-auto-commit-action@v5.0.0 + uses: stefanzweifel/git-auto-commit-action@v6.0.1 with: commit_message: Update README stars. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8e560d8f0..08d8e121d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,8 +13,8 @@ jobs: fail-fast: false max-parallel: 2 matrix: - os: [ 'ubuntu-22.04' ] - go-version: [1.21] + os: [ 'ubuntu-24.04' ] + go-version: [1.24] runs-on: ${{ matrix.os }} name: Build steps: @@ -24,7 +24,7 @@ jobs: fetch-depth: 0 - name: Set up go - uses: actions/setup-go@v4.1.0 + uses: actions/setup-go@v5.4.0 with: go-version: ${{ matrix.go-version }} @@ -33,7 +33,7 @@ jobs: shell: bash - name: Set up cache - uses: actions/cache@v3 + uses: actions/cache@v4 env: cache-name: cache-vendored-tools-${{ secrets.CACHE_VERSION }} with: @@ -59,8 +59,8 @@ jobs: fail-fast: false max-parallel: 2 matrix: - os: [ 'ubuntu-22.04' ] - go-version: [1.21] + os: [ 'ubuntu-24.04' ] + go-version: [1.24] runs-on: ${{ matrix.os }} name: Regression tests steps: @@ -70,7 +70,7 @@ jobs: fetch-depth: 0 - name: Set up go - uses: actions/setup-go@v4.1.0 + uses: actions/setup-go@v5.4.0 with: go-version: ${{ matrix.go-version }} @@ -79,7 +79,7 @@ jobs: shell: bash - name: Set up cache - uses: actions/cache@v3 + uses: actions/cache@v4 env: cache-name: cache-vendored-tools-${{ secrets.CACHE_VERSION }} with: @@ -108,8 +108,8 @@ jobs: fail-fast: false max-parallel: 1 matrix: - os: [ 'ubuntu-22.04' ] - go-version: [1.21] + os: [ 'ubuntu-24.04' ] + go-version: [1.24] runs-on: ${{ matrix.os }} name: Lint steps: @@ -119,7 +119,7 @@ jobs: fetch-depth: 0 - name: Set up go - uses: actions/setup-go@v4.1.0 + uses: actions/setup-go@v5.4.0 with: go-version: ${{ matrix.go-version }} @@ -128,7 +128,7 @@ jobs: shell: bash - name: Set up cache - uses: actions/cache@v3 + uses: actions/cache@v4 env: cache-name: cache-vendored-tools-${{ secrets.CACHE_VERSION }} with: @@ -161,8 +161,8 @@ jobs: fail-fast: false max-parallel: 1 matrix: - os: [ 'ubuntu-22.04' ] - go-version: [1.21] + os: [ 'ubuntu-24.04' ] + go-version: [1.24] runs-on: ${{ matrix.os }} name: Release steps: @@ -172,7 +172,7 @@ jobs: fetch-depth: 0 - name: Set up go - uses: actions/setup-go@v4.1.0 + uses: actions/setup-go@v5.4.0 with: go-version: ${{ matrix.go-version }} @@ -181,7 +181,7 @@ jobs: shell: bash - name: Install GoReleaser - uses: goreleaser/goreleaser-action@v5 + uses: goreleaser/goreleaser-action@v6 with: install-only: true diff --git a/.gitignore b/.gitignore index a14e03758..553b22be0 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,5 @@ bin/ dist/ .DS_Store coverage/ -tests-report.json \ No newline at end of file +tests-report.json +linters-report.xml diff --git a/.golangci.pipe.yml b/.golangci.pipe.yml index b368f9100..7fb453978 100644 --- a/.golangci.pipe.yml +++ b/.golangci.pipe.yml @@ -28,6 +28,10 @@ linters-settings: arguments: [ [ "call-chain", "loop", "method-call", "recover", "return" ] ] issues: + exclude-files: + - \.pb\.go$ + exclude-dirs: + - vendor exclude-use-default: false exclude: # for "public interface + private struct implementation" cases only! @@ -97,16 +101,16 @@ issues: run: issues-exit-code: 1 tests: true - skip-dirs: - - vendor/ - skip-files: - - \.pb\.go$ # output configuration options output: # colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions # default is "colored-line-number" - format: github-actions + formats: + - format: colored-line-number + path: stderr + - format: checkstyle + path: linters-report.xml # print lines of code with issue, default is true print-issued-lines: true diff --git a/.golangci.yml b/.golangci.yml index d9d3dbfe8..337846568 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -12,7 +12,33 @@ linters-settings: lines: 100 statements: 50 gci: - local-prefixes: github.com/obalunenko/advent-of-code + # Section configuration to compare against. + # Section names are case-insensitive and may contain parameters in (). + # The default order of sections is `standard > default > custom > blank > dot > alias > localmodule`, + # If `custom-order` is `true`, it follows the order of `sections` option. + # Default: ["standard", "default"] + sections: + - standard # Standard section: captures all standard packages. + - default # Default section: contains all imports that could not be matched to another section type. + - prefix(github.com/obalunenko/advent-of-code) # Custom section: groups all imports with the specified Prefix. + - blank # Blank section: contains all blank imports. This section is not present unless explicitly enabled. + - dot # Dot section: contains all dot imports. This section is not present unless explicitly enabled. + - alias # Alias section: contains all alias imports. This section is not present unless explicitly enabled. + - localmodule # Local module section: contains all local packages. This section is not present unless explicitly enabled. + + # Skip generated files. + # Default: true + skip-generated: false + + # Enable custom order of sections. + # If `true`, make the section order the same as the order of `sections`. + # Default: false + custom-order: true + + # Drops lexical ordering for custom sections. + # Default: false + no-lex-order: true + goconst: min-len: 2 min-occurrences: 2 @@ -33,13 +59,128 @@ linters-settings: min-complexity: 15 goimports: local-prefixes: github.com/obalunenko/advent-of-code - gomnd: - settings: - mnd: - # don't include the "operation" and "assign" - checks: [ argument,case,condition,return ] + mnd: + # List of enabled checks, see https://github.com/tommy-muehle/go-mnd/#checks for description. + # Default: ["argument", "case", "condition", "operation", "return", "assign"] + checks: + - argument + - case + - condition + - operation + - return + - assign + # List of numbers to exclude from analysis. + # The numbers should be written as string. + # Values always ignored: "1", "1.0", "0" and "0.0" + # Default: [] + ignored-numbers: [] + # List of file patterns to exclude from analysis. + # Values always ignored: `.+_test.go` + # Default: [] + ignored-files: [] + # List of function patterns to exclude from analysis. + # Following functions are always ignored: `time.Date`, + # `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`, + # `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`. + # Default: [] + ignored-functions: [] + govet: - check-shadowing: true + # Disable all analyzers. + # Default: false + disable-all: false + # Enable analyzers by name. + # (in addition to default: + # appends, asmdecl, assign, atomic, bools, buildtag, cgocall, composites, copylocks, defers, directive, errorsas, + # framepointer, httpresponse, ifaceassert, loopclosure, lostcancel, nilfunc, printf, shift, sigchanyzer, slog, + # stdmethods, stringintconv, structtag, testinggoroutine, tests, timeformat, unmarshal, unreachable, unsafeptr, + # unusedresult + # ). + # Run `GL_DEBUG=govet golangci-lint run --enable=govet` to see default, all available analyzers, and enabled analyzers. + # Default: [] + disable: + - fieldalignment + enable: + # Check for missing values after append. + - appends + # Report mismatches between assembly files and Go declarations. + - asmdecl + # Check for useless assignments. + - assign + # Check for common mistakes using the sync/atomic package. + - atomic + # Check for non-64-bits-aligned arguments to sync/atomic functions. + - atomicalign + # Check for common mistakes involving boolean operators. + - bools + # Check //go:build and // +build directives. + - buildtag + # Detect some violations of the cgo pointer passing rules. + - cgocall + # Check for unkeyed composite literals. + - composites + # Check for locks erroneously passed by value. + - copylocks + # Check for calls of reflect.DeepEqual on error values. + - deepequalerrors + # Report common mistakes in defer statements. + - defers + # Check Go toolchain directives such as //go:debug. + - directive + # Report passing non-pointer or non-error values to errors.As. + - errorsas + # Find calls to a particular function. + - findcall + # Report assembly that clobbers the frame pointer before saving it. + - framepointer + # Check for mistakes using HTTP responses. + - httpresponse + # Detect impossible interface-to-interface type assertions. + - ifaceassert + # Check references to loop variables from within nested functions. + - loopclosure + # Check cancel func returned by context.WithCancel is called. + - lostcancel + # Check for useless comparisons between functions and nil. + - nilfunc + # Check for redundant or impossible nil comparisons. + - nilness + # Check consistency of Printf format strings and arguments. + - printf + # Check for comparing reflect.Value values with == or reflect.DeepEqual. + - reflectvaluecompare + # Check for possible unintended shadowing of variables. + - shadow + # Check for shifts that equal or exceed the width of the integer. + - shift + # Check for unbuffered channel of os.Signal. + - sigchanyzer + # Check for invalid structured logging calls. + - slog + # Check the argument type of sort.Slice. + - sortslice + # Check signature of methods of well-known interfaces. + - stdmethods + # Check for string(int) conversions. + - stringintconv + # Check that struct field tags conform to reflect.StructTag.Get. + - structtag + # Report calls to (*testing.T).Fatal from goroutines started by a test. + - testinggoroutine + # Check for common mistaken usages of tests and examples. + - tests + # Check for calls of (time.Time).Format or time.Parse with 2006-02-01. + - timeformat + # Report passing non-pointer or non-interface values to unmarshal. + - unmarshal + # Check for unreachable code. + - unreachable + # Check for invalid conversions of uintptr to unsafe.Pointer. + - unsafeptr + # Check for unused results of calls to some functions. + - unusedresult + # Checks for unused writes. + - unusedwrite lll: line-length: 140 misspell: @@ -58,7 +199,7 @@ linters: - dogsled - dupl - errcheck - - exportloopref + - copyloopvar - exhaustive - funlen - gochecknoinits @@ -67,7 +208,7 @@ linters: - gocyclo - gofmt - goimports - - gomnd + - mnd - goprintffuncname - gosec - gosimple @@ -104,6 +245,19 @@ linters: # - testpackage issues: + # Show only new issues: if there are unstaged changes or untracked files, + # only those changes are analyzed, else only changes in HEAD~ are analyzed. + # It's a super-useful option for integration of golangci-lint into existing + # large codebase. It's not practical to fix all existing issues at the moment + # of integration: much better don't allow issues in new code. + # Default is false. + new: false + # Fix found issues (if it's supported by the linter) + fix: false + exclude-files: + - \.pb\.go$ + exclude-dirs: + - vendor/ exclude-use-default: false exclude: # for "public interface + private struct implementation" cases only! @@ -137,55 +291,77 @@ issues: linters: - gochecknoinits - # Show only new issues: if there are unstaged changes or untracked files, - # only those changes are analyzed, else only changes in HEAD~ are analyzed. - # It's a super-useful option for integration of golangci-lint into existing - # large codebase. It's not practical to fix all existing issues at the moment - # of integration: much better don't allow issues in new code. - # Default is false. - new: true - - # Fix found issues (if it's supported by the linter) - fix: false - - severity: - # Default value is empty string. - # Set the default severity for issues. If severity rules are defined and the issues - # do not match or no severity is provided to the rule this will be the default - # severity applied. Severities should match the supported severity names of the - # selected out format. - # - Code climate: https://docs.codeclimate.com/docs/issues#issue-severity - # - Checkstyle: https://checkstyle.sourceforge.io/property_types.html#severity - # - GitHub: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message - default-severity: error - - # The default value is false. - # If set to true severity-rules regular expressions become case sensitive. - case-sensitive: false - - # Default value is empty list. - # When a list of severity rules are provided, severity information will be added to lint - # issues. Severity rules have the same filtering capability as exclude rules except you - # are allowed to specify one matcher per severity rule. - # Only affects out formats that support setting severity information. - rules: - - linters: - - dupl - severity: warning - run: issues-exit-code: 0 tests: true - skip-dirs: - - vendor/ - skip-files: - - \.pb\.go$ + +severity: + # Set the default severity for issues. + # + # If severity rules are defined and the issues do not match or no severity is provided to the rule + # this will be the default severity applied. + # Severities should match the supported severity names of the selected out format. + # - Code climate: https://docs.codeclimate.com/docs/issues#issue-severity + # - Checkstyle: https://checkstyle.sourceforge.io/property_types.html#SeverityLevel + # - GitHub: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message + # - TeamCity: https://www.jetbrains.com/help/teamcity/service-messages.html#Inspection+Instance + # + # `@linter` can be used as severity value to keep the severity from linters (e.g. revive, gosec, ...) + # + # Default: "" + default-severity: error + + # If set to true `severity-rules` regular expressions become case-sensitive. + # Default: false + case-sensitive: true + + # When a list of severity rules are provided, severity information will be added to lint issues. + # Severity rules have the same filtering capability as exclude rules + # except you are allowed to specify one matcher per severity rule. + # + # `@linter` can be used as severity value to keep the severity from linters (e.g. revive, gosec, ...) + # + # Only affects out formats that support setting severity information. + # + # Default: [] + rules: + - linters: + - dupl + severity: info + # output configuration options output: # colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions # default is "colored-line-number" - format: checkstyle + formats: + - format: colored-line-number + path: stderr + - format: checkstyle + path: linters-report.xml + + + # Order to use when sorting results. + # Require `sort-results` to `true`. + # Possible values: `file`, `linter`, and `severity`. + # + # If the severity values are inside the following list, they are ordered in this order: + # 1. error + # 2. warning + # 3. high + # 4. medium + # 5. low + # Either they are sorted alphabetically. + # + # Default: ["file"] + sort-order: + - linter + - severity + - file # filepath, line, and column. + + # Show statistics per linter. + # Default: false + show-stats: true # print lines of code with issue, default is true print-issued-lines: true diff --git a/.goreleaser.yml b/.goreleaser.yml index 20736d779..c751385bc 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,3 +1,5 @@ +version: 2 + release: github: owner: obalunenko @@ -28,11 +30,7 @@ builds: - 'windows' goarch: - 'amd64' - - 'arm' - 'arm64' - ignore: - - goos: darwin - goarch: 'arm' mod_timestamp: '{{ .CommitTimestamp }}' env: @@ -40,9 +38,7 @@ builds: main: ./cmd/aoc-cli flags: - -trimpath - ldflags: - - "{{ .Env.GO_BUILD_LDFLAGS }}" - - + universal_binaries: - # ID of the source build # @@ -66,11 +62,13 @@ archives: - id: cli builds: - cli - format: tar.gz + formats: + - tar.gz wrap_in_directory: true format_overrides: - goos: windows - format: zip + formats: + - zip name_template: "{{ .Binary }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" files: - LICENSE @@ -79,7 +77,7 @@ checksum: name_template: "{{ .ProjectName }}-{{ .Version }}-checksums.txt" snapshot: - name_template: SNAPSHOT-{{ .Commit }} + version_template: SNAPSHOT-{{ .Commit }} changelog: sort: asc @@ -109,4 +107,4 @@ changelog: order: 999 milestones: - - close: true + - close: true \ No newline at end of file diff --git a/Makefile b/Makefile index 63b61bbf0..e400793f5 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,16 @@ SHELL := env VERSION=$(VERSION) $(SHELL) VERSION ?= $(shell git describe --tags $(git rev-list --tags --max-count=1)) -GOVERSION:=1.21 +GOVERSION:=1.24 APP_NAME?=aoc-cli SHELL := env APP_NAME=$(APP_NAME) $(SHELL) -GOTOOLS_IMAGE_TAG?=v0.12.1 SHELL := env GOTOOLS_IMAGE_TAG=$(GOTOOLS_IMAGE_TAG) $(SHELL) +AOC_PUZZLE_URL= +SHELL := env AOC_PUZZLE_URL=$(AOC_PUZZLE_URL) $(SHELL) + COMPOSE_TOOLS_FILE=deployments/docker-compose/go-tools-docker-compose.yml COMPOSE_TOOLS_CMD_BASE=docker compose -f $(COMPOSE_TOOLS_FILE) COMPOSE_TOOLS_CMD_UP=$(COMPOSE_TOOLS_CMD_BASE) up --exit-code-from @@ -157,5 +159,9 @@ new-version: vet test-regression build open-advent-homepage: ./scripts/browser-opener.sh -u 'https://adventofcode.com/' +gen-boilerplate: + ./scripts/codegen/puzzle-boilerplate.sh +.PHONY: gen-boilerplate + .DEFAULT_GOAL := help diff --git a/README.md b/README.md index ca3bd5dd0..4127ec07a 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,14 @@ This repository contains solutions for puzzles and cli tool to run solutions to ## Implemented solutions + +### 2024 Results + +| Day | Part 1 | Part 2 | +| :---: | :---: | :---: | +| [Day 1](https://adventofcode.com/2024/day/1) | ⭐ | ⭐ | + + ### 2023 Results diff --git a/build/docker/go-tools/Dockerfile b/build/docker/go-tools/Dockerfile index c7abfa182..cfebec54f 100644 --- a/build/docker/go-tools/Dockerfile +++ b/build/docker/go-tools/Dockerfile @@ -1,3 +1,3 @@ -FROM ghcr.io/obalunenko/go-tools:v1.3.2 AS builder +FROM ghcr.io/obalunenko/go-tools:v1.7.6 AS builder CMD ["/bin/sh", "-c", ""] diff --git a/cmd/aoc-cli/handlers.go b/cmd/aoc-cli/handlers.go index bcea700a0..2e1fd6dac 100644 --- a/cmd/aoc-cli/handlers.go +++ b/cmd/aoc-cli/handlers.go @@ -21,7 +21,7 @@ import ( ) func onExit(_ context.Context) cli.AfterFunc { - return func(c *cli.Context) error { + return func(_ *cli.Context) error { fmt.Println("Exit...") return nil diff --git a/deployments/docker-compose/go-tools-docker-compose.yml b/deployments/docker-compose/go-tools-docker-compose.yml index f0e7e08e3..76fd61ec9 100755 --- a/deployments/docker-compose/go-tools-docker-compose.yml +++ b/deployments/docker-compose/go-tools-docker-compose.yml @@ -1,5 +1,3 @@ -version: '3' - services: tools: build: @@ -29,6 +27,8 @@ services: extends: service: tools entrypoint: /bin/sh -c 'git config --global --add safe.directory /app && ./scripts/tests/run.sh' + environment: + AOC_PUZZLE_URL: ${AOC_PUZZLE_URL} run-tests-regression: extends: diff --git a/go.mod b/go.mod index 6eab13e07..3ac828d2a 100644 --- a/go.mod +++ b/go.mod @@ -1,29 +1,31 @@ module github.com/obalunenko/advent-of-code -go 1.21 +go 1.24 + +toolchain go1.24.0 require ( - github.com/briandowns/spinner v1.23.0 + github.com/briandowns/spinner v1.23.2 github.com/manifoldco/promptui v0.9.0 - github.com/obalunenko/getenv v1.12.1 - github.com/obalunenko/logger v1.0.1 - github.com/obalunenko/version v1.1.0 - github.com/savioxavier/termlink v1.3.0 - github.com/stretchr/testify v1.8.4 - github.com/urfave/cli/v2 v2.25.7 + github.com/obalunenko/getenv v1.14.1 + github.com/obalunenko/logger v1.2.0 + github.com/obalunenko/version v1.3.1 + github.com/savioxavier/termlink v1.4.2 + github.com/stretchr/testify v1.10.0 + github.com/urfave/cli/v2 v2.27.6 ) require ( github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.13.0 // indirect github.com/mattn/go-colorable v0.1.9 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect - golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect + golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect golang.org/x/sys v0.6.0 // indirect golang.org/x/term v0.6.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 23979e6d3..48686fcb2 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,13 @@ -github.com/briandowns/spinner v1.23.0 h1:alDF2guRWqa/FOZZYWjlMIx2L6H0wyewPxo/CH4Pt2A= -github.com/briandowns/spinner v1.23.0/go.mod h1:rPG4gmXeN3wQV/TsAY4w8lPdIM6RX3yqeBQJSrbXjuE= +github.com/briandowns/spinner v1.23.2 h1:Zc6ecUnI+YzLmJniCfDNaMbW0Wid1d5+qcTq4L2FW8w= +github.com/briandowns/spinner v1.23.2/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= @@ -19,26 +19,26 @@ github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/obalunenko/getenv v1.12.1 h1:16dfwnXa5AhZuScaT4qAxUapwpRBxTAdKmsHwc/aDvQ= -github.com/obalunenko/getenv v1.12.1/go.mod h1:GtEy59H7644JXfVjxDFo1jEUe1yJ4asSyPwN8D19Pvs= -github.com/obalunenko/logger v1.0.1 h1:XERBQry7q7Kxr6lDeh9RLsUr7oZj+T5J845B4RPxORE= -github.com/obalunenko/logger v1.0.1/go.mod h1:BLGJLvxm4ImsjuGVOjrx59VUsbsLR2f7IkVPFjLyBvg= -github.com/obalunenko/version v1.1.0 h1:yVua7OHnK3+MJpendeMmAlfzVmq7R1h8MO3Ufz7HEec= -github.com/obalunenko/version v1.1.0/go.mod h1:Or267aCQxNcAtgOeWA7yOe/RqJS4XDaMfcFwk3ohbOg= +github.com/obalunenko/getenv v1.14.1 h1:nFwG6PMKWKNBWnvlCbIOLgYdj/sSerbLZ9k3Is26HOU= +github.com/obalunenko/getenv v1.14.1/go.mod h1:O+JIpb//raW/4Mx6sno6lHckuFn7zbagod0KkyDHUX0= +github.com/obalunenko/logger v1.2.0 h1:MwsqJWtaxaHFQK7Cjkqk1NnlNPHH+tR1ergdnpST7Kg= +github.com/obalunenko/logger v1.2.0/go.mod h1:XaU3GhUJWda3ow3hhRjlItpIVgQRKa2KDsEeprSzBvg= +github.com/obalunenko/version v1.3.1 h1:NN+YSOrti8mEyJSnu+7//YSvGrOhLivh60hJXhIrNTI= +github.com/obalunenko/version v1.3.1/go.mod h1:56ydLXefFem3sEJ2iyguuZ7dwJ25VoIRTev/JfxFIa4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/savioxavier/termlink v1.3.0 h1:3Gl4FzQjUyiHzmoEDfmWEhgIwDiJY4poOQHP+k8ReA4= -github.com/savioxavier/termlink v1.3.0/go.mod h1:5T5ePUlWbxCHIwyF8/Ez1qufOoGM89RCg9NvG+3G3gc= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= -github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= -golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819 h1:EDuYyU/MkFXllv9QF9819VlI9a4tzGuCbhG0ExK9o1U= -golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +github.com/savioxavier/termlink v1.4.2 h1:PRlvcStluuSKA87KCIqzODknYeQ3XEcgJP6DvAAVl1c= +github.com/savioxavier/termlink v1.4.2/go.mod h1:5T5ePUlWbxCHIwyF8/Ez1qufOoGM89RCg9NvG+3G3gc= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= +github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4= +golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/command/command_test.go b/internal/command/command_test.go index 4a9959da1..bdebb60c9 100644 --- a/internal/command/command_test.go +++ b/internal/command/command_test.go @@ -34,10 +34,10 @@ func newMockHTTPClient(p returnParams) *mockHTTPClient { return &http.Response{ Status: http.StatusText(p.status), StatusCode: p.status, - Proto: "HTTP/1.0", - ProtoMajor: 1, - ProtoMinor: 0, - Header: nil, + Proto: req.Proto, + ProtoMajor: req.ProtoMajor, + ProtoMinor: req.ProtoMinor, + Header: req.Header, Body: p.body, ContentLength: 0, TransferEncoding: nil, diff --git a/internal/puzzles/common/intcomputer/intcoumputer_test.go b/internal/puzzles/common/intcomputer/intcoumputer_test.go index 6480ed388..3e9ecf9f7 100644 --- a/internal/puzzles/common/intcomputer/intcoumputer_test.go +++ b/internal/puzzles/common/intcomputer/intcoumputer_test.go @@ -47,8 +47,6 @@ func Test_New(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got, err := New(tt.args.input) if tt.wantErr { @@ -116,8 +114,6 @@ func Test_computer_add(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { err := tt.c.add(tt.args.aPos, tt.args.bPos, tt.args.resPos) require.NoError(t, err) @@ -163,8 +159,6 @@ func Test_computer_mult(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { err := tt.c.mult(tt.args.aPos, tt.args.bPos, tt.args.resPos) require.NoError(t, err) @@ -195,8 +189,6 @@ func Test_computer_Execute(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { comp := tt.c got, err := comp.Execute() @@ -233,8 +225,6 @@ func Test_computer_Input(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { comp := tt.c comp.Input(tt.args.noun, tt.args.verb) diff --git a/internal/puzzles/constants.go b/internal/puzzles/constants.go index f9e9f02ef..c298a57dc 100644 --- a/internal/puzzles/constants.go +++ b/internal/puzzles/constants.go @@ -54,6 +54,7 @@ const ( Year2021 // 2021 Year2022 // 2022 Year2023 // 2023 + Year2024 // 2024 yearSentinel ) diff --git a/internal/puzzles/input/content_test.go b/internal/puzzles/input/content_test.go index c16f58f0e..a9cd2e076 100644 --- a/internal/puzzles/input/content_test.go +++ b/internal/puzzles/input/content_test.go @@ -34,10 +34,10 @@ func newMockHTTPClient(p returnParams) *mockHTTPClient { return &http.Response{ Status: http.StatusText(p.status), StatusCode: p.status, - Proto: "HTTP/1.0", + Proto: req.Proto, ProtoMajor: 1, - ProtoMinor: 0, - Header: nil, + ProtoMinor: req.ProtoMinor, + Header: req.Header, Body: p.body, ContentLength: 0, TransferEncoding: nil, @@ -173,7 +173,7 @@ func TestGet(t *testing.T) { name: "", client: client{ IHTTPClient: &mockHTTPClient{ - MockDo: func(req *http.Request) (*http.Response, error) { + MockDo: func(_ *http.Request) (*http.Response, error) { return &http.Response{}, errors.New("error in test") }, }, diff --git a/internal/puzzles/name_test.go b/internal/puzzles/name_test.go index d4e057c85..cd554e6cc 100644 --- a/internal/puzzles/name_test.go +++ b/internal/puzzles/name_test.go @@ -49,8 +49,6 @@ func TestMakeName(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got, err := MakeName(tt.args.year, tt.args.puzzle) if tt.wantErr { @@ -58,7 +56,9 @@ func TestMakeName(t *testing.T) { return } + assert.NoError(t, err) + assert.Equal(t, tt.want, got) }) } diff --git a/internal/puzzles/result.go b/internal/puzzles/result.go index 8cb4143db..da457de3c 100644 --- a/internal/puzzles/result.go +++ b/internal/puzzles/result.go @@ -96,15 +96,13 @@ func printTable(w io.Writer, table [][]string) error { case 0: _, err = fmt.Fprintf(w, "\n") case 1: - _, err = fmt.Fprintf(writer, "\t"+strings.Join(line, "\t")+"\t\n") + _, err = fmt.Fprintf(writer, "\t%s", strings.Join(line, "\t")+"\t\n") default: - _, err = fmt.Fprintf(writer, "\t "+strings.Join(line, "\t")+"\t\n") + _, err = fmt.Fprintf(writer, "\t %s", strings.Join(line, "\t")+"\t\n") } if err != nil { - if err != nil { - return fmt.Errorf("fprintln: %w", err) - } + return fmt.Errorf("fprintln: %w", err) } } diff --git a/internal/puzzles/solutions/2015/day01/solution_test.go b/internal/puzzles/solutions/2015/day01/solution_test.go index c33d19e5c..038ac55d1 100644 --- a/internal/puzzles/solutions/2015/day01/solution_test.go +++ b/internal/puzzles/solutions/2015/day01/solution_test.go @@ -124,8 +124,6 @@ func Test_solution_Part1(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got, err := s.Part1(tt.args.input) if tt.wantErr { @@ -180,8 +178,6 @@ func Test_solution_Part2(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got, err := s.Part2(tt.args.input) if tt.wantErr { diff --git a/internal/puzzles/solutions/2015/day02/solution_test.go b/internal/puzzles/solutions/2015/day02/solution_test.go index 2949470b4..0793b9cd0 100644 --- a/internal/puzzles/solutions/2015/day02/solution_test.go +++ b/internal/puzzles/solutions/2015/day02/solution_test.go @@ -68,8 +68,6 @@ func Test_solution_Part1(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got, err := s.Part1(tt.args.input) if tt.wantErr { @@ -124,8 +122,6 @@ func Test_solution_Part2(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got, err := s.Part2(tt.args.input) if tt.wantErr { diff --git a/internal/puzzles/solutions/2016/day01/solution_test.go b/internal/puzzles/solutions/2016/day01/solution_test.go index 1c2d949d4..ead14d373 100644 --- a/internal/puzzles/solutions/2016/day01/solution_test.go +++ b/internal/puzzles/solutions/2016/day01/solution_test.go @@ -76,8 +76,6 @@ func Test_solution_Part1(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got, err := s.Part1(tt.args.input) if tt.wantErr { @@ -124,8 +122,6 @@ func Test_solution_Part2(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got, err := s.Part2(tt.args.input) if tt.wantErr { diff --git a/internal/puzzles/solutions/2016/day02/solution.go b/internal/puzzles/solutions/2016/day02/solution.go index a5b0e916a..7dccf6bc7 100644 --- a/internal/puzzles/solutions/2016/day02/solution.go +++ b/internal/puzzles/solutions/2016/day02/solution.go @@ -119,7 +119,7 @@ let's predict that this is a 2 dimension matrix and '5' is 0,2m func loadKeypadPart2() keypad { start := keypadPos{ x: 0, - y: 2, + y: 2, //nolint:mnd // False positive. } g := [][]string{ diff --git a/internal/puzzles/solutions/2017/day01/solution.go b/internal/puzzles/solutions/2017/day01/solution.go index 22761772a..2ac9c01b3 100644 --- a/internal/puzzles/solutions/2017/day01/solution.go +++ b/internal/puzzles/solutions/2017/day01/solution.go @@ -54,7 +54,7 @@ func part2(in io.Reader) (string, error) { return "", fmt.Errorf("make list: %w", err) } - shift := len(list) / 2 + shift := len(list) / 2 //nolint:mnd // False positive. itr := newIterator(list, shift, true) diff --git a/internal/puzzles/solutions/2017/day01/solution_test.go b/internal/puzzles/solutions/2017/day01/solution_test.go index 99057eddf..33dfe23c1 100644 --- a/internal/puzzles/solutions/2017/day01/solution_test.go +++ b/internal/puzzles/solutions/2017/day01/solution_test.go @@ -85,8 +85,6 @@ func Test_solution_Part1(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got, err := s.Part1(tt.args.input) if tt.wantErr { @@ -174,8 +172,6 @@ func Test_solution_Part2(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got, err := s.Part2(tt.args.input) if tt.wantErr { diff --git a/internal/puzzles/solutions/2017/day02/solution.go b/internal/puzzles/solutions/2017/day02/solution.go index 0d8118f90..a418f6d03 100644 --- a/internal/puzzles/solutions/2017/day02/solution.go +++ b/internal/puzzles/solutions/2017/day02/solution.go @@ -28,7 +28,7 @@ func (s solution) Year() string { func (s solution) Part1(input io.Reader) (string, error) { var f checksumFunc = func(row []string) (int, error) { - var min, max int + var minVal, maxVal int for i, number := range row { d, err := strconv.Atoi(number) @@ -37,19 +37,19 @@ func (s solution) Part1(input io.Reader) (string, error) { } if i == 0 { - min, max = d, d + minVal, maxVal = d, d } - if d < min { - min = d + if d < minVal { + minVal = d } - if d > max { - max = d + if d > maxVal { + maxVal = d } } - return max - min, nil + return maxVal - minVal, nil } return findChecksum(input, f) diff --git a/internal/puzzles/solutions/2018/day01/solution_test.go b/internal/puzzles/solutions/2018/day01/solution_test.go index 407b86c30..39876c26e 100644 --- a/internal/puzzles/solutions/2018/day01/solution_test.go +++ b/internal/puzzles/solutions/2018/day01/solution_test.go @@ -84,8 +84,6 @@ func Test_solution_Part1(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got, err := s.Part1(tt.args.input) if tt.wantErr { @@ -164,8 +162,6 @@ func Test_solution_Part2(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got, err := s.Part2(tt.args.input) if tt.wantErr { diff --git a/internal/puzzles/solutions/2019/day01/solution.go b/internal/puzzles/solutions/2019/day01/solution.go index 285dbbb58..d57840512 100644 --- a/internal/puzzles/solutions/2019/day01/solution.go +++ b/internal/puzzles/solutions/2019/day01/solution.go @@ -81,6 +81,7 @@ func calc(input io.Reader, calcFn calcFunc) (string, error) { in <- module{ mass: mass, } + lines++ } diff --git a/internal/puzzles/solutions/2019/day01/solution_test.go b/internal/puzzles/solutions/2019/day01/solution_test.go index e9c43f7c8..e199eaed5 100644 --- a/internal/puzzles/solutions/2019/day01/solution_test.go +++ b/internal/puzzles/solutions/2019/day01/solution_test.go @@ -68,8 +68,6 @@ func Test_module_fuel(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { m := module{ mass: tt.fields.mass, @@ -116,8 +114,6 @@ func Test_calcPart1(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { in <- module{mass: tt.input} @@ -175,8 +171,6 @@ func Test_calcPart2(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { in <- module{mass: tt.input} @@ -232,8 +226,6 @@ func Test_calc(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { input := utils.ReaderFromFile(t, tt.args.inputPath) got, err := calc(input, tt.args.calcFn) diff --git a/internal/puzzles/solutions/2019/day02/solution_test.go b/internal/puzzles/solutions/2019/day02/solution_test.go index 3a4d36191..e330dc4db 100644 --- a/internal/puzzles/solutions/2019/day02/solution_test.go +++ b/internal/puzzles/solutions/2019/day02/solution_test.go @@ -50,8 +50,6 @@ func Test_nounVerb(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got := nounVerb(tt.args.noun, tt.args.verb) assert.Equal(t, tt.want, got) @@ -91,8 +89,6 @@ func Test_solution_Part1(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got, err := s.Part1(tt.args.input) if tt.wantErr { diff --git a/internal/puzzles/solutions/2019/day03/solution.go b/internal/puzzles/solutions/2019/day03/solution.go index 15f0d02f3..22d6f9b5e 100644 --- a/internal/puzzles/solutions/2019/day03/solution.go +++ b/internal/puzzles/solutions/2019/day03/solution.go @@ -185,8 +185,6 @@ func findCross(wm1, wm2 map[pos]int) []pos { res := make([]pos, 0, len(wm1)) for p := range wm1 { - p := p - if _, exist := wm2[p]; exist { res = append(res, p) } diff --git a/internal/puzzles/solutions/2019/day03/solution_test.go b/internal/puzzles/solutions/2019/day03/solution_test.go index 9acf1c85c..2f4b253c5 100644 --- a/internal/puzzles/solutions/2019/day03/solution_test.go +++ b/internal/puzzles/solutions/2019/day03/solution_test.go @@ -79,8 +79,6 @@ func Test_solution_Part1(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got, err := s.Part1(tt.args.input) if tt.wantErr { @@ -131,8 +129,6 @@ func Test_findCross(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got := findCross(tt.args.wm1, tt.args.wm2) assert.ElementsMatch(t, tt.want, got) @@ -182,8 +178,6 @@ func Test_solution_Part2(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got, err := s.Part2(tt.args.input) if tt.wantErr { diff --git a/internal/puzzles/solutions/2019/day04/solution.go b/internal/puzzles/solutions/2019/day04/solution.go index ded564a62..ecf341e10 100644 --- a/internal/puzzles/solutions/2019/day04/solution.go +++ b/internal/puzzles/solutions/2019/day04/solution.go @@ -96,15 +96,13 @@ func isIncreasing(n int) bool { func hasRepeated(n int) bool { nmbs := intToSlice(n) - var hasRepeated bool - for i := 1; i <= len(nmbs)-1; i++ { if nmbs[i] == nmbs[i-1] { - hasRepeated = true + return true } } - return hasRepeated + return false } func hasRepeatedWithDouble(n int) bool { @@ -143,6 +141,7 @@ func isPasswordPart2(n int) bool { return isIncreasing(n) && hasRepeatedWithDouble(n) } +//nolint:mnd // False positive. func intToSlice(n int) [6]int { return [6]int{ (n % 1000000) / 100000, diff --git a/internal/puzzles/solutions/2019/day04/solution_test.go b/internal/puzzles/solutions/2019/day04/solution_test.go index 73572e06a..55d290868 100644 --- a/internal/puzzles/solutions/2019/day04/solution_test.go +++ b/internal/puzzles/solutions/2019/day04/solution_test.go @@ -60,8 +60,6 @@ func Test_solution_Part1(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got, err := s.Part1(tt.args.input) if tt.wantErr { @@ -100,8 +98,6 @@ func Test_findPasswords(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got, err := findPasswords(tt.args.low, tt.args.high, isPasswordPart1) if tt.wantErr { @@ -150,8 +146,6 @@ func Test_isIncreasing(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got := isIncreasing(tt.args.n) assert.Equal(t, tt.want, got) @@ -179,8 +173,6 @@ func Test_intToSlice(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got := intToSlice(tt.args.n) assert.Equal(t, tt.want, got) @@ -215,8 +207,6 @@ func Test_hasRepeated(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got := hasRepeated(tt.args.n) assert.Equal(t, tt.want, got) @@ -258,8 +248,6 @@ func Test_isPassword(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got := isPasswordPart1(tt.args.n) assert.Equal(t, tt.want, got) @@ -302,8 +290,6 @@ func Test_isPasswordPart2(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got := isPasswordPart2(tt.args.n) assert.Equal(t, tt.want, got) @@ -351,8 +337,6 @@ func Test_solution_Part2(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got, err := s.Part2(tt.args.input) if tt.wantErr { diff --git a/internal/puzzles/solutions/2020/day01/solution.go b/internal/puzzles/solutions/2020/day01/solution.go index 479ba44fe..5bcff8d5b 100644 --- a/internal/puzzles/solutions/2020/day01/solution.go +++ b/internal/puzzles/solutions/2020/day01/solution.go @@ -93,6 +93,7 @@ func (s solution) Part2(input io.Reader) (string, error) { return strconv.Itoa(res), nil } +//nolint:mnd // False positive due to magic numbers. func foundEntries(expensereport []int) (a, b, c int, err error) { const ( dest = 2020 diff --git a/internal/puzzles/solutions/2020/day01/solution_test.go b/internal/puzzles/solutions/2020/day01/solution_test.go index 7358c047b..ca86e681d 100644 --- a/internal/puzzles/solutions/2020/day01/solution_test.go +++ b/internal/puzzles/solutions/2020/day01/solution_test.go @@ -60,8 +60,6 @@ func Test_solution_Part1(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got, err := s.Part1(tt.args.input) if tt.wantErr { @@ -108,8 +106,6 @@ func Test_solution_Part2(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got, err := s.Part2(tt.args.input) if tt.wantErr { diff --git a/internal/puzzles/solutions/2020/day02/solution_test.go b/internal/puzzles/solutions/2020/day02/solution_test.go index 595506b42..5c1db6304 100644 --- a/internal/puzzles/solutions/2020/day02/solution_test.go +++ b/internal/puzzles/solutions/2020/day02/solution_test.go @@ -60,8 +60,6 @@ func Test_solution_Part1(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got, err := s.Part1(tt.args.input) if tt.wantErr { @@ -108,8 +106,6 @@ func Test_solution_Part2(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got, err := s.Part2(tt.args.input) if tt.wantErr { diff --git a/internal/puzzles/solutions/2021/day03/solution_test.go b/internal/puzzles/solutions/2021/day03/solution_test.go index 310a18731..160d18e71 100644 --- a/internal/puzzles/solutions/2021/day03/solution_test.go +++ b/internal/puzzles/solutions/2021/day03/solution_test.go @@ -185,6 +185,7 @@ func Test_bitrates_consumption(t *testing.T) { first: tt.fields.gamma, second: tt.fields.epsilon, } + got, err := b.consumption() if !tt.wantErr(t, err, "consumption()") { return diff --git a/internal/puzzles/solutions/2021/day04/solution.go b/internal/puzzles/solutions/2021/day04/solution.go index 06ff77a0c..7dbbb0512 100644 --- a/internal/puzzles/solutions/2021/day04/solution.go +++ b/internal/puzzles/solutions/2021/day04/solution.go @@ -60,7 +60,7 @@ func (s solution) Part2(input io.Reader) (string, error) { func rule(boardsNum int) winRule { var count int - return func(w winner) bool { + return func(_ winner) bool { count++ return count == boardsNum diff --git a/internal/puzzles/solutions/2021/day05/solution_test.go b/internal/puzzles/solutions/2021/day05/solution_test.go index 1dc56ce29..0c6bf15a5 100644 --- a/internal/puzzles/solutions/2021/day05/solution_test.go +++ b/internal/puzzles/solutions/2021/day05/solution_test.go @@ -207,6 +207,7 @@ func Test_parseCoordinates(t *testing.T) { if !tt.wantErr(t, err, fmt.Sprintf("parseCoordinates(%v)", tt.args.s)) { return } + assert.Equalf(t, tt.want, got, "parseCoordinates(%v)", tt.args.s) }) } @@ -247,6 +248,7 @@ func Test_getLines(t *testing.T) { if !tt.wantErr(t, err, fmt.Sprintf("getLines(%v)", tt.args.input)) { return } + assert.Equalf(t, tt.want, got, "getLines(%v)", tt.args.input) }) } diff --git a/internal/puzzles/solutions/2021/day07/solution.go b/internal/puzzles/solutions/2021/day07/solution.go index f1f28f16e..b1faed8a5 100644 --- a/internal/puzzles/solutions/2021/day07/solution.go +++ b/internal/puzzles/solutions/2021/day07/solution.go @@ -77,19 +77,19 @@ func makeMatrix(crabs []int) [][]int { cnum := len(crabs) - max := crabs[cnum-1] + maxC := crabs[cnum-1] matrix := make([][]int, cnum+header) // matrix[i][j]. - // i - crabs; j - all positions from 0 to max + // i - crabs; j - all positions from 0 to maxC for i := 0; i < cnum+header; i++ { - matrix[i] = make([]int, max+header+1) + matrix[i] = make([]int, maxC+header+1) if i == 0 { matrix[i][0] = undef - for j := 1; j <= max+header; j++ { + for j := 1; j <= maxC+header; j++ { matrix[i][j] = j - 1 } @@ -136,6 +136,7 @@ func part1Cost(p int) int { return p } +//nolint:mnd // Formula is not a magic number. func part2Cost(p int) int { // formula a_{n}=a_{1}+(n-1)d an := 1 + 1*(p-1) @@ -164,7 +165,7 @@ func (s swarm) minDistanceCost() int { } func minDistanceCost(matrix [][]int) int { - var min int + var minC int ilen := len(matrix) jlen := len(matrix[0]) @@ -177,13 +178,13 @@ func minDistanceCost(matrix [][]int) int { } if j == 1 { - min = f + minC = f } - if f < min { - min = f + if f < minC { + minC = f } } - return min + return minC } diff --git a/internal/puzzles/solutions/2022/day01/solution.go b/internal/puzzles/solutions/2022/day01/solution.go index 20ae4fcec..e14bb6771 100644 --- a/internal/puzzles/solutions/2022/day01/solution.go +++ b/internal/puzzles/solutions/2022/day01/solution.go @@ -130,23 +130,23 @@ func (e elves) String() string { } func (e elves) maxTotalCalories() int { - var max int + var maxC int for _, el := range e { - if max == 0 { - max = el.totalCalories() + if maxC == 0 { + maxC = el.totalCalories() continue } c := el.totalCalories() - if c > max { - max = c + if c > maxC { + maxC = c } } - return max + return maxC } func (e elves) backupSnackCalc() int { diff --git a/internal/puzzles/solutions/2023/day01/solution.go b/internal/puzzles/solutions/2023/day01/solution.go index 52405f940..669e31c0a 100644 --- a/internal/puzzles/solutions/2023/day01/solution.go +++ b/internal/puzzles/solutions/2023/day01/solution.go @@ -56,6 +56,7 @@ const ( nine = "nine" ) +//nolint:mnd // False positive. var digitsDict = map[string]int{ one: 1, two: 2, diff --git a/internal/puzzles/solutions/2024/day01/solution.go b/internal/puzzles/solutions/2024/day01/solution.go new file mode 100755 index 000000000..94c0627cf --- /dev/null +++ b/internal/puzzles/solutions/2024/day01/solution.go @@ -0,0 +1,126 @@ +// Package day01 contains solution for https://adventofcode.com/2024/day/1 puzzle. +package day01 + +import ( + "bufio" + "fmt" + "io" + "slices" + "strconv" + "strings" + + "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.Day01.String() +} + +func (s solution) Part1(input io.Reader) (string, error) { + l, err := parseInput(input) + if err != nil { + return "", fmt.Errorf("failed to parse input: %w", err) + } + + slices.Sort(l.itemsA) + slices.Sort(l.itemsB) + + var sum int + + for i := 0; i < len(l.itemsA); i++ { + d := l.itemsA[i] - l.itemsB[i] + if d < 0 { + d = -d + } + + sum += d + } + + return strconv.Itoa(sum), nil +} + +func (s solution) Part2(input io.Reader) (string, error) { + l, err := parseInput(input) + if err != nil { + return "", fmt.Errorf("failed to parse input: %w", err) + } + + seenA := make(map[int]int) + + for _, a := range l.itemsA { + seenA[a] = 0 + + for _, b := range l.itemsB { + if a == b { + seenA[a]++ + } + } + } + + var sum int + + for _, a := range l.itemsA { + sum += a * seenA[a] + } + + return strconv.Itoa(sum), nil +} + +type lists struct { + itemsA []int + itemsB []int +} + +func parseInput(input io.Reader) (lists, error) { + const ( + listsNum = 2 + listAIdx = 0 + listBIdx = 1 + ) + + l := lists{ + itemsA: make([]int, 0), + itemsB: make([]int, 0), + } + + scanner := bufio.NewScanner(input) + for scanner.Scan() { + line := scanner.Text() + + parts := strings.Split(line, " ") + if len(parts) != listsNum { + return lists{}, fmt.Errorf("invalid input line: %s", line) + } + + // Parse parts[0] and parts[1] to integers and append them to l.itemsA and l.itemsB respectively. + a, err := strconv.Atoi(parts[listAIdx]) + if err != nil { + return lists{}, fmt.Errorf("failed to parse int: %w", err) + } + + b, err := strconv.Atoi(parts[listBIdx]) + if err != nil { + return lists{}, fmt.Errorf("failed to parse int: %w", err) + } + + l.itemsA = append(l.itemsA, a) + + l.itemsB = append(l.itemsB, b) + } + + if scanner.Err() != nil { + return lists{}, fmt.Errorf("scanner error: %w", scanner.Err()) + } + + return l, nil +} diff --git a/internal/puzzles/solutions/2024/day01/solution_test.go b/internal/puzzles/solutions/2024/day01/solution_test.go new file mode 100755 index 000000000..af9659e74 --- /dev/null +++ b/internal/puzzles/solutions/2024/day01/solution_test.go @@ -0,0 +1,117 @@ +package day01 + +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 := "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: utils.ReaderFromFile(t, filepath.Join("testdata", "input.txt")), + }, + want: "11", + 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: "test example from description", + args: args{ + input: utils.ReaderFromFile(t, filepath.Join("testdata", "input.txt")), + }, + want: "31", + 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/day01/spec.md b/internal/puzzles/solutions/2024/day01/spec.md new file mode 100755 index 000000000..2545ebcab --- /dev/null +++ b/internal/puzzles/solutions/2024/day01/spec.md @@ -0,0 +1,104 @@ +# Puzzle https://adventofcode.com/2024/day/1 + +# --- Day 1: Historian Hysteria --- + +## --- Part One --- + +The Chief Historian is always present for the big Christmas sleigh launch, but nobody has seen him in months! +Last anyone heard, he was visiting locations that are historically significant to the North Pole; +a group of Senior Historians has asked you to accompany them as they check the places they think he was most likely to visit. + +As each location is checked, they will mark it on their list with a star. They figure the Chief Historian must be +in one of the first fifty places they'll look, so in order to save Christmas, you need to help them get fifty stars on +their list before Santa takes off on 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 haven't even left yet and the group of Elvish Senior Historians has already hit a problem: their list of locations +to check is currently empty. Eventually, someone decides that the best place to check first would be the +Chief Historian's office. + +Upon pouring into the office, everyone confirms that the Chief Historian is indeed nowhere to be found. +Instead, the Elves discover an assortment of notes and lists of historically significant locations! T +his seems to be the planning the Chief Historian was doing before he left. +Perhaps these notes can be used to determine which locations to search? + +Throughout the Chief's office, the historically significant locations are listed not by name but by a unique number +called the location ID. To make sure they don't miss anything, The Historians split into two groups, +each searching the office and trying to create their own complete list of location IDs. + +There's just one problem: by holding the two lists up side by side (your puzzle input), it quickly becomes clear that +the lists aren't very similar. Maybe you can help The Historians reconcile their lists? + +For example: + +```text +3 4 +4 3 +2 5 +1 3 +3 9 +3 3 +``` + +Maybe the lists are only off by a small amount! To find out, pair up the numbers and measure how far apart they are. +Pair up the smallest number in the left list with the smallest number in the right list, then the second-smallest left +number with the second-smallest right number, and so on. + +Within each pair, figure out how far apart the two numbers are; you'll need to add up all of those distances. +For example, if you pair up a 3 from the left list with a 7 from the right list, the distance apart is 4; if you pair +up a 9 with a 3, the distance apart is 6. + +In the example list above, the pairs and distances would be as follows: + +- The smallest number in the left list is 1, and the smallest number in the right list is 3. The distance between them is 2. +- The second-smallest number in the left list is 2, and the second-smallest number in the right list is another 3. The distance between them is 1. +- The third-smallest number in both lists is 3, so the distance between them is 0. +- The next numbers to pair up are 3 and 4, a distance of 1. +- The fifth-smallest numbers in each list are 3 and 5, a distance of 2. +- Finally, the largest number in the left list is 4, while the largest number in the right list is 9; these are a distance 5 apart. + +To find the total distance between the left list and the right list, add up the distances between all of the pairs you +found. In the example above, this is `2 + 1 + 0 + 1 + 2 + 5`, a total distance of `11`! + +Your actual left and right lists contain many location IDs. What is the total distance between your lists? + +## --- Part Two --- + +Your analysis only confirmed what everyone feared: the two lists of location IDs are indeed very different. + +Or are they? + +The Historians can't agree on which group made the mistakes or how to read most of the Chief's handwriting, +but in the commotion you notice an interesting detail: a lot of location IDs appear in both lists! Maybe the other +numbers aren't location IDs at all but rather misinterpreted handwriting. + +This time, you'll need to figure out exactly how often each number from the left list appears in the right list. +Calculate a total similarity score by adding up each number in the left list after multiplying it by the number of +times that number appears in the right list. + +Here are the same example lists again: + +```text +3 4 +4 3 +2 5 +1 3 +3 9 +3 3 +``` + +For these example lists, here is the process of finding the similarity score: + +- The first number in the left list is 3. It appears in the right list three times, so the similarity score increases by 3 * 3 = 9. +- The second number in the left list is 4. It appears in the right list once, so the similarity score increases by 4 * 1 = 4. +- The third number in the left list is 2. It does not appear in the right list, so the similarity score does not increase (2 * 0 = 0). +- The fourth number, 1, also does not appear in the right list. +- The fifth number, 3, appears in the right list three times; the similarity score increases by 9. +- The last number, 3, appears in the right list three times; the similarity score again increases by 9. + +So, for these example lists, the similarity score at the end of this process is `31` `(9 + 4 + 0 + 0 + 9 + 9)`. + +Once again consider your left and right lists. What is their similarity score? + diff --git a/internal/puzzles/solutions/2024/day01/testdata/input.txt b/internal/puzzles/solutions/2024/day01/testdata/input.txt new file mode 100644 index 000000000..b8af9ad29 --- /dev/null +++ b/internal/puzzles/solutions/2024/day01/testdata/input.txt @@ -0,0 +1,6 @@ +3 4 +4 3 +2 5 +1 3 +3 9 +3 3 diff --git a/internal/puzzles/solutions/new.go b/internal/puzzles/solutions/new.go new file mode 100644 index 000000000..8a927293e --- /dev/null +++ b/internal/puzzles/solutions/new.go @@ -0,0 +1,173 @@ +package solutions + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + "text/template" + + "github.com/obalunenko/advent-of-code/internal/puzzles/solutions/templates" +) + +func createNewFromTemplate(purl string) error { + const ( + perms = os.ModePerm + yearLen = 4 + dayLen = 2 + ) + + pd, err := parsePuzzleURL(purl) + if err != nil { + return fmt.Errorf("parse puzzle url %q: %w", purl, err) + } + + day := strconv.Itoa(pd.day) + if len(day) < dayLen { + day = "0" + day + } + + if len(day) != dayLen { + return fmt.Errorf("invalid day: %s", day) + } + + year := strconv.Itoa(pd.year) + + if len(year) != yearLen { + return fmt.Errorf("invalid year: %s", year) + } + + params := templates.Params{ + Year: year, + Day: pd.day, + DayStr: day, + URL: purl, + Title: "", + DescriptionPartOne: "", + DescriptionPartTwo: "", + } + + path := filepath.Clean(filepath.Join(year, "day"+day)) + + if err = createPuzzleDir(path, perms); err != nil { + return fmt.Errorf("failed to create puzzle dir: %w", err) + } + + testdata := filepath.Clean(filepath.Join(path, "testdata")) + + if err = createTestdata(testdata, perms); err != nil { + return fmt.Errorf("failed to create testdata: %w", err) + } + + tmplsFns := []func() (*template.Template, error){ + templates.SolutionTmpl, templates.SolutionTestTmpl, templates.SpecTmpl, + } + + for _, tmplFn := range tmplsFns { + var tmpl *template.Template + + tmpl, err = tmplFn() + if err != nil { + return fmt.Errorf("failed to get template: %w", err) + } + + if err = createFromTemplate(tmpl, path, perms, params); err != nil { + return fmt.Errorf("failed to create from template: %w", err) + } + } + + return nil +} + +func createFromTemplate(tmpl *template.Template, path string, perms os.FileMode, params templates.Params) error { + fpath := filepath.Clean(filepath.Join(path, tmpl.Name())) + + if isExist(fpath) { + return nil + } + + var content []byte + + content, err := templates.SubstituteTemplate(tmpl, params) + if err != nil { + return fmt.Errorf("failed to substitute template: %w", err) + } + + if err = os.WriteFile(fpath, content, perms); err != nil { + return fmt.Errorf("failed to write file: %w", err) + } + + return nil +} + +func createPuzzleDir(path string, perms os.FileMode) error { + if !isExist(path) { + if err := os.MkdirAll(path, perms); err != nil { + return fmt.Errorf("failed to create dir: %w", err) + } + } + + return nil +} + +func createTestdata(path string, perms os.FileMode) error { + if !isExist(path) { + if err := os.MkdirAll(path, perms); err != nil { + return fmt.Errorf("failed to create dir: %w", err) + } + } + + input := filepath.Clean(filepath.Join(path, "input.txt")) + + if !isExist(input) { + var f *os.File + + f, err := os.Create(input) + if err != nil { + return fmt.Errorf("failed to create file: %w", err) + } + + if err = f.Close(); err != nil { + return fmt.Errorf("failed to close file: %w", err) + } + } + + return nil +} + +func isExist(path string) bool { + stat, err := os.Stat(path) + if err != nil && !os.IsNotExist(err) { + return false + } + + return stat != nil && stat.Name() != "" +} + +type puzzleDate struct { + year int + day int +} + +func parsePuzzleURL(url string) (puzzleDate, error) { + const ( + urlFmt = "https://adventofcode.com/%d/day/%d" + paramsNum = 2 + ) + + var year, day int + + n, err := fmt.Sscanf(url, urlFmt, &year, &day) + if err != nil { + return puzzleDate{}, fmt.Errorf("parse puzzle url: %w", err) + } + + if n != paramsNum { + return puzzleDate{}, fmt.Errorf("invalid puzzle url: %s", url) + } + + return puzzleDate{ + year: year, + day: day, + }, nil +} diff --git a/internal/puzzles/solutions/new_test.go b/internal/puzzles/solutions/new_test.go new file mode 100644 index 000000000..065c92754 --- /dev/null +++ b/internal/puzzles/solutions/new_test.go @@ -0,0 +1,94 @@ +package solutions + +import ( + "errors" + "testing" + + "github.com/obalunenko/getenv" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_createNewFromTemplate(t *testing.T) { + const envName = "AOC_PUZZLE_URL" + + purl, err := getenv.Env[string](envName) + if err != nil { + if errors.Is(err, getenv.ErrNotSet) { + t.Skipf("%s is not set", envName) + } + + t.Fatalf("failed to get environment variable[%s]: %v", envName, err) + } + + require.NoError(t, createNewFromTemplate(purl)) +} + +func Test_parsePuzzleURL(t *testing.T) { + type args struct { + url string + } + + tests := []struct { + name string + args args + wandDate puzzleDate + wantErr assert.ErrorAssertionFunc + }{ + { + name: "valid url", + args: args{ + url: "https://adventofcode.com/2022/day/1", + }, + wandDate: puzzleDate{ + year: 2022, + day: 1, + }, + wantErr: assert.NoError, + }, + { + name: "invalid url", + args: args{ + url: "https://adventofcode.com/2022", + }, + wandDate: puzzleDate{ + year: 0, + day: 0, + }, + wantErr: assert.Error, + }, + { + name: "empty url", + args: args{ + url: "", + }, + wandDate: puzzleDate{ + year: 0, + day: 0, + }, + wantErr: assert.Error, + }, + { + name: "whitespace url", + args: args{ + url: " ", + }, + wandDate: puzzleDate{ + year: 0, + day: 0, + }, + wantErr: assert.Error, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotDate, err := parsePuzzleURL(tt.args.url) + if !tt.wantErr(t, err) { + return + } + + assert.Equal(t, tt.wandDate, gotDate) + }) + } +} diff --git a/internal/puzzles/solutions/register_2024.go b/internal/puzzles/solutions/register_2024.go new file mode 100644 index 000000000..2bf6def2d --- /dev/null +++ b/internal/puzzles/solutions/register_2024.go @@ -0,0 +1,9 @@ +package solutions + +import ( + /* + 2024 solutions. + */ + // register day01 solution. + _ "github.com/obalunenko/advent-of-code/internal/puzzles/solutions/2024/day01" +) diff --git a/internal/puzzles/solutions/templates/embed.go b/internal/puzzles/solutions/templates/embed.go new file mode 100644 index 000000000..41f7571ab --- /dev/null +++ b/internal/puzzles/solutions/templates/embed.go @@ -0,0 +1,71 @@ +// Package templates contains templates for solution.go, solution_test.go and spec.md files. +package templates + +import ( + "bytes" + _ "embed" + "fmt" + "text/template" +) + +var ( + //go:embed solution.go.tmpl + solutionTmpl string + //go:embed solution_test.go.tmpl + solutionTestTmpl string + //go:embed spec.md.tmpl + specTmpl string +) + +// Params contains parameters for templates. +type Params struct { + Year string // e.g. "2023" + Day int // e.g. 2 + DayStr string // e.g. "02" + URL string // e.g. "https://adventofcode.com/2023/day/2" + Title string // For now it's empty. + DescriptionPartOne string // For now it's empty. + DescriptionPartTwo string // For now it's empty. +} + +// SolutionTmpl returns template for solution.go file. +func SolutionTmpl() (*template.Template, error) { + tmpl, err := template.New("solution.go").Parse(solutionTmpl) + if err != nil { + return nil, fmt.Errorf("failed to parse solution template: %w", err) + } + + return tmpl, nil +} + +// SolutionTestTmpl returns template for solution_test.go file. +func SolutionTestTmpl() (*template.Template, error) { + tmpl, err := template.New("solution_test.go").Parse(solutionTestTmpl) + if err != nil { + return nil, fmt.Errorf("failed to parse solution test template: %w", err) + } + + return tmpl, nil +} + +// SpecTmpl returns template for spec.md file. +func SpecTmpl() (*template.Template, error) { + tmpl, err := template.New("spec.md").Parse(specTmpl) + if err != nil { + return nil, fmt.Errorf("failed to parse spec template: %w", err) + } + + return tmpl, nil +} + +// SubstituteTemplate substitutes template with given parameters. +func SubstituteTemplate(tmpl *template.Template, p Params) ([]byte, error) { + var buf bytes.Buffer + + err := tmpl.Execute(&buf, p) + if err != nil { + return nil, fmt.Errorf("failed to execute template: %w", err) + } + + return buf.Bytes(), nil +} diff --git a/internal/puzzles/solutions/templates/solution.go.tmpl b/internal/puzzles/solutions/templates/solution.go.tmpl new file mode 100644 index 000000000..c4b7fce1f --- /dev/null +++ b/internal/puzzles/solutions/templates/solution.go.tmpl @@ -0,0 +1,30 @@ +// Package day{{ .DayStr }} contains solution for {{ .URL }} puzzle. +package day{{ .DayStr }} + +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.Year{{ .Year }}.String() +} + +func (s solution) Day() string { + return puzzles.Day{{ .DayStr }}.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/templates/solution_test.go.tmpl b/internal/puzzles/solutions/templates/solution_test.go.tmpl new file mode 100644 index 000000000..e1fddad17 --- /dev/null +++ b/internal/puzzles/solutions/templates/solution_test.go.tmpl @@ -0,0 +1,117 @@ +package day{{ .DayStr }} + +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 := "{{ .Year }}" + got := s.Year() + + assert.Equal(t, want, got) +} + +func Test_solution_Day(t *testing.T) { + var s solution + + want := "{{ .Day }}" + 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: "8", + 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/templates/spec.md.tmpl b/internal/puzzles/solutions/templates/spec.md.tmpl new file mode 100644 index 000000000..9724dbe58 --- /dev/null +++ b/internal/puzzles/solutions/templates/spec.md.tmpl @@ -0,0 +1,12 @@ +# Puzzle {{ .URL }} + +# --- Day {{ .Day }}: {{ .Title }} --- + +## --- Part One --- + +{{ .DescriptionPartOne }} + +## --- Part Two --- + +{{ .DescriptionPartTwo }} + diff --git a/internal/puzzles/solver_test.go b/internal/puzzles/solver_test.go index 0f9b0db88..a02c9eb12 100644 --- a/internal/puzzles/solver_test.go +++ b/internal/puzzles/solver_test.go @@ -195,8 +195,6 @@ func TestRun(t *testing.T) { } for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { got, err := puzzles.Solve(tt.args.solver, tt.args.input) if tt.wantErr { diff --git a/internal/puzzles/year_string.go b/internal/puzzles/year_string.go index ed6a37e55..8d9cda4fe 100644 --- a/internal/puzzles/year_string.go +++ b/internal/puzzles/year_string.go @@ -18,12 +18,13 @@ func _() { _ = x[Year2021-7] _ = x[Year2022-8] _ = x[Year2023-9] - _ = x[yearSentinel-10] + _ = x[Year2024-10] + _ = x[yearSentinel-11] } -const _Year_name = "yearUnknown201520162017201820192020202120222023yearSentinel" +const _Year_name = "yearUnknown2015201620172018201920202021202220232024yearSentinel" -var _Year_index = [...]uint8{0, 11, 15, 19, 23, 27, 31, 35, 39, 43, 47, 59} +var _Year_index = [...]uint8{0, 11, 15, 19, 23, 27, 31, 35, 39, 43, 47, 51, 63} func (i Year) String() string { if i < 0 || i >= Year(len(_Year_index)-1) { diff --git a/scripts/build/app.sh b/scripts/build/app.sh index 4f62f78ee..2d00d9947 100755 --- a/scripts/build/app.sh +++ b/scripts/build/app.sh @@ -13,45 +13,17 @@ mkdir -p "${BIN_DIR}" echo "${SCRIPT_NAME} is running... " -checkInstalled 'goreleaser' - -goreleaser healthcheck - APP=${APP_NAME} echo "Building ${APP}..." -COMMIT="$(git rev-parse HEAD)" -SHORTCOMMIT="$(git rev-parse --short HEAD)" -DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" -VERSION="$(git tag | sort -V | tail -1)" -GOVERSION="$(go version | awk '{print $3;}')" - -if [ -z "${VERSION}" ] || [ "${VERSION}" = "${SHORTCOMMIT}" ]; then - VERSION="v0.0.0" -fi - -if [[ $(git diff --stat) != '' ]]; then - echo 'dirty' - - COMMIT="${COMMIT}-dirty" - SHORTCOMMIT="${SHORTCOMMIT}-dirty" - VERSION="${VERSION}-dirty" -fi - BIN_OUT="${BIN_DIR}/${APP}" -BUILDINFO_VARS_PKG=github.com/obalunenko/version -export GO_BUILD_LDFLAGS="-s -w \ --X ${BUILDINFO_VARS_PKG}.version=${VERSION} \ --X ${BUILDINFO_VARS_PKG}.commit=${COMMIT} \ --X ${BUILDINFO_VARS_PKG}.shortcommit=${SHORTCOMMIT} \ --X ${BUILDINFO_VARS_PKG}.builddate=${DATE} \ --X ${BUILDINFO_VARS_PKG}.appname=${APP} \ --X ${BUILDINFO_VARS_PKG}.goversion=${GOVERSION}" + +GO_BUILD_PACKAGE="${REPO_ROOT}/cmd/${APP}" rm -rf "${BIN_OUT}" -goreleaser build --skip=validate --clean --single-target --output "${BIN_OUT}" +go build -trimpath -o "${BIN_OUT}" "${GO_BUILD_PACKAGE}" -echo "Binary compiled at ${BIN_OUT}" +echo "Build ${BIN_OUT} success" diff --git a/scripts/codegen/puzzle-boilerplate.sh b/scripts/codegen/puzzle-boilerplate.sh new file mode 100755 index 000000000..c50d1873d --- /dev/null +++ b/scripts/codegen/puzzle-boilerplate.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -Eeuo pipefail + +SCRIPT_NAME="$(basename "$0")" +SCRIPT_DIR="$(dirname "$0")" +REPO_ROOT="$(cd "${SCRIPT_DIR}" && git rev-parse --show-toplevel)" +SCRIPTS_DIR="${REPO_ROOT}/scripts" + +source "${SCRIPTS_DIR}/helpers-source.sh" + +cd ${REPO_ROOT}/internal/puzzles/solutions || exit 1 + +go test -v -run Test_createNewFromTemplate + +echo "${SCRIPT_NAME} done." + diff --git a/scripts/linting/golangci-pipeline.sh b/scripts/linting/golangci-pipeline.sh index 5404b62b4..b0d10cf13 100755 --- a/scripts/linting/golangci-pipeline.sh +++ b/scripts/linting/golangci-pipeline.sh @@ -15,7 +15,7 @@ checkInstalled golangci-lint echo "Linting..." -golangci-lint run --out-format=github-actions --no-config --disable-all -E govet +golangci-lint run --out-format=colored-line-number --no-config --disable-all -E govet golangci-lint run --config .golangci.pipe.yml echo "${SCRIPT_NAME} done." diff --git a/scripts/linting/golangci-sonar.sh b/scripts/linting/golangci-sonar.sh index e756a3ac6..a345acc1f 100755 --- a/scripts/linting/golangci-sonar.sh +++ b/scripts/linting/golangci-sonar.sh @@ -15,6 +15,6 @@ checkInstalled golangci-lint echo "Linting..." -golangci-lint run --config .golangci.yml >linters.out +golangci-lint run --config .golangci.yml echo "${SCRIPT_NAME} done." diff --git a/sonar-project.properties b/sonar-project.properties index 9986baadb..6257e4775 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -28,6 +28,6 @@ sonar.links.issue=https://github.com/obalunenko/advent-of-code/issues # Properties specific to Go # ===================================================== -sonar.go.golangci-lint.reportPaths=linters.out +sonar.go.golangci-lint.reportPaths=linters-report.xml sonar.go.tests.reportPaths=tests-report.json sonar.go.coverage.reportPaths=coverage/full.cov diff --git a/tests/regression_2024_test.go b/tests/regression_2024_test.go new file mode 100644 index 000000000..09341465f --- /dev/null +++ b/tests/regression_2024_test.go @@ -0,0 +1,364 @@ +package tests_test + +import ( + "testing" + + "github.com/obalunenko/advent-of-code/internal/puzzles" +) + +func testcases2024(tb testing.TB) []testcase { + year := puzzles.Year2024 + + 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: "936063", + Part2: "23150395", + }, + 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 dc025d71c..9d3f10887 100644 --- a/tests/regression_test.go +++ b/tests/regression_test.go @@ -55,6 +55,7 @@ func TestRun(t *testing.T) { tests = append(tests, testcases2021(t)...) tests = append(tests, testcases2022(t)...) tests = append(tests, testcases2023(t)...) + tests = append(tests, testcases2024(t)...) for i := range tests { tt := tests[i] diff --git a/vendor/github.com/briandowns/spinner/spinner.go b/vendor/github.com/briandowns/spinner/spinner.go index 97b1a8f74..8c0640802 100644 --- a/vendor/github.com/briandowns/spinner/spinner.go +++ b/vendor/github.com/briandowns/spinner/spinner.go @@ -29,7 +29,6 @@ import ( "unicode/utf8" "github.com/fatih/color" - "github.com/mattn/go-isatty" "golang.org/x/term" ) @@ -476,7 +475,7 @@ func (s *Spinner) erase() { // For each additional lines, go up one line and erase it. eraseCodeString.WriteString("\033[F\033[K") } - fmt.Fprintf(s.Writer, eraseCodeString.String()) + fmt.Fprint(s.Writer, eraseCodeString.String()) s.lastOutputPlain = "" } @@ -502,7 +501,8 @@ func GenerateNumberSequence(length int) []string { // isRunningInTerminal check if the writer file descriptor is a terminal func isRunningInTerminal(s *Spinner) bool { - return isatty.IsTerminal(s.WriterFile.Fd()) + fd := s.WriterFile.Fd() + return term.IsTerminal(int(fd)) } func computeNumberOfLinesNeededToPrintString(linePrinted string) int { diff --git a/vendor/github.com/cpuguy83/go-md2man/v2/md2man/debug.go b/vendor/github.com/cpuguy83/go-md2man/v2/md2man/debug.go new file mode 100644 index 000000000..0ec4b12c7 --- /dev/null +++ b/vendor/github.com/cpuguy83/go-md2man/v2/md2man/debug.go @@ -0,0 +1,62 @@ +package md2man + +import ( + "fmt" + "io" + "os" + "strings" + + "github.com/russross/blackfriday/v2" +) + +func fmtListFlags(flags blackfriday.ListType) string { + knownFlags := []struct { + name string + flag blackfriday.ListType + }{ + {"ListTypeOrdered", blackfriday.ListTypeOrdered}, + {"ListTypeDefinition", blackfriday.ListTypeDefinition}, + {"ListTypeTerm", blackfriday.ListTypeTerm}, + {"ListItemContainsBlock", blackfriday.ListItemContainsBlock}, + {"ListItemBeginningOfList", blackfriday.ListItemBeginningOfList}, + {"ListItemEndOfList", blackfriday.ListItemEndOfList}, + } + + var f []string + for _, kf := range knownFlags { + if flags&kf.flag != 0 { + f = append(f, kf.name) + flags &^= kf.flag + } + } + if flags != 0 { + f = append(f, fmt.Sprintf("Unknown(%#x)", flags)) + } + return strings.Join(f, "|") +} + +type debugDecorator struct { + blackfriday.Renderer +} + +func depth(node *blackfriday.Node) int { + d := 0 + for n := node.Parent; n != nil; n = n.Parent { + d++ + } + return d +} + +func (d *debugDecorator) RenderNode(w io.Writer, node *blackfriday.Node, entering bool) blackfriday.WalkStatus { + fmt.Fprintf(os.Stderr, "%s%s %v %v\n", + strings.Repeat(" ", depth(node)), + map[bool]string{true: "+", false: "-"}[entering], + node, + fmtListFlags(node.ListFlags)) + var b strings.Builder + status := d.Renderer.RenderNode(io.MultiWriter(&b, w), node, entering) + if b.Len() > 0 { + fmt.Fprintf(os.Stderr, ">> %q\n", b.String()) + } + return status +} diff --git a/vendor/github.com/cpuguy83/go-md2man/v2/md2man/md2man.go b/vendor/github.com/cpuguy83/go-md2man/v2/md2man/md2man.go index b48005673..62d91b77d 100644 --- a/vendor/github.com/cpuguy83/go-md2man/v2/md2man/md2man.go +++ b/vendor/github.com/cpuguy83/go-md2man/v2/md2man/md2man.go @@ -1,14 +1,23 @@ package md2man import ( + "os" + "strconv" + "github.com/russross/blackfriday/v2" ) // Render converts a markdown document into a roff formatted document. func Render(doc []byte) []byte { renderer := NewRoffRenderer() + var r blackfriday.Renderer = renderer + if v, _ := strconv.ParseBool(os.Getenv("MD2MAN_DEBUG")); v { + r = &debugDecorator{Renderer: r} + } return blackfriday.Run(doc, - []blackfriday.Option{blackfriday.WithRenderer(renderer), - blackfriday.WithExtensions(renderer.GetExtensions())}...) + []blackfriday.Option{ + blackfriday.WithRenderer(r), + blackfriday.WithExtensions(renderer.GetExtensions()), + }...) } diff --git a/vendor/github.com/cpuguy83/go-md2man/v2/md2man/roff.go b/vendor/github.com/cpuguy83/go-md2man/v2/md2man/roff.go index be2b34360..9d6c473fd 100644 --- a/vendor/github.com/cpuguy83/go-md2man/v2/md2man/roff.go +++ b/vendor/github.com/cpuguy83/go-md2man/v2/md2man/roff.go @@ -1,6 +1,8 @@ package md2man import ( + "bufio" + "bytes" "fmt" "io" "os" @@ -12,68 +14,72 @@ import ( // roffRenderer implements the blackfriday.Renderer interface for creating // roff format (manpages) from markdown text type roffRenderer struct { - extensions blackfriday.Extensions listCounters []int firstHeader bool - firstDD bool listDepth int } const ( - titleHeader = ".TH " - topLevelHeader = "\n\n.SH " - secondLevelHdr = "\n.SH " - otherHeader = "\n.SS " - crTag = "\n" - emphTag = "\\fI" - emphCloseTag = "\\fP" - strongTag = "\\fB" - strongCloseTag = "\\fP" - breakTag = "\n.br\n" - paraTag = "\n.PP\n" - hruleTag = "\n.ti 0\n\\l'\\n(.lu'\n" - linkTag = "\n\\[la]" - linkCloseTag = "\\[ra]" - codespanTag = "\\fB\\fC" - codespanCloseTag = "\\fR" - codeTag = "\n.PP\n.RS\n\n.nf\n" - codeCloseTag = "\n.fi\n.RE\n" - quoteTag = "\n.PP\n.RS\n" - quoteCloseTag = "\n.RE\n" - listTag = "\n.RS\n" - listCloseTag = "\n.RE\n" - dtTag = "\n.TP\n" - dd2Tag = "\n" - tableStart = "\n.TS\nallbox;\n" - tableEnd = ".TE\n" - tableCellStart = "T{\n" - tableCellEnd = "\nT}\n" + titleHeader = ".TH " + topLevelHeader = "\n\n.SH " + secondLevelHdr = "\n.SH " + otherHeader = "\n.SS " + crTag = "\n" + emphTag = "\\fI" + emphCloseTag = "\\fP" + strongTag = "\\fB" + strongCloseTag = "\\fP" + breakTag = "\n.br\n" + paraTag = "\n.PP\n" + hruleTag = "\n.ti 0\n\\l'\\n(.lu'\n" + linkTag = "\n\\[la]" + linkCloseTag = "\\[ra]" + codespanTag = "\\fB" + codespanCloseTag = "\\fR" + codeTag = "\n.EX\n" + codeCloseTag = ".EE\n" // Do not prepend a newline character since code blocks, by definition, include a newline already (or at least as how blackfriday gives us on). + quoteTag = "\n.PP\n.RS\n" + quoteCloseTag = "\n.RE\n" + listTag = "\n.RS\n" + listCloseTag = ".RE\n" + dtTag = "\n.TP\n" + dd2Tag = "\n" + tableStart = "\n.TS\nallbox;\n" + tableEnd = ".TE\n" + tableCellStart = "T{\n" + tableCellEnd = "\nT}\n" + tablePreprocessor = `'\" t` ) // NewRoffRenderer creates a new blackfriday Renderer for generating roff documents // from markdown func NewRoffRenderer() *roffRenderer { // nolint: golint - var extensions blackfriday.Extensions - - extensions |= blackfriday.NoIntraEmphasis - extensions |= blackfriday.Tables - extensions |= blackfriday.FencedCode - extensions |= blackfriday.SpaceHeadings - extensions |= blackfriday.Footnotes - extensions |= blackfriday.Titleblock - extensions |= blackfriday.DefinitionLists - return &roffRenderer{ - extensions: extensions, - } + return &roffRenderer{} } // GetExtensions returns the list of extensions used by this renderer implementation -func (r *roffRenderer) GetExtensions() blackfriday.Extensions { - return r.extensions +func (*roffRenderer) GetExtensions() blackfriday.Extensions { + return blackfriday.NoIntraEmphasis | + blackfriday.Tables | + blackfriday.FencedCode | + blackfriday.SpaceHeadings | + blackfriday.Footnotes | + blackfriday.Titleblock | + blackfriday.DefinitionLists } // RenderHeader handles outputting the header at document start func (r *roffRenderer) RenderHeader(w io.Writer, ast *blackfriday.Node) { + // We need to walk the tree to check if there are any tables. + // If there are, we need to enable the roff table preprocessor. + ast.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus { + if node.Type == blackfriday.Table { + out(w, tablePreprocessor+"\n") + return blackfriday.Terminate + } + return blackfriday.GoToNext + }) + // disable hyphenation out(w, ".nh\n") } @@ -86,12 +92,27 @@ func (r *roffRenderer) RenderFooter(w io.Writer, ast *blackfriday.Node) { // RenderNode is called for each node in a markdown document; based on the node // type the equivalent roff output is sent to the writer func (r *roffRenderer) RenderNode(w io.Writer, node *blackfriday.Node, entering bool) blackfriday.WalkStatus { - - var walkAction = blackfriday.GoToNext + walkAction := blackfriday.GoToNext switch node.Type { case blackfriday.Text: - escapeSpecialChars(w, node.Literal) + // Special case: format the NAME section as required for proper whatis parsing. + // Refer to the lexgrog(1) and groff_man(7) manual pages for details. + if node.Parent != nil && + node.Parent.Type == blackfriday.Paragraph && + node.Parent.Prev != nil && + node.Parent.Prev.Type == blackfriday.Heading && + node.Parent.Prev.FirstChild != nil && + bytes.EqualFold(node.Parent.Prev.FirstChild.Literal, []byte("NAME")) { + before, after, found := bytes.Cut(node.Literal, []byte(" - ")) + escapeSpecialChars(w, before) + if found { + out(w, ` \- `) + escapeSpecialChars(w, after) + } + } else { + escapeSpecialChars(w, node.Literal) + } case blackfriday.Softbreak: out(w, crTag) case blackfriday.Hardbreak: @@ -109,9 +130,16 @@ func (r *roffRenderer) RenderNode(w io.Writer, node *blackfriday.Node, entering out(w, strongCloseTag) } case blackfriday.Link: - if !entering { - out(w, linkTag+string(node.LinkData.Destination)+linkCloseTag) + // Don't render the link text for automatic links, because this + // will only duplicate the URL in the roff output. + // See https://daringfireball.net/projects/markdown/syntax#autolink + if !bytes.Equal(node.LinkData.Destination, node.FirstChild.Literal) { + out(w, string(node.FirstChild.Literal)) } + // Hyphens in a link must be escaped to avoid word-wrap in the rendered man page. + escapedLink := strings.ReplaceAll(string(node.LinkData.Destination), "-", "\\-") + out(w, linkTag+escapedLink+linkCloseTag) + walkAction = blackfriday.SkipChildren case blackfriday.Image: // ignore images walkAction = blackfriday.SkipChildren @@ -122,14 +150,25 @@ func (r *roffRenderer) RenderNode(w io.Writer, node *blackfriday.Node, entering case blackfriday.Document: break case blackfriday.Paragraph: - // roff .PP markers break lists - if r.listDepth > 0 { - return blackfriday.GoToNext - } if entering { - out(w, paraTag) + if r.listDepth > 0 { + // roff .PP markers break lists + if node.Prev != nil { // continued paragraph + if node.Prev.Type == blackfriday.List && node.Prev.ListFlags&blackfriday.ListTypeDefinition == 0 { + out(w, ".IP\n") + } else { + out(w, crTag) + } + } + } else if node.Prev != nil && node.Prev.Type == blackfriday.Heading { + out(w, crTag) + } else { + out(w, paraTag) + } } else { - out(w, crTag) + if node.Next == nil || node.Next.Type != blackfriday.List { + out(w, crTag) + } } case blackfriday.BlockQuote: if entering { @@ -160,6 +199,11 @@ func (r *roffRenderer) RenderNode(w io.Writer, node *blackfriday.Node, entering r.handleTableCell(w, node, entering) case blackfriday.HTMLSpan: // ignore other HTML tags + case blackfriday.HTMLBlock: + if bytes.HasPrefix(node.Literal, []byte("