Stream Video
Code
(Note: this is updated slightly from stream’s code. I split the part1/part2 solutions as I have done on previous days. It’s the same algorithm as streamed, just a slight refactor).
package aoc
import (
"fmt"
"io"
"sort"
"strconv"
"strings"
"mono3/advent/lib/lineio"
"mono3/common/log"
)
type bySize []*entry
func (b bySize) Len() int { return len(b) }
func (b bySize) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
func (b bySize) Less(i, j int) bool { return b[i].sz < b[j].sz }
type entry struct {
name string
sz int
c []*entry
p *entry
}
func (e *entry) dump(w io.Writer, indent int) {
for i := 0; i < indent; i++ {
w.Write([]byte(" "))
}
t := "file"
if len(e.c) > 0 {
t = "dir"
}
fmt.Fprintf(w, "- %s (%s, size=%d)\n", e.name, t, e.sz)
for _, child := range e.c {
child.dump(w, indent+2)
}
}
func (e *entry) String() string {
var sb strings.Builder
e.dump(&sb, 0)
return sb.String()
}
func (e *entry) GetSize() int {
if e.sz > 0 {
return e.sz
}
for _, child := range e.c {
e.sz += child.GetSize()
}
return e.sz
}
func (e *entry) GetChild(name string) *entry {
for _, child := range e.c {
if child.name == name {
return child
}
}
return nil
}
func (e *entry) AddChild(child *entry) {
e.c = append(e.c, child)
}
func (e *entry) walk(fun func(*entry)) {
fun(e)
for _, child := range e.c {
child.walk(fun)
}
}
// Each part gets the same input with reader pointing at the start of input.
var PuzzleParts = []func(r io.Reader) error{
PuzzlePart1,
PuzzlePart2,
}
func PuzzlePart1(r io.Reader) error {
return solveit(r, 1, func(root *entry) int {
max := 100000
sum := 0
root.walk(func(e *entry) {
if len(e.c) > 0 && e.sz < max {
sum += e.sz
}
})
return sum
})
}
func PuzzlePart2(r io.Reader) error {
return solveit(r, 2, func(root *entry) int {
needSpace := 30000000
freeSpace := 70000000 - root.GetSize()
if freeSpace < needSpace {
log.Infof("Still need %d freed", needSpace-freeSpace)
// find smallest directory that is at least needSpace-freeSpace
// in size.
var candidates []*entry
root.walk(func(e *entry) {
if len(e.c) == 0 {
return
}
if e.sz >= needSpace-freeSpace {
candidates = append(candidates, e)
}
})
log.Infof("Candidates:")
for _, c := range candidates {
log.Infof("\t%s sz %d", c.name, c.sz)
}
sort.Sort(bySize(candidates))
selected := candidates[0]
log.Infof("Selected: name=%s", selected.name)
return selected.sz
}
return 0
})
}
func solveit(r io.Reader, part int, fun func(root *entry) int) error {
log.Info("Running")
root := &entry{
name: "/",
sz: 0,
}
var cur *entry
if err := lineio.ForEach(r, func(ln int, line string) error {
if strings.HasPrefix(line, "$ ") {
// Is a command.
parts := strings.SplitN(line[2:], " ", 2)
cmd := parts[0]
var arg string
if len(parts) >= 2 {
arg = parts[1]
}
switch cmd {
case "cd":
// arg is either: filename or ".." or "/"
if arg == "/" {
cur = root
} else if arg == ".." {
if cur != nil && cur.p != nil {
cur = cur.p
}
} else if cur != nil {
child := cur.GetChild(arg)
cur = child
} else {
return fmt.Errorf("cur is nil and this is bad")
}
case "ls":
// Do nothing.
}
} else {
// Is command output, and "ls" is the only command that
// generates output.
parts := strings.SplitN(line, " ", 2)
filename := parts[1]
if cur == nil {
return fmt.Errorf("cur is nil this is bad.")
}
if parts[0] == "dir" {
cur.AddChild(&entry{
name: filename,
sz: 0,
c: nil,
p: cur,
})
} else {
// parts[0] is a size, parse that int.
sz, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil {
return fmt.Errorf("err parsing size of %s: %w", filename, err)
}
cur.AddChild(&entry{
name: filename,
sz: int(sz),
c: nil,
p: cur,
})
}
}
return nil
}); err != nil {
return err
}
root.GetSize()
w := log.Writer()
w.Write([]byte(root.String()))
w.Close()
result := fun(root)
log.Infof("Part %d result: %d", part, result)
return nil
}