diff --git a/examples/golang/2016/01/main.go b/examples/golang/2016/01/main.go index a789626..164305f 100644 --- a/examples/golang/2016/01/main.go +++ b/examples/golang/2016/01/main.go @@ -3,6 +3,8 @@ package day1 import ( "fmt" "path/filepath" + "os" + "github.com/dolfolife/aoctl/pkg/puzzle" "github.com/dolfolife/aoctl/pkg/aoc" ) @@ -14,8 +16,13 @@ func Solve() { file := filepath.Join(aocConfig.ProjectPath, "01/puzzle.yaml") inputfile1 := filepath.Join(aocConfig.ProjectPath, "01/input.txt") inputfile2 := filepath.Join(aocConfig.ProjectPath, "01/input.txt") - p := puzzle.NewPuzzleFromCache(file, []string{ inputfile1, inputfile2 }) + p, err := puzzle.NewPuzzleFromCache(file, []string{ inputfile1, inputfile2 }) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + part1 := Day1Part1Solver{} part1.Puzzle = p.Puzzles[0] answer1, err := part1.Solve() diff --git a/examples/golang/2016/02/main.go b/examples/golang/2016/02/main.go index c5e9790..4896cfc 100644 --- a/examples/golang/2016/02/main.go +++ b/examples/golang/2016/02/main.go @@ -2,7 +2,9 @@ package day2 import ( "fmt" + "os" "path/filepath" + "github.com/dolfolife/aoctl/pkg/puzzle" "github.com/dolfolife/aoctl/pkg/aoc" ) @@ -14,8 +16,12 @@ func Solve() { file := filepath.Join(aocConfig.ProjectPath, "02/puzzle.yaml") inputfile1 := filepath.Join(aocConfig.ProjectPath, "02/input.txt") inputfile2 := filepath.Join(aocConfig.ProjectPath, "02/input.txt") - p := puzzle.NewPuzzleFromCache(file, []string{inputfile1, inputfile2}) + p, err := puzzle.NewPuzzleFromCache(file, []string{inputfile1, inputfile2}) + if err != nil { + fmt.Println(err) + os.Exit(1) + } part1 := Day2Part1Solver{} part1.Puzzle = p.Puzzles[0] answer1, err := part1.Solve() diff --git a/examples/golang/2016/03/main.go b/examples/golang/2016/03/main.go index 98a1580..8beccc6 100644 --- a/examples/golang/2016/03/main.go +++ b/examples/golang/2016/03/main.go @@ -3,6 +3,8 @@ package day3 import ( "fmt" "path/filepath" + "os" + "github.com/dolfolife/aoctl/pkg/puzzle" "github.com/dolfolife/aoctl/pkg/aoc" ) @@ -14,7 +16,13 @@ func Solve() { file := filepath.Join(aocConfig.ProjectPath, "03/puzzle.yaml") inputfile1 := filepath.Join(aocConfig.ProjectPath, "03/input.txt") inputfile2 := filepath.Join(aocConfig.ProjectPath, "03/input.txt") - p := puzzle.NewPuzzleFromCache(file, []string{inputfile1, inputfile2}) + p, err := puzzle.NewPuzzleFromCache(file, []string{inputfile1, inputfile2}) + + if err != nil { + fmt.Println(err) + os.Exit(1) + } + part1 := Day3Part1Solver{} part1.Puzzle = p.Puzzles[0] answer1, err := part1.Solve() diff --git a/pkg/math/geometry.go b/pkg/math/geometry.go index 949a075..97c9f98 100644 --- a/pkg/math/geometry.go +++ b/pkg/math/geometry.go @@ -1,45 +1,44 @@ package math import ( - "math" - "strconv" + "math" + "strconv" ) type Point struct { - X, Y float64 + X, Y float64 } type Segment struct { - A, B Point + A, B Point } func CalDistancePoints(pos1 Point, pos2 Point) string { - partX := math.Abs(float64(pos1.X) - float64(pos2.X)) - partY := math.Abs(float64(pos1.Y) - float64(pos2.Y)) - return strconv.Itoa(int(partX + partY)) + partX := math.Abs(float64(pos1.X) - float64(pos2.X)) + partY := math.Abs(float64(pos1.Y) - float64(pos2.Y)) + return strconv.Itoa(int(partX + partY)) } - func CalIntersectionPoint(a, b, c, d Point) (bool, Point) { - cross := (c.Y-d.Y)*(b.X-a.X) - (c.X-d.X)*(b.Y-a.Y) - if cross == 0 { - return false, Point{} // Parallel lines, no intersection - } - - t1 := float64((c.Y-d.Y)*(c.X-a.X) + (d.X-c.X)*(c.Y-a.Y)) / float64(cross) - t2 := float64((a.Y-b.Y)*(c.X-a.X) + (b.X-a.X)*(c.Y-a.Y)) / float64(cross) - - p1 := Point{b.X - a.X, b.Y - a.Y} - // Check if the intersection point is within the line segments - if t1 >= 0 && t1 <= 1 && t2 >= 0 && t2 <= 1 { - intersectionX := a.X + t1*p1.X - intersectionY := a.Y + t1*p1.Y - return true, Point{intersectionX, intersectionY} - } - - return false, Point{} // The intersection point is outside the line segments + cross := (c.Y-d.Y)*(b.X-a.X) - (c.X-d.X)*(b.Y-a.Y) + if cross == 0 { + return false, Point{} // Parallel lines, no intersection + } + + t1 := float64((c.Y-d.Y)*(c.X-a.X)+(d.X-c.X)*(c.Y-a.Y)) / float64(cross) + t2 := float64((a.Y-b.Y)*(c.X-a.X)+(b.X-a.X)*(c.Y-a.Y)) / float64(cross) + + p1 := Point{b.X - a.X, b.Y - a.Y} + // Check if the intersection point is within the line segments + if t1 >= 0 && t1 <= 1 && t2 >= 0 && t2 <= 1 { + intersectionX := a.X + t1*p1.X + intersectionY := a.Y + t1*p1.Y + return true, Point{intersectionX, intersectionY} + } + + return false, Point{} // The intersection point is outside the line segments } func IsValidTriangle(a, b, c int) bool { - return a+b > c && a+c > b && b+c > a + return a+b > c && a+c > b && b+c > a } diff --git a/pkg/math/geometry_test.go b/pkg/math/geometry_test.go index 102483f..1fcaf68 100755 --- a/pkg/math/geometry_test.go +++ b/pkg/math/geometry_test.go @@ -1,40 +1,40 @@ package math import ( - "testing" + "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/assert" ) func TestCalDistancePoints(t *testing.T) { - assert.Equal(t, "5", CalDistancePoints(Point{X: 0, Y: 0}, Point{X: 5, Y: 0})) - assert.Equal(t, "5", CalDistancePoints(Point{X: 0, Y: 0}, Point{X: 0, Y: 5})) - assert.Equal(t, "5", CalDistancePoints(Point{X: 0, Y: 0}, Point{X: -5, Y: 0})) - assert.Equal(t, "5", CalDistancePoints(Point{X: 0, Y: 0}, Point{X: 0, Y: -5})) - assert.Equal(t, "10", CalDistancePoints(Point{X: 0, Y: 0}, Point{X: 5, Y: 5})) - assert.Equal(t, "10", CalDistancePoints(Point{X: 0, Y: 0}, Point{X: -5, Y: 5})) - assert.Equal(t, "10", CalDistancePoints(Point{X: 0, Y: 0}, Point{X: 5, Y: -5})) - assert.Equal(t, "10", CalDistancePoints(Point{X: 0, Y: 0}, Point{X: -5, Y: -5})) + assert.Equal(t, "5", CalDistancePoints(Point{X: 0, Y: 0}, Point{X: 5, Y: 0})) + assert.Equal(t, "5", CalDistancePoints(Point{X: 0, Y: 0}, Point{X: 0, Y: 5})) + assert.Equal(t, "5", CalDistancePoints(Point{X: 0, Y: 0}, Point{X: -5, Y: 0})) + assert.Equal(t, "5", CalDistancePoints(Point{X: 0, Y: 0}, Point{X: 0, Y: -5})) + assert.Equal(t, "10", CalDistancePoints(Point{X: 0, Y: 0}, Point{X: 5, Y: 5})) + assert.Equal(t, "10", CalDistancePoints(Point{X: 0, Y: 0}, Point{X: -5, Y: 5})) + assert.Equal(t, "10", CalDistancePoints(Point{X: 0, Y: 0}, Point{X: 5, Y: -5})) + assert.Equal(t, "10", CalDistancePoints(Point{X: 0, Y: 0}, Point{X: -5, Y: -5})) } func TestCalIntersectionPoint(t *testing.T) { - intersect, value := CalIntersectionPoint(Point{X: 0, Y: 0}, Point{X: 8, Y: 0}, Point{X: 4, Y: -4}, Point{X: 4, Y: 4}) - assert.Equal(t, true, intersect) - assert.Equal(t, Point{4,0}, value) - - intersect, value = CalIntersectionPoint(Point{X: 4, Y: -4}, Point{X: 4, Y: 4}, Point{X: 0, Y: 0}, Point{X: 8, Y: 0}) - assert.Equal(t, true, intersect) - assert.Equal(t, Point{4,0}, value) - - intersect, value = CalIntersectionPoint(Point{X: 0, Y: -8}, Point{X: 0, Y: -1}, Point{X: 0, Y: 0}, Point{X: 8, Y: 0}) - assert.Equal(t, false, intersect) + intersect, value := CalIntersectionPoint(Point{X: 0, Y: 0}, Point{X: 8, Y: 0}, Point{X: 4, Y: -4}, Point{X: 4, Y: 4}) + assert.Equal(t, true, intersect) + assert.Equal(t, Point{4, 0}, value) + + intersect, value = CalIntersectionPoint(Point{X: 4, Y: -4}, Point{X: 4, Y: 4}, Point{X: 0, Y: 0}, Point{X: 8, Y: 0}) + assert.Equal(t, true, intersect) + assert.Equal(t, Point{4, 0}, value) + + intersect, value = CalIntersectionPoint(Point{X: 0, Y: -8}, Point{X: 0, Y: -1}, Point{X: 0, Y: 0}, Point{X: 8, Y: 0}) + assert.Equal(t, false, intersect) } func TestTriangleMath(t *testing.T) { - assert.Equal(t, true, IsValidTriangle(1, 1, 1)) - assert.Equal(t, false, IsValidTriangle(5, 10, 25)) - assert.Equal(t, true, IsValidTriangle(10, 1, 10)) - assert.Equal(t, true, IsValidTriangle(25, 1, 25)) + assert.Equal(t, true, IsValidTriangle(1, 1, 1)) + assert.Equal(t, false, IsValidTriangle(5, 10, 25)) + assert.Equal(t, true, IsValidTriangle(10, 1, 10)) + assert.Equal(t, true, IsValidTriangle(25, 1, 25)) } diff --git a/pkg/puzzle/puzzle.go b/pkg/puzzle/puzzle.go index 004b49d..300b4f0 100644 --- a/pkg/puzzle/puzzle.go +++ b/pkg/puzzle/puzzle.go @@ -4,26 +4,23 @@ import( "gopkg.in/yaml.v3" "log" "os" + "errors" + "fmt" + "strings" ) type PuzzleStatus string -const ( - Unsolved PuzzleStatus = "UNSOLVED" - Solved PuzzleStatus = "SOLVED" - Unreachable PuzzleStatus = "UNREACHABLE" -) - type PuzzleMetadata struct { - Day string `yaml:"day,omitempty"` - Title string `yaml:"title,omitempty"` - Year string `yaml:"year,omitempty"` + Day string `yaml:"day"` + Title string `yaml:"title"` + Year string `yaml:"year"` } type PuzzlePart struct { Answer string `yaml:"answer,omitempty"` - Description string `yaml:"description,omitempty"` - Status PuzzleStatus `yaml:"status,omitempty"` + Description string `yaml:"description"` + Status PuzzleStatus `yaml:"status"` RawInput []byte } @@ -44,27 +41,58 @@ func NewPuzzleFromHTML(day string, year string, htmlString string, input []byte) } } -func NewPuzzleFromCache(filepath string, inputFilepath []string) Puzzle { +// The field status is a collection of status and we need to validate that the +// status is in the set of valid statuses +func (p *Puzzle) ParseFields() error { + mapStatus := map[string]PuzzleStatus{ + "UNSOLVED": Unsolved, + "SOLVED": Solved, + "UNREACHABLE": Unreachable, + } + for i, puzzle := range p.Puzzles { + status := strings.ToUpper(string(puzzle.Status)) + if _, ok := mapStatus[status]; ok { + p.Puzzles[i].Status = mapStatus[status] + } else { + errorMessage := fmt.Sprintf("cannot parse Puzzle Part %d", i) + return NewError(ErrInvalidStatus, errors.New(errorMessage)) + } + } + return nil +} + +func NewPuzzleFromCache(filepath string, inputFilepath []string) (Puzzle, error) { var puzzle Puzzle yamlFile, err := os.ReadFile(filepath) if err != nil { log.Printf("Error trying to read the YAML file err = #%v ", err) + return Puzzle{}, err } err = yaml.Unmarshal(yamlFile, &puzzle) if err != nil { log.Fatalf("Unmarshal: %v", err) + return Puzzle{}, err + } + + // yaml.Umarshall does not have validation on sets like the status field + // We need to map the status of the puzzle + err = puzzle.ParseFields() + if err != nil { + log.Fatalf("Error trying to parse the Puzzle err = #%v ", err) + return Puzzle{}, err } for i, inputFile := range inputFilepath { rawInput, err := os.ReadFile(inputFile) if err != nil { - log.Printf("Error trying to read the input for Puzzle Part %d err #%v ", i, err) + log.Fatalf("Error trying to read the input for Puzzle Part %d err #%v ", i, err) + return Puzzle{}, err } // we need to delete the last byte of the input because it is a newline or EOF rawInput = rawInput[:len(rawInput)-1] puzzle.Puzzles[i].RawInput = rawInput } - return puzzle + return puzzle, nil } func getTitleFromBody(body string) string { diff --git a/pkg/puzzle/puzzle_errors.go b/pkg/puzzle/puzzle_errors.go new file mode 100644 index 0000000..6845ba5 --- /dev/null +++ b/pkg/puzzle/puzzle_errors.go @@ -0,0 +1,32 @@ +package puzzle + +import ( + "errors" +) + +const ( + Unsolved PuzzleStatus = "UNSOLVED" + Solved PuzzleStatus = "SOLVED" + Unreachable PuzzleStatus = "UNREACHABLE" +) + +var ( + ErrInvalidStatus = errors.New("invalid status") +) + +type InvalidPuzzle struct { + appError error + statusErr error +} + +// to implement the error interface we need to implement the Error() method +func (e *InvalidPuzzle) Error() string { + return errors.Join(e.statusErr, e.appError).Error() +} + +func NewError(appError error, statusErr error) *InvalidPuzzle { + return &InvalidPuzzle{ + appError: appError, + statusErr: statusErr, + } +} diff --git a/pkg/puzzle/puzzle_part.go b/pkg/puzzle/puzzle_part.go deleted file mode 100644 index 07c8813..0000000 --- a/pkg/puzzle/puzzle_part.go +++ /dev/null @@ -1,3 +0,0 @@ -package puzzle - - diff --git a/pkg/puzzle/puzzle_test.go b/pkg/puzzle/puzzle_test.go new file mode 100644 index 0000000..1e21df1 --- /dev/null +++ b/pkg/puzzle/puzzle_test.go @@ -0,0 +1,30 @@ +package puzzle + +import ( + "testing" + "os" + "path/filepath" + + "github.com/stretchr/testify/assert" +) + +func TestSolve(t *testing.T) { + ROOT_DIR := os.Getenv("PWD") + basicYaml := filepath.Join(ROOT_DIR, "test_data/basic.yaml") + subject, err := NewPuzzleFromCache(basicYaml, []string{}) + + // Basic Yaml has no errors + assert.Nil(t, err) + + // Metada is set correctly + assert.Equal(t, subject.Metadata.Year, "year_basic") + assert.Equal(t, subject.Metadata.Day, "day_basic") + assert.Equal(t, subject.Metadata.Title, "title_basic") + + // Puzzles check + assert.Equal(t, len(subject.Puzzles), 2) + assert.Equal(t, subject.Puzzles[0].Description, "description_1_basic") + assert.Equal(t, subject.Puzzles[0].Answer, "answer_1_basic") + assert.Equal(t, subject.Puzzles[0].Status, Solved) +} + diff --git a/pkg/puzzle/test_data/basic.yaml b/pkg/puzzle/test_data/basic.yaml new file mode 100644 index 0000000..f5f829c --- /dev/null +++ b/pkg/puzzle/test_data/basic.yaml @@ -0,0 +1,12 @@ +--- +metadata: + day: day_basic + year: year_basic + title: title_basic +puzzles: + - description: description_1_basic + answer: answer_1_basic + status: solved + - description: description_2_basic + answer: answer_2_basic + status: unsolved