From 25f498db285bc7774cbba0dad58287763f04ba41 Mon Sep 17 00:00:00 2001 From: Mindaugas Bernotas Date: Wed, 3 Dec 2025 22:38:57 +0200 Subject: [PATCH] Init commit --- .gitignore | 3 + README.md | 0 cmd/day1/example.txt | 10 +++ cmd/day1/main.go | 147 ++++++++++++++++++++++++++++++++++++++++ cmd/day1/main_test.go | 80 ++++++++++++++++++++++ cmd/day2/digits.go | 22 ++++++ cmd/day2/digits_test.go | 30 ++++++++ cmd/day2/example.txt | 1 + cmd/day2/main.go | 97 ++++++++++++++++++++++++++ cmd/day2/main_test.go | 77 +++++++++++++++++++++ cmd/day2/range.go | 62 +++++++++++++++++ cmd/day3/battery.go | 22 ++++++ cmd/day3/example.txt | 4 ++ cmd/day3/main.go | 86 +++++++++++++++++++++++ go.mod | 11 +++ go.sum | 10 +++ 16 files changed, 662 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 cmd/day1/example.txt create mode 100644 cmd/day1/main.go create mode 100644 cmd/day1/main_test.go create mode 100644 cmd/day2/digits.go create mode 100644 cmd/day2/digits_test.go create mode 100644 cmd/day2/example.txt create mode 100644 cmd/day2/main.go create mode 100644 cmd/day2/main_test.go create mode 100644 cmd/day2/range.go create mode 100644 cmd/day3/battery.go create mode 100644 cmd/day3/example.txt create mode 100644 cmd/day3/main.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c6ddf0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.vscode +debug*.txt +input*.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/cmd/day1/example.txt b/cmd/day1/example.txt new file mode 100644 index 0000000..53287c7 --- /dev/null +++ b/cmd/day1/example.txt @@ -0,0 +1,10 @@ +L68 +L30 +R48 +L5 +R60 +L55 +L1 +L99 +R14 +L82 diff --git a/cmd/day1/main.go b/cmd/day1/main.go new file mode 100644 index 0000000..49b0385 --- /dev/null +++ b/cmd/day1/main.go @@ -0,0 +1,147 @@ +package main + +import ( + "bufio" + "errors" + "flag" + "fmt" + "log" + "os" + "strconv" +) + +func main() { + debug := flag.Bool("debug", false, "Debug flag.") + flag.Parse() + + d1 := dial{n: 50} + d2 := dial{n: 50} + if *debug { + fmt.Println("LINE d1 d2") + fmt.Println(" 50 50") + } + + sc := bufio.NewScanner(os.Stdin) + for sc.Scan() { + if sc.Text() == "" { + log.Fatal("empty line") + } + n, err := strconv.ParseUint(sc.Text()[1:], 10, 0) + if err != nil { + log.Fatal(err) + } + i := int(n) + if sc.Text()[0] == 'L' { + i = -int(n) + } + + // if err := handleLine(d1.Add1, sc.Text()); err != nil { + // log.Fatal(err) + // } + if err := handleLine(d1.Add1, sc.Text()); err != nil { + log.Fatal(err) + } + if err := handleLine(d2.Add2, sc.Text()); err != nil { + log.Fatal(err) + } + if *debug { + fmt.Printf("%+4d %2d %2d (%+3d %+3d)\n", i, d1.n, d2.n, d1.lastClicks, d2.lastClicks) + } + } + if err := sc.Err(); err != nil { + log.Fatal(err) + } + + fmt.Println("zeros:", d1.zeros, d2.zeros) + fmt.Println("zero passes:", d1.zeroClicks, d2.zeroClicks) +} + +func handleLine(add func(int), line string) error { + if line == "" { + return errors.New("empty line") + } + n, err := strconv.ParseUint(line[1:], 10, 0) + if err != nil { + return err + } + switch line[0] { + case 'L': + add(-int(n)) + return nil + case 'R': + add(int(n)) + return nil + default: + return fmt.Errorf("unexpected line: %q", line) + } +} + +type dial struct { + n int + lastClicks int + zeros int + zeroClicks int +} + +func (d *dial) Add1(n int) { + wasAtZero := d.n == 0 + d.n += n + step := +100 + if d.n < 0 { + step = -100 + } + d.lastClicks = 0 + for d.n < 0 || d.n > 99 { + d.lastClicks++ + d.n -= step + } + if d.lastClicks > 0 && wasAtZero { + d.lastClicks-- + } + if d.n == 0 { + d.zeros++ + if d.lastClicks == 0 { + d.lastClicks++ + } + } + d.zeroClicks += d.lastClicks +} + +func (d *dial) Add2(diff int) { + n := d.n + diff + + var clicks int + switch { + case n < 0: + wasAtZero := d.n == 0 + for n < 0 { + if wasAtZero { + wasAtZero = false + } else { + clicks++ + } + n += 100 + } + if n == 0 && !wasAtZero { + clicks++ + } + + case n > 99: + for n > 99 { + clicks++ + n -= 100 + } + + default: + if n == 0 && d.n != 0 { + clicks++ + } + } + + if n == 0 { + d.zeros++ + } + d.n = n + d.lastClicks = clicks + d.zeroClicks += clicks +} diff --git a/cmd/day1/main_test.go b/cmd/day1/main_test.go new file mode 100644 index 0000000..6ec28bc --- /dev/null +++ b/cmd/day1/main_test.go @@ -0,0 +1,80 @@ +package main + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_dial_Add2(t *testing.T) { + tests := []struct { + start int + turn int + want int + wantClicks int + }{ + { + start: 50, + turn: 300, + want: 50, + wantClicks: 3, + }, + { + start: 50, + turn: -300, + want: 50, + wantClicks: 3, + }, + + { + start: 0, + turn: -100, + want: 0, + wantClicks: 1, + }, + { + start: 1, + turn: -101, + want: 0, + wantClicks: 2, + }, + { + start: 99, + turn: -199, + want: 0, + wantClicks: 2, + }, + + { + start: 0, + turn: 100, + want: 0, + wantClicks: 1, + }, + { + start: 99, + turn: 101, + want: 0, + wantClicks: 2, + }, + { + start: 1, + turn: 199, + want: 0, + wantClicks: 2, + }, + } + for _, tt := range tests { + name := fmt.Sprintf("R%d", tt.turn) + if tt.turn < 0 { + name = fmt.Sprintf("L%d", -tt.turn) + } + t.Run(name, func(t *testing.T) { + d := dial{n: tt.start} + d.Add2(tt.turn) + assert.Equal(t, tt.want, d.n, "dial") + assert.Equal(t, tt.wantClicks, d.zeroClicks, "clicks") + }) + } +} diff --git a/cmd/day2/digits.go b/cmd/day2/digits.go new file mode 100644 index 0000000..3b55ba2 --- /dev/null +++ b/cmd/day2/digits.go @@ -0,0 +1,22 @@ +package main + +func digits(n uint) []uint8 { + if n == 0 { + return []uint8{0} + } + var digits []uint8 + for ; n != 0; n /= 10 { + digits = append(digits, uint8(n%10)) + } + return digits +} + +func number(digits []uint8) uint { + var n uint + base := uint(1) + for _, d := range digits { + n += uint(d) * base + base *= 10 + } + return n +} diff --git a/cmd/day2/digits_test.go b/cmd/day2/digits_test.go new file mode 100644 index 0000000..3f36657 --- /dev/null +++ b/cmd/day2/digits_test.go @@ -0,0 +1,30 @@ +package main + +import ( + "strconv" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_digits_number(t *testing.T) { + tests := []struct { + n uint + d []uint8 + }{ + {n: 0, d: []uint8{0}}, + {n: 1234, d: []uint8{4, 3, 2, 1}}, + {n: 12345, d: []uint8{5, 4, 3, 2, 1}}, + } + for _, tt := range tests { + name := strconv.FormatUint(uint64(tt.n), 10) + t.Run(name, func(t *testing.T) { + t.Run("digits", func(t *testing.T) { + assert.Equal(t, tt.d, digits(tt.n)) + }) + t.Run("number", func(t *testing.T) { + assert.Equal(t, tt.n, number(tt.d)) + }) + }) + } +} diff --git a/cmd/day2/example.txt b/cmd/day2/example.txt new file mode 100644 index 0000000..a3f22ef --- /dev/null +++ b/cmd/day2/example.txt @@ -0,0 +1 @@ +11-22,95-115,998-1012,1188511880-1188511890,222220-222224,1698522-1698528,446443-446449,38593856-38593862,565653-565659,824824821-824824827,2121212118-2121212124 diff --git a/cmd/day2/main.go b/cmd/day2/main.go new file mode 100644 index 0000000..d077e27 --- /dev/null +++ b/cmd/day2/main.go @@ -0,0 +1,97 @@ +package main + +import ( + "flag" + "fmt" + "log" + "math" + "os" + "slices" +) + +func main() { + debug := flag.Bool("debug", false, "Debug.") + flag.Parse() + + ranges, err := readRanges(os.Stdin) + if err != nil { + log.Fatal(err) + } + + var sum1, sum2 uint + for _, r := range ranges { + invalid1 := handleRange(r, scanPart1) + invalid2 := handleRange(r, scanPart2) + + if *debug { + fmt.Printf("range: %d-%d\n", r.from, r.to) + } + if *debug && len(invalid1) > 0 { + fmt.Println("invalid (part 1):") + for _, id := range invalid1 { + fmt.Printf(" %d\n", id) + } + } + if *debug && len(invalid2) > 0 { + fmt.Println("invalid (part 2):") + for _, id := range invalid2 { + fmt.Printf(" %d\n", id) + } + } + + for _, id := range invalid1 { + sum1 += id + } + for _, id := range invalid2 { + sum2 += id + } + } + + fmt.Println("sum (part 1):", sum1) + fmt.Println("sum (part 2):", sum2) +} + +func handleRange(r idRange, scan func(id uint) (advance uint, ok bool)) []uint { + var invalid []uint + for id := r.from; id <= r.to; { + advance, ok := scan(id) + if !ok { + invalid = append(invalid, id) + } + id += advance + } + return invalid +} + +func scanPart1(id uint) (advance uint, ok bool) { + digits := digits(id) + if len(digits)%2 == 1 { // odd is always valid + nextSmallestEven := uint(math.Pow10(len(digits))) + return nextSmallestEven - id, true + } + var ( + x = uint(math.Pow10(len(digits) / 2)) + left = id / x + right = id % x + ) + return 1, left != right +} + +func scanPart2(id uint) (advance uint, ok bool) { + digits := digits(id) + if len(digits) <= 1 { + return 1, true + } + +loop: + for i := 1; i <= len(digits)/2; i++ { + ref := digits[:i] + for chunk := range slices.Chunk(digits, i) { + if !slices.Equal(chunk, ref) { + continue loop + } + } + return 1, false + } + return 1, true +} diff --git a/cmd/day2/main_test.go b/cmd/day2/main_test.go new file mode 100644 index 0000000..4bf2d16 --- /dev/null +++ b/cmd/day2/main_test.go @@ -0,0 +1,77 @@ +package main + +import ( + "strconv" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_scanPart1(t *testing.T) { + tests := []struct { + id uint + advance uint + ok bool + }{ + // { + // id: 9, + // advance: 1, + // ok: true, + // }, + // { + // id: 10, + // advance: 1, + // ok: true, + // }, + // { + // id: 11, + // advance: 1, + // ok: false, + // }, + // { + // id: 99, + // advance: 1, + // ok: false, + // }, + // { + // id: 100, + // advance: 900, + // ok: true, + // }, + { + id: 1234, + advance: 1, + ok: true, + }, + } + for _, tt := range tests { + name := strconv.FormatUint(uint64(tt.id), 10) + t.Run(name, func(t *testing.T) { + advance, ok := scanPart1(tt.id) + assert.Equal(t, tt.advance, advance, "advance") + assert.Equal(t, tt.ok, ok, "ok") + }) + } +} + +func Test_scanPart2(t *testing.T) { + tests := []struct { + id uint + advance uint + ok bool + }{ + { + id: 111, + advance: 1, + ok: false, + }, + } + for _, tt := range tests { + name := strconv.FormatUint(uint64(tt.id), 10) + t.Run(name, func(t *testing.T) { + advance, ok := scanPart2(tt.id) + assert.Equal(t, tt.advance, advance, "advance") + assert.Equal(t, tt.ok, ok, "ok") + }) + } +} diff --git a/cmd/day2/range.go b/cmd/day2/range.go new file mode 100644 index 0000000..f25d409 --- /dev/null +++ b/cmd/day2/range.go @@ -0,0 +1,62 @@ +package main + +import ( + "bufio" + "bytes" + "fmt" + "io" + "strconv" + "strings" +) + +type idRange struct { + from, to uint +} + +func readRanges(r io.Reader) ([]idRange, error) { + sc := bufio.NewScanner(r) + sc.Split(scanSimpleCSV) + + var ranges []idRange + for sc.Scan() { + before, after, ok := strings.Cut(sc.Text(), "-") + if !ok { + return nil, fmt.Errorf("range %q: missing dash", sc.Text()) + } + from, err := strconv.ParseUint(before, 10, 0) + if err != nil { + return nil, fmt.Errorf("range %q: %w", sc.Text(), err) + } + to, err := strconv.ParseUint(after, 10, 0) + if err != nil { + return nil, fmt.Errorf("range %q: %w", sc.Text(), err) + } + if from > to { + return nil, fmt.Errorf("range %q: refers from higher to lower", sc.Text()) + } + ranges = append(ranges, idRange{ + from: uint(from), + to: uint(to), + }) + } + return ranges, sc.Err() +} + +func scanSimpleCSV(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + if i := bytes.IndexByte(data, ','); i >= 0 { + return i + 1, data[:i], nil + } + if atEOF { + if trimmed, ok := bytes.CutSuffix(data, []byte("\r\n")); ok { + return len(data), trimmed, nil + } + if trimmed, ok := bytes.CutSuffix(data, []byte{'\n'}); ok { + return len(data), trimmed, nil + } + return len(data), data, nil + } + return 0, nil, nil +} diff --git a/cmd/day3/battery.go b/cmd/day3/battery.go new file mode 100644 index 0000000..2cb89ca --- /dev/null +++ b/cmd/day3/battery.go @@ -0,0 +1,22 @@ +package main + +import ( + "bufio" + "fmt" + "io" +) + +func readBatteries(r io.Reader) (banks [][]uint8, err error) { + sc := bufio.NewScanner(r) + for sc.Scan() { + var bank []uint8 + for _, b := range sc.Bytes() { + if b < '0' || b > '9' { + return nil, fmt.Errorf("bank %q: invalid joltage %q", sc.Text(), b) + } + bank = append(bank, b-'0') + } + banks = append(banks, bank) + } + return banks, sc.Err() +} diff --git a/cmd/day3/example.txt b/cmd/day3/example.txt new file mode 100644 index 0000000..7255fca --- /dev/null +++ b/cmd/day3/example.txt @@ -0,0 +1,4 @@ +987654321111111 +811111111111119 +234234234234278 +818181911112111 diff --git a/cmd/day3/main.go b/cmd/day3/main.go new file mode 100644 index 0000000..368a1c9 --- /dev/null +++ b/cmd/day3/main.go @@ -0,0 +1,86 @@ +package main + +import ( + "cmp" + "flag" + "fmt" + "log" + "math" + "os" +) + +func main() { + debug := flag.Bool("debug", false, "Debug.") + flag.Parse() + + f := os.Stdin + if flag.NArg() > 0 { + var err error + f, err = os.Open(flag.Arg(0)) + if err != nil { + log.Fatal(err) + } + defer f.Close() + } + + banks, err := readBatteries(f) + if err != nil { + log.Fatal(err) + } + + var sum1, sum2 uint + for _, bank := range banks { + power1 := bestPowerPart1(bank) + power2 := bestPowerPart2(bank) + if *debug { + for _, b := range bank { + fmt.Print(b) + } + fmt.Println(":", power1, power2) + } + sum1 += power1 + sum2 += power2 + } + + fmt.Println("sum (part 1):", sum1) + fmt.Println("sum (part 2):", sum2) +} + +func bestPowerPart1(bank []uint8) uint { + // i := maxIndex(bank[:len(bank)-1]) + // j := maxIndex(bank[i+1:]) + i + 1 + // return uint(bank[i])*10 + uint(bank[j]) + return bestPower(bank, 2) +} + +func bestPowerPart2(bank []uint8) uint { + return bestPower(bank, 12) +} + +func bestPower(bank []uint8, batteries int) uint { + if len(bank) < batteries { + panic(fmt.Sprintf("bank (%d) is smaller than requested batteries (%d)", len(bank), batteries)) + } + var joltage uint + var i int + for j := batteries - 1; j >= 0; j-- { + k := maxIndex(bank[i : len(bank)-j]) + k += i + i = k + 1 + joltage += uint(bank[k]) * uint(math.Pow10(j)) + } + return joltage +} + +func maxIndex[S ~[]E, E cmp.Ordered](x S) int { + if len(x) < 1 { + panic("maxIndex: empty list") + } + i := 0 + for j := 1; j < len(x); j++ { + if x[j] > x[i] { + i = j + } + } + return i +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3314e41 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module git.bernotas.lt/sewiti/advent-of-code-2025 + +go 1.25.5 + +require github.com/stretchr/testify v1.11.1 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c4c1710 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=