tangled
alpha
login
or
join now
evan.jarrett.net
/
aoc2025
1
fork
atom
no this isn't about alexandria ocasio-cortez
1
fork
atom
overview
issues
pulls
pipelines
day 2, day 3
evan.jarrett.net
3 months ago
09607325
fecd7a2b
verified
This commit was signed with the committer's
known signature
.
evan.jarrett.net
SSH Key Fingerprint:
SHA256:bznk0uVPp7XFOl67P0uTM1pCjf2A4ojeP/lsUE7uauQ=
+425
-33
7 changed files
expand all
collapse all
unified
split
cmd
one
main.go
main_test.go
three
main.go
main_test.go
two
main.go
main_test.go
newday.sh
+8
-11
cmd/one/main.go
···
2
3
import (
4
"fmt"
0
5
"strconv"
6
"strings"
7
···
31
func (d *DayOne) Part1() (int, error) {
32
position := 50
33
zeroCount := 0
34
-
fmt.Printf("The dial starts by pointing at %d.\n", position)
35
36
for _, rot := range d.rotations {
37
newPosition, _, err := Rotate(position, rot)
38
if err != nil {
39
return 0, err
40
}
41
-
fmt.Printf("The dial is rotated %d to point at %d.\n", rot, newPosition)
42
position = newPosition
43
if position == 0 {
44
zeroCount++
45
}
46
}
47
48
-
fmt.Printf("Final position: %d\n", position)
49
return zeroCount, nil
50
}
51
52
func (d *DayOne) Part2() (int, error) {
53
position := 50
54
zeroCount := 0
55
-
fmt.Printf("The dial starts by pointing at %d.\n", position)
56
57
for _, rot := range d.rotations {
58
newPosition, passes, err := Rotate(position, rot)
59
if err != nil {
60
return 0, err
61
}
62
-
fmt.Printf("The dial is rotated %d to point at %d.", rot, newPosition)
63
if passes > 0 {
64
-
if newPosition != 0 || passes > 2 {
65
-
fmt.Printf(" during this rotation, it points at 0 %d times.", passes)
66
-
}
67
zeroCount += passes
68
}
69
-
fmt.Println()
70
-
fmt.Printf("count is now: %d\n", zeroCount)
71
position = newPosition
72
}
73
74
-
fmt.Printf("Final position: %d\n", position)
75
return zeroCount, nil
76
}
77
···
2
3
import (
4
"fmt"
5
+
"log/slog"
6
"strconv"
7
"strings"
8
···
32
func (d *DayOne) Part1() (int, error) {
33
position := 50
34
zeroCount := 0
35
+
slog.Debug("The dial starts by pointing at", "position", position)
36
37
for _, rot := range d.rotations {
38
newPosition, _, err := Rotate(position, rot)
39
if err != nil {
40
return 0, err
41
}
42
+
slog.Debug("The dial is rotated", "rotation", rot, "newPosition", newPosition)
43
position = newPosition
44
if position == 0 {
45
zeroCount++
46
}
47
}
48
49
+
slog.Debug("Final position", "position", position)
50
return zeroCount, nil
51
}
52
53
func (d *DayOne) Part2() (int, error) {
54
position := 50
55
zeroCount := 0
56
+
slog.Debug("The dial starts by pointing at", "position", position)
57
58
for _, rot := range d.rotations {
59
newPosition, passes, err := Rotate(position, rot)
60
if err != nil {
61
return 0, err
62
}
63
+
slog.Debug("The dial is rotated", "rotation", rot, "newPosition", newPosition, "passes", passes)
64
if passes > 0 {
0
0
0
65
zeroCount += passes
66
}
67
+
slog.Debug("count updated", "zeroCount", zeroCount)
0
68
position = newPosition
69
}
70
71
+
slog.Debug("Final position", "position", position)
72
return zeroCount, nil
73
}
74
+57
-2
cmd/one/main_test.go
···
1
package main
2
3
import (
0
4
"testing"
0
0
5
)
6
7
const testInput = `L68
···
54
input string
55
want int
56
}{
57
-
{"R555", 6},
58
{"L432", 4},
59
{"L555", 6},
60
}
···
84
position int
85
amount int
86
wantPos int
87
-
wantCount int
88
}{
89
// Starting at 0, no crossing
90
{"from 0, R50", 0, 50, 50, 0},
···
122
})
123
}
124
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
package main
2
3
import (
4
+
"os"
5
"testing"
6
+
7
+
"tangled.org/evan.jarrett.net/aoc2025/internal"
8
)
9
10
const testInput = `L68
···
57
input string
58
want int
59
}{
60
+
{"R555", 6},
61
{"L432", 4},
62
{"L555", 6},
63
}
···
87
position int
88
amount int
89
wantPos int
90
+
wantCount int
91
}{
92
// Starting at 0, no crossing
93
{"from 0, R50", 0, 50, 50, 0},
···
125
})
126
}
127
}
128
+
129
+
// getRealInput loads the real puzzle input, caching it to a file to avoid
130
+
// repeated network calls during benchmarks.
131
+
func getRealInput(b *testing.B) string {
132
+
b.Helper()
133
+
const cacheFile = "testdata/input.txt"
134
+
135
+
// Try to read from cache first
136
+
if data, err := os.ReadFile(cacheFile); err == nil {
137
+
return string(data)
138
+
}
139
+
140
+
// Fetch from AOC and cache it
141
+
input, err := internal.GetInput(1)
142
+
if err != nil {
143
+
b.Skipf("Could not fetch real input: %v", err)
144
+
}
145
+
146
+
// Cache for future runs
147
+
_ = os.MkdirAll("testdata", 0755)
148
+
_ = os.WriteFile(cacheFile, []byte(input), 0644)
149
+
150
+
return input
151
+
}
152
+
153
+
func BenchmarkPart1(b *testing.B) {
154
+
input := getRealInput(b)
155
+
156
+
d := &DayOne{}
157
+
if err := d.ParseInput(input); err != nil {
158
+
b.Fatalf("ParseInput failed: %v", err)
159
+
}
160
+
161
+
b.ResetTimer()
162
+
for i := 0; i < b.N; i++ {
163
+
d.Part1()
164
+
}
165
+
}
166
+
167
+
func BenchmarkPart2(b *testing.B) {
168
+
input := getRealInput(b)
169
+
170
+
d := &DayOne{}
171
+
if err := d.ParseInput(input); err != nil {
172
+
b.Fatalf("ParseInput failed: %v", err)
173
+
}
174
+
175
+
b.ResetTimer()
176
+
for i := 0; i < b.N; i++ {
177
+
d.Part2()
178
+
}
179
+
}
+75
cmd/three/main.go
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
package main
2
+
3
+
import (
4
+
"log/slog"
5
+
"strings"
6
+
7
+
"tangled.org/evan.jarrett.net/aoc2025/internal/puzzle"
8
+
)
9
+
10
+
type DayThree struct {
11
+
batteries []string
12
+
}
13
+
14
+
func (d *DayThree) ParseInput(input string) error {
15
+
for line := range strings.SplitSeq(strings.TrimSpace(input), "\n") {
16
+
line = strings.TrimSpace(line)
17
+
if line == "" {
18
+
continue
19
+
}
20
+
d.batteries = append(d.batteries, line)
21
+
}
22
+
return nil
23
+
}
24
+
25
+
// exactly 2
26
+
func (d *DayThree) Part1() (int, error) {
27
+
sum := 0
28
+
for _, battery := range d.batteries {
29
+
first := 0
30
+
second := 0
31
+
for i, ch := range battery {
32
+
digit := int(ch - '0')
33
+
if digit > first && i < len(battery)-1 {
34
+
first = digit
35
+
second = 0 // reset our second digit since it needs to come after the first
36
+
} else if digit > second {
37
+
second = digit
38
+
}
39
+
}
40
+
combined := first*10 + second
41
+
slog.Debug("joltage", "largest", combined, "first", first, "second", second)
42
+
sum += combined
43
+
}
44
+
return sum, nil
45
+
}
46
+
47
+
// exactly 12
48
+
func (d *DayThree) Part2() (int, error) {
49
+
max := 12
50
+
sum := 0
51
+
for _, battery := range d.batteries {
52
+
arr := make([]int, max)
53
+
for i, ch := range battery {
54
+
digit := int(ch - '0')
55
+
for j := range max {
56
+
if digit > arr[j] && i < len(battery)-(max-1-j) {
57
+
arr[j] = digit
58
+
clear(arr[j+1:])
59
+
break
60
+
}
61
+
}
62
+
}
63
+
combined := 0
64
+
for _, digit := range arr {
65
+
combined = combined*10 + digit
66
+
}
67
+
slog.Debug("joltage", "largest", combined)
68
+
sum += combined
69
+
}
70
+
return sum, nil
71
+
}
72
+
73
+
func main() {
74
+
puzzle.Run(3, &DayThree{})
75
+
}
+51
cmd/three/main_test.go
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
package main
2
+
3
+
import (
4
+
"testing"
5
+
)
6
+
7
+
const testInput = `987654321111111
8
+
811111111111119
9
+
234234234234278
10
+
818181911112111
11
+
`
12
+
13
+
func TestPart1(t *testing.T) {
14
+
// slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
15
+
// Level: slog.LevelDebug,
16
+
// })))
17
+
d := &DayThree{}
18
+
if err := d.ParseInput(testInput); err != nil {
19
+
t.Fatalf("ParseInput failed: %v", err)
20
+
}
21
+
22
+
got, err := d.Part1()
23
+
if err != nil {
24
+
t.Fatalf("Part1 failed: %v", err)
25
+
}
26
+
27
+
want := 357
28
+
if got != want {
29
+
t.Errorf("Part1() = %d, want %d", got, want)
30
+
}
31
+
}
32
+
33
+
func TestPart2(t *testing.T) {
34
+
// slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
35
+
// Level: slog.LevelDebug,
36
+
// })))
37
+
d := &DayThree{}
38
+
if err := d.ParseInput(testInput); err != nil {
39
+
t.Fatalf("ParseInput failed: %v", err)
40
+
}
41
+
42
+
got, err := d.Part2()
43
+
if err != nil {
44
+
t.Fatalf("Part2 failed: %v", err)
45
+
}
46
+
47
+
want := 3121910778619 // TODO: set expected value
48
+
if got != want {
49
+
t.Errorf("Part2() = %d, want %d", got, want)
50
+
}
51
+
}
+85
cmd/two/main.go
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
package main
2
+
3
+
import (
4
+
"log/slog"
5
+
"strconv"
6
+
"strings"
7
+
8
+
"tangled.org/evan.jarrett.net/aoc2025/internal/puzzle"
9
+
)
10
+
11
+
type ProductId struct {
12
+
firstId int
13
+
lastId int
14
+
}
15
+
16
+
type DayTwo struct {
17
+
productIds []ProductId
18
+
}
19
+
20
+
func (d *DayTwo) ParseInput(input string) error {
21
+
for productId := range strings.SplitSeq(strings.TrimSpace(input), ",") {
22
+
productId = strings.TrimSpace(productId)
23
+
if productId == "" {
24
+
continue
25
+
}
26
+
parts := strings.Split(productId, "-")
27
+
first, _ := strconv.Atoi(parts[0])
28
+
last, _ := strconv.Atoi(parts[1])
29
+
30
+
product := ProductId{
31
+
firstId: first,
32
+
lastId: last,
33
+
}
34
+
35
+
d.productIds = append(d.productIds, product)
36
+
}
37
+
return nil
38
+
}
39
+
40
+
func findPattern(productId int, j int) int {
41
+
s := strconv.Itoa(productId)
42
+
n := len(s)
43
+
if n%j == 0 {
44
+
piece := s[:n/j]
45
+
if strings.Repeat(piece, j) == s {
46
+
slog.Debug("found invalid ID", "id", s)
47
+
return productId
48
+
}
49
+
}
50
+
return 0
51
+
}
52
+
53
+
// find ids that have a repeating pattern. 55 = 5 twice, 6464 = 64 twice.
54
+
func (d *DayTwo) Part1() (int, error) {
55
+
sum := 0
56
+
for _, product := range d.productIds {
57
+
for i := product.firstId; i <= product.lastId; i++ {
58
+
sum += findPattern(i, 2)
59
+
}
60
+
}
61
+
62
+
return sum, nil
63
+
}
64
+
65
+
func (d *DayTwo) Part2() (int, error) {
66
+
sum := 0
67
+
for _, product := range d.productIds {
68
+
for i := product.firstId; i <= product.lastId; i++ {
69
+
s := strconv.Itoa(i)
70
+
n := len(s)
71
+
for j := 2; j <= n; j++ {
72
+
if result := findPattern(i, j); result > 0 {
73
+
sum += result
74
+
break
75
+
}
76
+
}
77
+
}
78
+
}
79
+
80
+
return sum, nil
81
+
}
82
+
83
+
func main() {
84
+
puzzle.Run(2, &DayTwo{})
85
+
}
+96
cmd/two/main_test.go
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
package main
2
+
3
+
import (
4
+
"os"
5
+
"testing"
6
+
7
+
"tangled.org/evan.jarrett.net/aoc2025/internal"
8
+
)
9
+
10
+
const testInput = `11-22,95-115,998-1012,1188511880-1188511890,222220-222224,1698522-1698528,446443-446449,38593856-38593862,565653-565659,824824821-824824827,2121212118-2121212124`
11
+
12
+
func TestPart1(t *testing.T) {
13
+
d := &DayTwo{}
14
+
if err := d.ParseInput(testInput); err != nil {
15
+
t.Fatalf("ParseInput failed: %v", err)
16
+
}
17
+
18
+
got, err := d.Part1()
19
+
if err != nil {
20
+
t.Fatalf("Part1 failed: %v", err)
21
+
}
22
+
23
+
want := 1227775554
24
+
if got != want {
25
+
t.Errorf("Part1() = %d, want %d", got, want)
26
+
}
27
+
}
28
+
29
+
func TestPart2(t *testing.T) {
30
+
d := &DayTwo{}
31
+
if err := d.ParseInput(testInput); err != nil {
32
+
t.Fatalf("ParseInput failed: %v", err)
33
+
}
34
+
35
+
got, err := d.Part2()
36
+
if err != nil {
37
+
t.Fatalf("Part2 failed: %v", err)
38
+
}
39
+
40
+
want := 4174379265
41
+
if got != want {
42
+
t.Errorf("Part2() = %d, want %d", got, want)
43
+
}
44
+
}
45
+
46
+
// getRealInput loads the real puzzle input, caching it to a file to avoid
47
+
// repeated network calls during benchmarks.
48
+
func getRealInput(b *testing.B) string {
49
+
b.Helper()
50
+
const cacheFile = "testdata/input.txt"
51
+
52
+
// Try to read from cache first
53
+
if data, err := os.ReadFile(cacheFile); err == nil {
54
+
return string(data)
55
+
}
56
+
57
+
// Fetch from AOC and cache it
58
+
input, err := internal.GetInput(2)
59
+
if err != nil {
60
+
b.Skipf("Could not fetch real input: %v", err)
61
+
}
62
+
63
+
// Cache for future runs
64
+
_ = os.MkdirAll("testdata", 0755)
65
+
_ = os.WriteFile(cacheFile, []byte(input), 0644)
66
+
67
+
return input
68
+
}
69
+
70
+
func BenchmarkPart1(b *testing.B) {
71
+
input := getRealInput(b)
72
+
73
+
d := &DayTwo{}
74
+
if err := d.ParseInput(input); err != nil {
75
+
b.Fatalf("ParseInput failed: %v", err)
76
+
}
77
+
78
+
b.ResetTimer()
79
+
for i := 0; i < b.N; i++ {
80
+
d.Part1()
81
+
}
82
+
}
83
+
84
+
func BenchmarkPart2(b *testing.B) {
85
+
input := getRealInput(b)
86
+
87
+
d := &DayTwo{}
88
+
if err := d.ParseInput(input); err != nil {
89
+
b.Fatalf("ParseInput failed: %v", err)
90
+
}
91
+
92
+
b.ResetTimer()
93
+
for i := 0; i < b.N; i++ {
94
+
d.Part2()
95
+
}
96
+
}
+53
-20
newday.sh
···
2
3
set -e
4
5
-
if [ -z "$1" ]; then
6
-
echo "Usage: $0 <day>"
7
-
echo "Example: $0 5"
0
0
0
8
exit 1
9
fi
10
11
-
DAY=$1
12
-
13
-
if ! [[ "$DAY" =~ ^[0-9]+$ ]] || [ "$DAY" -lt 1 ] || [ "$DAY" -gt 12 ]; then
14
-
echo "Error: Day must be a number between 1 and 12"
15
exit 1
16
fi
17
18
-
# Check if the puzzle is available (unlocks at midnight EST / 11pm CST)
19
-
EST_DAY=$(TZ="America/New_York" date +%-d)
20
-
EST_MONTH=$(TZ="America/New_York" date +%-m)
21
-
EST_YEAR=$(TZ="America/New_York" date +%Y)
22
-
23
-
if [ "$EST_YEAR" -eq 2025 ] && [ "$EST_MONTH" -eq 12 ]; then
24
-
if [ "$DAY" -gt "$EST_DAY" ]; then
25
-
echo "Error: Day $DAY puzzle not yet available."
26
-
echo "Puzzles unlock at midnight EST (11pm CST)."
27
-
echo "Current EST date: December $EST_DAY, 2025"
28
-
exit 1
29
-
fi
30
-
fi
31
32
declare -a WORDS=("" "one" "two" "three" "four" "five" "six" "seven" "eight" "nine" "ten"
33
"eleven" "twelve")
···
87
puzzle.Run($DAY, &Day${DAY_WORD_CAP}{})
88
}
89
EOF
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
90
echo "Created $CMD_DIR/main.go"
0
91
fi
···
2
3
set -e
4
5
+
# Determine current day based on EST timezone (puzzles unlock at midnight EST)
6
+
EST_DAY=$(TZ="America/New_York" date +%-d)
7
+
EST_MONTH=$(TZ="America/New_York" date +%-m)
8
+
9
+
if [ "$EST_MONTH" -ne 12 ]; then
10
+
echo "Error: Advent of Code runs in December"
11
exit 1
12
fi
13
14
+
if [ "$EST_DAY" -gt 12 ]; then
15
+
echo "Error: This event only has 12 days (current EST day: $EST_DAY)"
0
0
16
exit 1
17
fi
18
19
+
DAY=$EST_DAY
0
0
0
0
0
0
0
0
0
0
0
0
20
21
declare -a WORDS=("" "one" "two" "three" "four" "five" "six" "seven" "eight" "nine" "ten"
22
"eleven" "twelve")
···
76
puzzle.Run($DAY, &Day${DAY_WORD_CAP}{})
77
}
78
EOF
79
+
cat > "$CMD_DIR/main_test.go" << EOF
80
+
package main
81
+
82
+
import (
83
+
"testing"
84
+
)
85
+
86
+
const testInput = \`\`
87
+
88
+
func TestPart1(t *testing.T) {
89
+
d := &Day${DAY_WORD_CAP}{}
90
+
if err := d.ParseInput(testInput); err != nil {
91
+
t.Fatalf("ParseInput failed: %v", err)
92
+
}
93
+
94
+
got, err := d.Part1()
95
+
if err != nil {
96
+
t.Fatalf("Part1 failed: %v", err)
97
+
}
98
+
99
+
want := 0 // TODO: set expected value
100
+
if got != want {
101
+
t.Errorf("Part1() = %d, want %d", got, want)
102
+
}
103
+
}
104
+
105
+
func TestPart2(t *testing.T) {
106
+
d := &Day${DAY_WORD_CAP}{}
107
+
if err := d.ParseInput(testInput); err != nil {
108
+
t.Fatalf("ParseInput failed: %v", err)
109
+
}
110
+
111
+
got, err := d.Part2()
112
+
if err != nil {
113
+
t.Fatalf("Part2 failed: %v", err)
114
+
}
115
+
116
+
want := 0 // TODO: set expected value
117
+
if got != want {
118
+
t.Errorf("Part2() = %d, want %d", got, want)
119
+
}
120
+
}
121
+
EOF
122
echo "Created $CMD_DIR/main.go"
123
+
echo "Created $CMD_DIR/main_test.go"
124
fi