Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
devries authored Nov 26, 2023
0 parents commit 6ae7847
Show file tree
Hide file tree
Showing 20 changed files with 1,116 additions and 0 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Test

on:
push:
pull_request:
branches: [ main ]
workflow_dispatch:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v4
with:
go-version: '^1.21'

- name: Test utilities
run: go test -cover ./utils

- name: Test solutions
if: ${{ github.repository != 'devries/aoc_template' }}
run: go test -cover ./day*
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*~
.*.swp
.DS_Store
*.json
run.go
aoc_run
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2021 Christopher De Vries

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
40 changes: 40 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
GOFILES := $(filter-out run.go, $(wildcard **/*.go))
TEMPLATES := $(wildcard templates/*.tmpl)
GO ?= go
.PHONY: run runall test build clean start help

run.go: $(GOFILES) $(TEMPLATES)
$(GO) generate

run: main.go run.go ## run the most recently edited day
$(GO) run .

runall: main.go run.go ## Run all days
$(GO) run . -a

test: ## Run all tests
$(GO) test -cover ./utils
$(GO) test -cover ./day*

aoc_run: main.go run.go
$(GO) build -o aoc_run .

build: aoc_run ## Build binary executable aoc_run

clean: ## Clean run.go and aoc_run
- rm run.go
- rm aoc_run

help: ## Show this help
@echo "These are the make commands for the solutions to this Advent of Code repository.\n"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

day%p1:
$(GO) run ./start -d $(shell echo $* | sed 's/^0*//')

day%p2: day%p1
mkdir $@
- sed -E 's/^package day(.*)p1$$/package day\1p2/' day$(*)p1/solution.go > day$(*)p2/solution.go
- sed -E 's/^package day(.*)p1$$/package day\1p2/' day$(*)p1/solution_test.go > day$(*)p2/solution_test.go

start: day$(shell date +%d)p1 ## Start today
69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Advent of Code Template in Go

This is a template to put advent of code problems into a single executable. It
allows you to measure the execution time of each part of each problem, and to
ignore some of the boilerplate. Not sure if anyone else will be interested, but
I thought I would give it a try because I have seen a lot of templates like
this.

# Instructions

To start a new day, download your input from the advent of code website and put
it in a file named `dayXX.txt` where `XX` is replaced by the two digit day of
the month within a subdirectory called `inputs`. Then, to generate code from a
template using the command:

```
make start
```

It will default to adding code for the current day. This will create a
directory called `dayXXp1` where `XX` is replaced by the day number. Inside
will be a file called `solution.go` with a `Solve` function in which to put
your solution, and a `solution_test.go` file to write your tests. The `Solve`
function takes an `io.Reader` argument which is the input and returns a
solution which can be any type.

If you wish to start a problem for a specific day, say the 21st, you can create
the desired directory from the template by using the make command to create the
part 1 directory for that day using the command below.

```
make day21p1
```

To run the last code you worked on use the command:

```
make run
```

This will generate a `run.go` file and run the most recently modified code. Once
the first part is finished you can start the second part by using the command:

```
make day21p2
```

This will copy your current part 1 code from `day21p1` and update the package
names. You can then edit that code to complete part 2.

You can run all the days with the command:

```
make runall
```

You can build a binary called `aoc_run` by using the

```
make build
```

command.

Finally, you can run your tests with the command:

```
make test
```
93 changes: 93 additions & 0 deletions gen/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package main

import (
"fmt"
"os"
"sort"
"strings"
"text/template"
"time"

"aoc/utils"
)

type DayDirectory struct {
Name string
Day int
Part string
}

func newDayDirectory(name string) DayDirectory {
res := DayDirectory{Name: name}

fmt.Sscanf(name, "day%dp%s", &(res.Day), &(res.Part))

return res
}

type TemplateData struct {
DayDirectories []DayDirectory
CurrentDirectory DayDirectory
}

type ByName []DayDirectory

func (a ByName) Len() int { return len(a) }
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByName) Less(i, j int) bool {
return a[i].Name < a[j].Name
}

func main() {
files, err := os.ReadDir(".")
utils.Check(err, "unable to read directory")

td := TemplateData{}

latest := time.Time{}

for _, f := range files {
if strings.HasPrefix(f.Name(), "day") && f.IsDir() {
found := false
sub, err := os.ReadDir(f.Name())
utils.Check(err, "unable to read subdirectory %s", f.Name())

for _, sf := range sub {
switch sf.Name() {
case "solution.go":
t, err := getModTime(sf)
utils.Check(err, "unable to get mod time for %s/%s", f.Name(), sf.Name())
if t.After(latest) {
latest = t
td.CurrentDirectory = newDayDirectory(f.Name())
}
found = true
}
}

if found {
td.DayDirectories = append(td.DayDirectories, newDayDirectory(f.Name()))
}
}
}

sort.Sort(ByName(td.DayDirectories))

tpls := template.Must(template.ParseFS(os.DirFS("templates"), "*.tmpl"))

fout, err := os.Create("run.go")
utils.Check(err, "unable to create run.go file")
defer fout.Close()

err = tpls.ExecuteTemplate(fout, "run.tmpl", td)
utils.Check(err, "unable to execute template")
}

func getModTime(de os.DirEntry) (time.Time, error) {
fi, err := de.Info()
if err != nil {
return time.Time{}, fmt.Errorf("unable to get file information: %w", err)
}

return fi.ModTime(), nil
}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module aoc

go 1.21

require github.com/spf13/pflag v1.0.5
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
116 changes: 116 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package main

//go:generate go run ./gen

import (
"fmt"
"io"
"os"
"strconv"
"time"

"aoc/utils"
"github.com/spf13/pflag"
)

var RunAll bool
var DaySelected int
var PartSelected string

func init() {
pflag.BoolVarP(&RunAll, "all", "a", false, "run all days")
pflag.IntVarP(&DaySelected, "day", "d", 0, "run specific day")
pflag.StringVarP(&PartSelected, "part", "p", "2", "run specific part (default 2)")
}

func main() {
pflag.Parse()
if RunAll {
runAll()
return
}

switch DaySelected {
case 0:
runCurrent()
default:
runDay(DaySelected, PartSelected)
}
}

type aocFunc func(io.Reader) any

type aocResult struct {
Result string
TimeElapsed time.Duration
}

type aocRunnerInput struct {
Name string
Func aocFunc
Filename string
Day int
Part string
}

func runAocPart(partFunc aocFunc, filename string) aocResult {
f, err := os.Open(filename)
utils.Check(err, "unable to open file %s", filename)
defer f.Close()

start := time.Now()
r := partFunc(f)
duration := time.Since(start)

res := aocResult{TimeElapsed: duration}

switch v := r.(type) {
case int:
res.Result = strconv.Itoa(v)
case int64:
res.Result = strconv.FormatInt(v, 10)
case uint64:
res.Result = strconv.FormatUint(v, 10)
case string:
res.Result = v
case fmt.Stringer:
res.Result = v.String()
default:
res.Result = "unknown return value"
}

return res
}

func runAll() {
var r aocResult
var total time.Duration

for _, v := range days {
r = runAocPart(v.Func, v.Filename)
total += r.TimeElapsed

fmt.Printf("%s: %s time elapsed: %s\n", v.Name, r.Result, r.TimeElapsed)
}

fmt.Printf("Overall time elapsed: %s\n", total)
}

func runDay(day int, part string) {
found := false

for _, v := range days {
if v.Day == day && v.Part == part {
fmt.Printf("Day %d part %s\n", day, part)
r := runAocPart(v.Func, v.Filename)
fmt.Println(r.Result)
fmt.Printf("Time elapsed: %s\n", r.TimeElapsed)
found = true
break
}
}

if !found {
fmt.Printf("Did not find a solution for day %d part %s\n", day, part)
}
}
Loading

0 comments on commit 6ae7847

Please sign in to comment.