Aetherus
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
Edit: 2026 May 15 - This post is archived.
Mob is alive!!
Main docs: mob v0.7.11 — Documentation
A bit of explanation for the slightly c...
New
I am happy to introduce the very α version of the new programming language compiled to BEAM.
Welcome Cure.
It has literally three kille...
New
Hobbes is a low-level distributed database for the Elixir programming language.
Hobbes provides a simple, safe, and scalable storage lay...
New
A little off-topic, but I feel like people here have a good head on their shoulders.
I used to be quite good at making software. Was luc...
New
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ai
- #ecto-query
- #elixirconf-us
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #elixirconf-eu
- #api
- #forms
- #metaprogramming
- #hex










Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
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
mruoss
I went with
Stream.transform/5keeping track of a stack of directory_sizes.Day 7 Livebook
deadbeef
Also went the stack route.
I was wondering if there was a
Streamfunction that took anafter_fun. I’ll probably refactor to useStream.transform/5(rather than reading in the entire list and recursing over function)!https://github.com/ed-flanagan/advent-of-code-solutions-elixir/blob/main/lib/advent/y2022/d07.ex
mudasobwa
Oldie but goldie, my first Elixir library ever
Iteraptorcame to the rescue after parsing the tree.code-shoily
I first extracted the files, keeping the size and the parent directories, and then updated the directories by walking through those files.
https://github.com/code-shoily/advent_of_code/blob/master/lib/2022/day_07.ex
nallwhy
With simple recursion
https://github.com/nallwhy/advent-of-code/blob/main/2022/day_07.livemd
wasi0013
Pattern matching + Map of directory size.
https://github.com/wasi0013/advent_of_code/blob/master/lib/y2022/day_07.ex
stevensonmt
This one was a little more challenging until I remembered to use
:digraphto make life easier. Not totally happy with my parsing that required the full path as a node key, but I couldn’t think of another way to keep/a/efrom being overwritten by a possible/b/epath. Also not super happy about having to walk the graph to update the sizes with the sub-directory sizes but it ended up being efficient enough for this exercise. Also a couple lines of duplication between part1 and part2 that I could have turned into one helper function, but LOC would actually increase so I didn’t think it was worth it.https://github.com/stevensonmt/advent_of_code/blob/2022/2022/day7/lib/day7.ex
Very crude benchmarking, but with
:timer.tcparsing and solving part 1 was 3192 usec, part 2 was 3321 usec.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
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