Aetherus
Advent of Code 2022 - Day 7
Today’s challenge is quite interesting. I ended up using Zipper to solve this problem. Maybe I overengineered quite a bit.
The data structure
defmodule FSNode do
@moduledoc """
A node representing either a file or a dir.
"""
@type filename :: String.t
@type t :: %__MODULE__{
dir?: boolean,
size: non_neg_integer,
children: %{optional(filename) => t}
}
defstruct dir?: false, size: 0, children: %{}
@doc """
Create a node representing an empty dir.
"""
@spec dir() :: t
def dir do
%__MODULE__{dir?: true}
end
@doc """
Create a node representing a file of a specific size.
"""
@spec file(non_neg_integer) :: t
def file(size) do
%__MODULE__{dir?: false, size: size}
end
@doc """
Adds a child to the current node if the current node is a dir.
"""
@spec add_child!(current_node :: t, filename, child :: t) :: t
def add_child!(%__MODULE__{dir?: true} = curr, name, %__MODULE__{} = child) do
%{curr | size: curr.size + child.size , children: Map.put(curr.children, name, child)}
end
def add_child!(%__MODULE__{dir?: false}, _, _) do
raise ArgumentError, "Can't add children to a file."
end
@doc """
Pop the child of specific filename from the current node.
"""
@spec pop_child!(dir :: t, filename) :: {child :: t, dir_without_child :: t}
def pop_child!(%__MODULE__{dir?: true} = curr, name) do
child = Map.fetch!(curr.children, name)
{child, %{curr | children: Map.delete(curr.children, name), size: curr.size - child.size}}
end
def pop_child!(%__MODULE__{dir?: false}, _name) do
raise ArgumentError, "File has no children."
end
end
The zipper
defmodule FSZipper do
@moduledoc """
Zipper of FSNode tree.
"""
@opaque t :: {
focus :: FSNode.t,
trail :: [{FSNode.t, FSNode.filename}]
}
@spec from_tree(FSNode.t) :: t
def from_tree(tree) do
{tree, []}
end
@spec cd(t, FSNode.filename) :: t
def cd({%FSNode{dir?: true} = cwd, [{parent, name} | t]}, "..") do
{FSNode.add_child!(parent, name, cwd), t}
end
def cd({%FSNode{dir?: true} = cwd, trail}, name) do
{%FSNode{dir?: true} = child, cwd} = FSNode.pop_child!(cwd, name)
{child, [{cwd, name} | trail]}
end
@spec add_child(t, FSNode.filename, FSNode.t) :: t
def add_child({%FSNode{dir?: true} = cwd, trail}, name, %FSNode{} = node) do
{FSNode.add_child!(cwd, name, node), trail}
end
@spec to_tree(t) :: FSNode.t
def to_tree({node, []}), do: node
def to_tree(zipper), do: zipper |> cd("..") |> to_tree()
end
The parser
defmodule FSParser do
@spec parse([String.t]) :: FSNode.t
def parse(lines) do
# `lines` should not contain \n and the end of each line.
FSNode.dir()
# 'Cuz there's no `dir /` in the input,
# but there is `$ cd /`,
# I just put it there by default.
|> FSNode.add_child!("/", FSNode.dir())
|> FSZipper.from_tree()
|> parse(lines)
|> FSZipper.to_tree()
end
@spec parse(FSZipper.t, [String.t]) :: FSZipper.t
defp parse(zipper, []), do: zipper
defp parse(zipper, [line | rest]) do
case parse_line(line) do
{:dir, name} ->
zipper
|> FSZipper.add_child(name, FSNode.dir())
|> parse(rest)
{:file, name, size} ->
zipper
|> FSZipper.add_child(name, FSNode.file(size))
|> parse(rest)
{:cd, dir} ->
zipper
|> FSZipper.cd(dir)
|> parse(rest)
_ ->
parse(zipper, rest)
end
end
@spec parse_line(String.t) ::
:ls |
{:cd, FSNode.filename} |
{:dir, FSNode.filename} |
{:file, FSNode.filename, size :: non_neg_integer} |
:garbage
defp parse_line("$ ls"), do: :ls
defp parse_line("$ cd " <> dir), do: {:cd, dir}
defp parse_line("dir " <> name), do: {:dir, name}
defp parse_line(<<char, _::binary>> = line) when char in ?1..?9 do
[size, name] = String.split(line, " ", parts: 2, trim: true)
{:file, name, String.to_integer(size)}
end
defp parse_line(_), do: :garbage
end
The solution part
defmodule Day07 do
@spec part1([String.t]) :: non_neg_integer
def part1(input) do
input
|> FSParser.parse()
|> filter1([])
|> Enum.map(& &1.size)
|> Enum.sum()
end
@total_capacity 70000000
@target_free_capacity 30000000
@spec part2([String.t]) :: non_neg_integer
def part2(input) do
fs = FSParser.parse(input)
size_to_free = fs.size - (@total_capacity - @target_free_capacity)
fs
|> filter2(size_to_free, [])
|> Enum.map(& &1.size)
|> Enum.min()
end
@cap 100000
defp filter1(%FSNode{dir?: true, size: size, children: children}, acc) when size > @cap do
children
|> Map.values()
|> Enum.filter(& &1.dir?)
|> Enum.reduce(acc, &filter1/2)
end
defp filter1(%FSNode{dir?: true} = node, acc) do
node.children
|> Map.values()
|> Enum.filter(& &1.dir?)
|> Enum.reduce([node | acc], &filter1/2)
end
defp filter1(%FSNode{dir?: false}, acc), do: acc
defp filter2(%FSNode{dir?: false}, _size_to_free, acc), do: acc
defp filter2(%FSNode{dir?: true, size: size}, size_to_free, acc)
when size < size_to_free,
do: acc
defp filter2(%FSNode{dir?: true} = dir, size_to_free, acc) do
case Enum.filter(Map.values(dir.children), & &1.size >= size_to_free) do
[] -> [dir | acc]
children -> Enum.reduce(children, acc, &filter2(&1, size_to_free, &2))
end
end
end
Most Liked
LostKobrakai
Reading part 1 this morning I didn’t want to deal with trees or whatever, so I just didn’t bother. Now in the evening I noticed I don’t need to deal with half of the stuff in the input. Just go the S3 route of a KV storage of {path, size} and the rest can be computed from there. I did a second pass to calculate all the directory sizes, which made answering the puzzle questions rather simple operations. In the end I was really surprised by how expressive the resulting code turned out to be.
Solution
defmodule Day7 do
defstruct prefix: nil, files: %{}, dirs: %{}
def parse_files(text) do
state =
text
|> String.split("\n")
|> Enum.reject(&(&1 == ""))
|> Enum.reduce(%__MODULE__{}, fn
"$ cd /", state ->
%__MODULE__{state | prefix: "/"}
"$ cd ..", state ->
%__MODULE__{state | prefix: Path.dirname(state.prefix)}
"$ cd " <> dir, state ->
%__MODULE__{state | prefix: Path.join(state.prefix, dir)}
"$ ls", state ->
state
"dir " <> _, state ->
state
file_info, state ->
[size, filename] = String.split(file_info, " ", parts: 2)
files = Map.put(state.files, Path.join(state.prefix, filename), String.to_integer(size))
%__MODULE__{state | files: files}
end)
Enum.reduce(state.files, state, fn {path, size}, state ->
path
|> stream_of_parent_directories()
|> Enum.reduce(state, fn dir, state ->
update_in(state, [Access.key!(:dirs), Access.key(dir, 0)], &(&1 + size))
end)
end)
end
defp stream_of_parent_directories(path) do
Stream.unfold(path, fn
nil ->
nil
path ->
case Path.dirname(path) do
"/" -> {"/", nil}
dir -> {dir, dir}
end
end)
end
def find_sum_of_small_folders(text) do
text
|> parse_files()
|> Map.fetch!(:dirs)
|> Map.values()
|> Enum.filter(fn size -> size < 100_000 end)
|> Enum.sum()
end
def find_size_of_smallest_directory_to_delete(text) do
dirs =
text
|> parse_files()
|> Map.fetch!(:dirs)
total_space = 70_000_000
total_used = dirs["/"]
total_free = total_space - total_used
size_update = 30_000_000
size_to_be_freed = size_update - total_free
dirs
|> Map.values()
|> Enum.filter(fn size -> size >= size_to_be_freed end)
|> Enum.min()
end
end
mruoss
I went with Stream.transform/5 keeping track of a stack of directory_sizes.
adamu
This is the first day that broke me ![]()
Not happy with my answer as I’ve clearly over-complicated it and will definitely be reading through everyone else’s. It does complete in less that 1ms though.
I did:
- Build up the file tree as a
%{path => node}map - Depth-first search to find the directories in order of fewest descendants
- Walk the list from 2, updating the dir sizes
https://git.adamu.jp/adam/AdventOfCode/src/branch/main/2022/day7.exs
Popular in Challenges
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance








