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
Trending in Challenges
Other Trending Topics
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve.
They are GUI (Emerge) and State management (S...
New
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
I just stumbled on a newly redesigned elixir-lang.org. :tada: It looks like @Software_Mansion did the work, and I think it is generally a...
New
@hugobarauna and I (Alex Koutmos) have been hard at work on writing a book on Nerves that takes you from simply blinking LEDs to building...
New
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
- #elixirconf-us
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #performance
- #security










First Post!
stefanluptak
I feel I went a very similar route. Tree with sizes already calculated and then a custom filter function on top of it.
LiveBook with solution
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
mruoss
I went with
Stream.transform/5keeping track of a stack of directory_sizes.Day 7 Livebook
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:
%{path => node}maphttps://git.adamu.jp/adam/AdventOfCode/src/branch/main/2022/day7.exs
Last Post!
vfrangipani
You should consider the possibility that a directory has subdirectories with the same name.