From 5b7f652deeaa13e506c4bede67d993bf69c25d9a Mon Sep 17 00:00:00 2001 From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com> Date: Fri, 19 Dec 2025 19:01:44 -0600 Subject: [PATCH] refactor: edge and rectangle in separate packages --- cmd/09/edge/edge.go | 97 ++++++++++++++++++++++++++ cmd/09/edge/edge_test.go | 71 +++++++++++++++++++ cmd/09/main.go | 85 +++++------------------ cmd/09/rectangle/rectangle.go | 83 +++++++++++++++++++++++ cmd/09/rectangle/rectangle_test.go | 105 +++++++++++++++++++++++++++++ 5 files changed, 373 insertions(+), 68 deletions(-) create mode 100644 cmd/09/edge/edge.go create mode 100644 cmd/09/edge/edge_test.go create mode 100644 cmd/09/rectangle/rectangle.go create mode 100644 cmd/09/rectangle/rectangle_test.go diff --git a/cmd/09/edge/edge.go b/cmd/09/edge/edge.go new file mode 100644 index 0000000..2e18e4a --- /dev/null +++ b/cmd/09/edge/edge.go @@ -0,0 +1,97 @@ +// Package edge provides a model and methods for representing and manipulating edges. +package edge + +import ( + "math" + + "github.com/StevanFreeborn/advent-of-code-2025/cmd/09/rectangle" + "github.com/StevanFreeborn/advent-of-code-2025/internal/position" +) + +type Edge interface { + Start() position.Position + End() position.Position + VerticallyContains(position.Position) bool + IntersectsHorizontalLineAt(position.Position) bool + Intersects(rect rectangle.Rectangle) bool +} + +type edge struct { + start position.Position + end position.Position + totalHeight int + totalWidth int + minRow float64 + maxRow float64 + minColumn float64 + maxColumn float64 +} + +func From(start position.Position, end position.Position) Edge { + totalHeight := int(math.Abs(float64(end.Row() - start.Row()))) + totalWidth := int(math.Abs(float64(end.Column() - start.Column()))) + + minRow := math.Min(float64(start.Row()), float64(end.Row())) + maxRow := math.Max(float64(start.Row()), float64(end.Row())) + minColumn := math.Min(float64(start.Column()), float64(end.Column())) + maxColumn := math.Max(float64(start.Column()), float64(end.Column())) + + return edge{ + start: start, + end: end, + totalHeight: totalHeight, + totalWidth: totalWidth, + minRow: minRow, + maxRow: maxRow, + minColumn: minColumn, + maxColumn: maxColumn, + } +} + +func (e edge) Start() position.Position { + return e.start +} + +func (e edge) End() position.Position { + return e.end +} + +func (e edge) VerticallyContains(p position.Position) bool { + return (e.start.Row() <= p.Row()) && (p.Row() <= e.end.Row()) +} + +func (e edge) IntersectsHorizontalLineAt(p position.Position) bool { + centerPercentageOfEdgeHeight := int(float64(p.Row()-e.start.Row()) / float64(e.totalHeight)) + edgeColumnAtCenterRow := e.start.Column() + centerPercentageOfEdgeHeight*e.totalWidth + return p.Row() <= edgeColumnAtCenterRow +} + +func (e edge) Intersects(rect rectangle.Rectangle) bool { + isVerticalEdge := e.start.Column() == e.end.Column() + + if isVerticalEdge { + isRectBetweenEdgeColumns := e.start.Column() > rect.MinColumn() && e.start.Column() < rect.MaxColumn() + + if isRectBetweenEdgeColumns { + overlapStart := int(math.Max(e.minRow, float64(rect.MinRow()))) + overlapEnd := int(math.Min(e.maxRow, float64(rect.MaxRow()))) + + if overlapEnd > overlapStart { + return true + } + } + } else { + isRectBetweenEdgeRows := e.start.Row() > rect.MinRow() && e.start.Row() < rect.MaxRow() + + if isRectBetweenEdgeRows { + overlapStart := int(math.Max(e.minColumn, float64(rect.MinColumn()))) + overlapEnd := int(math.Min(e.maxColumn, float64(rect.MaxColumn()))) + + if overlapEnd > overlapStart { + return true + } + } + } + + return false +} diff --git a/cmd/09/edge/edge_test.go b/cmd/09/edge/edge_test.go new file mode 100644 index 0000000..dddd9ae --- /dev/null +++ b/cmd/09/edge/edge_test.go @@ -0,0 +1,71 @@ +package edge_test + +import ( + "testing" + + "github.com/StevanFreeborn/advent-of-code-2025/cmd/09/edge" + "github.com/StevanFreeborn/advent-of-code-2025/internal/position" +) + +func TestFrom(t *testing.T) { + start := position.From(1, 2) + end := position.From(3, 4) + + e := edge.From(start, end) + + if e.Start() != start { + t.Errorf("Expected start position %v, got %v", start, e.Start()) + } + + if e.End() != end { + t.Errorf("Expected end position %v, got %v", end, e.End()) + } +} + +func TestVerticallyContains(t *testing.T) { + start := position.From(1, 2) + end := position.From(3, 4) + e := edge.From(start, end) + + tests := []struct { + p position.Position + expected bool + }{ + {position.From(2, 3), true}, + {position.From(1, 2), true}, + {position.From(3, 4), true}, + {position.From(0, 0), false}, + {position.From(4, 5), false}, + } + + for _, test := range tests { + result := e.VerticallyContains(test.p) + + if result != test.expected { + t.Errorf("VerticallyContains(%v) = %v; want %v", test.p, result, test.expected) + } + } +} + +func TestIntersectsHorizontalLineAt(t *testing.T) { + start := position.From(1, 2) + end := position.From(3, 4) + e := edge.From(start, end) + + tests := []struct { + p position.Position + expected bool + }{ + {position.From(2, 3), true}, + {position.From(1, 2), true}, + {position.From(3, 4), true}, + } + + for _, test := range tests { + result := e.IntersectsHorizontalLineAt(test.p) + + if result != test.expected { + t.Errorf("IntersectsHorizontalLineAt(%v) = %v; want %v", test.p, result, test.expected) + } + } +} diff --git a/cmd/09/main.go b/cmd/09/main.go index e30d9c7..bb4b191 100644 --- a/cmd/09/main.go +++ b/cmd/09/main.go @@ -1,10 +1,11 @@ package main import ( - "math" "strconv" "strings" + "github.com/StevanFreeborn/advent-of-code-2025/cmd/09/edge" + "github.com/StevanFreeborn/advent-of-code-2025/cmd/09/rectangle" "github.com/StevanFreeborn/advent-of-code-2025/internal/file" "github.com/StevanFreeborn/advent-of-code-2025/internal/position" ) @@ -27,14 +28,10 @@ func SolvePartOne(filePath string) int { for j := i + 1; j < numberOfPositions; j++ { start := positions[i] end := positions[j] + rect := rectangle.From(start, end) - width := int(math.Abs(float64(end.Column()-start.Column())) + 1) - height := int(math.Abs(float64(end.Row()-start.Row())) + 1) - - a := width * height - - if a > largestArea { - largestArea = a + if rect.Area() > largestArea { + largestArea = rect.Area() } } } @@ -42,8 +39,6 @@ func SolvePartOne(filePath string) int { return largestArea } -// TODO: Refactor below so more understandable - func SolvePartTwo(filePath string) int { positions := []position.Position{} @@ -62,41 +57,21 @@ func SolvePartTwo(filePath string) int { for j := i + 1; j < numberOfPositions; j++ { start := positions[i] end := positions[j] + rect := rectangle.From(start, end) - rectMinRow := int(math.Min(float64(start.Row()), float64(end.Row()))) - rectMaxRow := int(math.Max(float64(start.Row()), float64(end.Row()))) - rectMinCol := int(math.Min(float64(start.Column()), float64(end.Column()))) - rectMaxCol := int(math.Max(float64(start.Column()), float64(end.Column()))) - - width := int(math.Abs(float64(rectMaxCol-rectMinCol)) + 1) - height := int(math.Abs(float64(rectMaxRow-rectMinRow)) + 1) - - a := width * height - - if a <= largestArea { + if rect.Area() <= largestArea { continue } - centerRow := int((rectMaxRow + rectMinRow) / 2) - centerCol := int((rectMaxCol + rectMinCol) / 2) - center := position.From(centerRow, centerCol) - centerIsInside := false for i := range numberOfPositions { edgeStart := positions[i] edgeEndIndex := (i + 1) % numberOfPositions edgeEnd := positions[edgeEndIndex] + edge := edge.From(edgeStart, edgeEnd) - isCenterVerticallyBetweenEdge := (edgeStart.Row() <= center.Row()) && (center.Row() <= edgeEnd.Row()) - - edgeTotalHeight := int(math.Abs(float64(edgeEnd.Row() - edgeStart.Row()))) - edgeTotalWidth := int(math.Abs(float64(edgeEnd.Column() - edgeStart.Column()))) - centerPercentageOfEdgeHeight := int(float64(center.Row()-edgeStart.Row()) / float64(edgeTotalHeight)) - edgeColumnAtCenterRow := edgeStart.Column() + centerPercentageOfEdgeHeight*edgeTotalWidth - isCenterHorizontallyToTheLeft := center.Row() <= edgeColumnAtCenterRow - - if isCenterVerticallyBetweenEdge && isCenterHorizontallyToTheLeft { + if edge.VerticallyContains(rect.Center()) && edge.IntersectsHorizontalLineAt(rect.Center()) { centerIsInside = !centerIsInside } } @@ -108,40 +83,14 @@ func SolvePartTwo(filePath string) int { isIntersectedByEdge := false for i := range numberOfPositions { - currPos := positions[i] - nextPosIndex := (i + 1) % numberOfPositions - nextPos := positions[nextPosIndex] + edgeStart := positions[i] + edgeEndIndex := (i + 1) % numberOfPositions + edgeEnd := positions[edgeEndIndex] + edge := edge.From(edgeStart, edgeEnd) - if currPos.Column() == nextPos.Column() { - col := currPos.Column() - - if col > rectMinCol && col < rectMaxCol { - edgeMinRow := math.Min(float64(currPos.Row()), float64(nextPos.Row())) - edgeMaxRow := math.Max(float64(currPos.Row()), float64(nextPos.Row())) - - overlapStart := int(math.Max(edgeMinRow, float64(rectMinRow))) - overlapEnd := int(math.Min(edgeMaxRow, float64(rectMaxRow))) - - if overlapEnd > overlapStart { - isIntersectedByEdge = true - break - } - } - } else { - row := currPos.Row() - - if row > rectMinRow && row < rectMaxRow { - edgeMinCol := math.Min(float64(currPos.Column()), float64(nextPos.Column())) - edgeMaxCol := math.Max(float64(currPos.Column()), float64(nextPos.Column())) - - overlapStart := int(math.Max(edgeMinCol, float64(rectMinCol))) - overlapEnd := int(math.Min(edgeMaxCol, float64(rectMaxCol))) - - if overlapEnd > overlapStart { - isIntersectedByEdge = true - break - } - } + if edge.Intersects(rect) { + isIntersectedByEdge = true + break } } @@ -149,7 +98,7 @@ func SolvePartTwo(filePath string) int { continue } - largestArea = a + largestArea = rect.Area() } } diff --git a/cmd/09/rectangle/rectangle.go b/cmd/09/rectangle/rectangle.go new file mode 100644 index 0000000..d297f56 --- /dev/null +++ b/cmd/09/rectangle/rectangle.go @@ -0,0 +1,83 @@ +// Package rectangle provides a model and methods for representing and manipulating rectangles. +package rectangle + +import ( + "math" + + "github.com/StevanFreeborn/advent-of-code-2025/internal/position" +) + +type Rectangle interface { + Corners() []position.Position + MaxRow() int + MaxColumn() int + MinRow() int + MinColumn() int + Area() int + Center() position.Position +} + +type rectangle struct { + corners []position.Position + maxRow int + maxColumn int + minRow int + minColumn int + area int + center position.Position +} + +func From(cornerOne position.Position, cornerTwo position.Position) Rectangle { + thirdCorner := position.From(cornerOne.Row(), cornerTwo.Column()) + fourthCorner := position.From(cornerTwo.Row(), cornerOne.Column()) + + corners := []position.Position{cornerOne, cornerTwo, thirdCorner, fourthCorner} + maxRow := math.Max(float64(cornerOne.Row()), float64(cornerTwo.Row())) + maxColumn := math.Max(float64(cornerOne.Column()), float64(cornerTwo.Column())) + minRow := math.Min(float64(cornerOne.Row()), float64(cornerTwo.Row())) + minColumn := math.Min(float64(cornerOne.Column()), float64(cornerTwo.Column())) + + area := int((maxRow - minRow + 1) * (maxColumn - minColumn + 1)) + + centerRow := (maxRow + minRow) / 2 + centerColumn := (maxColumn + minColumn) / 2 + center := position.From(int(centerRow), int(centerColumn)) + + return rectangle{ + corners: corners, + maxRow: int(maxRow), + maxColumn: int(maxColumn), + minRow: int(minRow), + minColumn: int(minColumn), + area: area, + center: center, + } +} + +func (r rectangle) Corners() []position.Position { + return r.corners +} + +func (r rectangle) MaxRow() int { + return r.maxRow +} + +func (r rectangle) MinRow() int { + return r.minRow +} + +func (r rectangle) MaxColumn() int { + return r.maxColumn +} + +func (r rectangle) MinColumn() int { + return r.minColumn +} + +func (r rectangle) Area() int { + return r.area +} + +func (r rectangle) Center() position.Position { + return r.center +} diff --git a/cmd/09/rectangle/rectangle_test.go b/cmd/09/rectangle/rectangle_test.go new file mode 100644 index 0000000..a249eb4 --- /dev/null +++ b/cmd/09/rectangle/rectangle_test.go @@ -0,0 +1,105 @@ +package rectangle_test + +import ( + "slices" + "testing" + + "github.com/StevanFreeborn/advent-of-code-2025/cmd/09/rectangle" + "github.com/StevanFreeborn/advent-of-code-2025/internal/position" +) + +func TestFrom(t *testing.T) { + cornerOne := position.From(1, 2) + cornerTwo := position.From(3, 4) + + expectedCorners := []position.Position{ + cornerOne, + cornerTwo, + position.From(1, 4), + position.From(3, 2), + } + + result := rectangle.From(cornerOne, cornerTwo) + + if slices.Equal(result.Corners(), expectedCorners) == false { + t.Errorf("Expected corners %v, but got %v", expectedCorners, result.Corners()) + } +} + +func TestMaxRow(t *testing.T) { + expectedMaxRow := 3 + + cornerOne := position.From(1, 2) + cornerTwo := position.From(3, 4) + + result := rectangle.From(cornerOne, cornerTwo).MaxRow() + + if result != expectedMaxRow { + t.Errorf("Expected max row %d, but got %d", expectedMaxRow, result) + } +} + +func TestMinRow(t *testing.T) { + expectedMinRow := 1 + + cornerOne := position.From(1, 2) + cornerTwo := position.From(3, 4) + + result := rectangle.From(cornerOne, cornerTwo).MinRow() + + if result != expectedMinRow { + t.Errorf("Expected min row %d, but got %d", expectedMinRow, result) + } +} + +func TestMaxColumn(t *testing.T) { + expectedMaxColumn := 4 + + cornerOne := position.From(1, 2) + cornerTwo := position.From(3, 4) + + result := rectangle.From(cornerOne, cornerTwo).MaxColumn() + + if result != expectedMaxColumn { + t.Errorf("Expected max column %d, but got %d", expectedMaxColumn, result) + } +} + +func TestMinColumn(t *testing.T) { + expectedMinColumn := 2 + + cornerOne := position.From(1, 2) + cornerTwo := position.From(3, 4) + + result := rectangle.From(cornerOne, cornerTwo).MinColumn() + + if result != expectedMinColumn { + t.Errorf("Expected min column %d, but got %d", expectedMinColumn, result) + } +} + +func TestArea(t *testing.T) { + expectedArea := 9 + + cornerOne := position.From(1, 2) + cornerTwo := position.From(3, 4) + + result := rectangle.From(cornerOne, cornerTwo).Area() + + if result != expectedArea { + t.Errorf("Expected area %d, but got %d", expectedArea, result) + } +} + +func TestCenter(t *testing.T) { + expectedCenter := position.From(2, 3) + + cornerOne := position.From(1, 2) + cornerTwo := position.From(3, 4) + + result := rectangle.From(cornerOne, cornerTwo).Center() + + if result != expectedCenter { + t.Errorf("Expected center %v, but got %v", expectedCenter, result) + } +}