Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Day 7: No Space Left On Device

2022/elixir/day-07.livemd

Day 7: No Space Left On Device

Mix.install([:kino])

Input

input = Kino.Input.textarea("Input")

Part 1

defmodule FileNode do
  defstruct [:name, :type, :path, :parent, size: 0]

  @type type :: :file | :dir
end

defmodule FileSystem do
  defstruct nodes: %{}, cwd: "/"

  def new() do
    root_node = %FileNode{name: "/", path: "/", type: :dir}
    %__MODULE__{nodes: %{"/" => root_node}}
  end

  def cd(self, path = "/") do
    %__MODULE__{self | cwd: path}
  end

  def cd(self, "..") do
    %__MODULE__{self | cwd: Path.dirname(self.cwd)}
  end

  def cd(self, path) do
    %__MODULE__{self | cwd: Path.join(self.cwd, path)}
  end

  def add(self, node = %FileNode{}) do
    path = Path.join(self.cwd, node.name)
    nodes = Map.put(self.nodes, path, %FileNode{node | path: path, parent: self.cwd})
    %__MODULE__{self | nodes: nodes}
  end

  def size(self, path \\ "/")

  def size(self, path) when is_bitstring(path) do
    size(self, self.nodes[path])
  end

  def size(_, node = %FileNode{type: :file}) do
    node.size
  end

  def size(self, node = %FileNode{type: :dir}) do
    sub_nodes(self, node.path)
    |> Enum.map(&size(self, &1))
    |> Enum.sum()
  end

  def nodes(self) do
    Map.values(self.nodes)
  end

  def sub_nodes(self, path) do
    Enum.filter(
      nodes(self),
      fn node -> node.path != path && Path.dirname(node.path) == path end
    )
  end
end
defmodule Parser do
  def parse("$ cd " <> path) do
    {:cd, path: path}
  end

  def parse("$ ls") do
    {:ls, []}
  end

  def parse("dir " <> name) do
    {:file, %FileNode{name: name, type: :dir}}
  end

  def parse(s) do
    [size, name] = String.split(s, " ")
    {:file, %FileNode{name: name, type: :file, size: String.to_integer(size)}}
  end
end
defmodule Executer do
  def commands(input) do
    input
    |> Kino.Input.read()
    |> String.split("\n")
    |> Enum.map(fn s -> Parser.parse(s) end)
  end

  def execute(commands) when is_list(commands) do
    Enum.reduce(commands, FileSystem.new(), fn cmd, fs -> Map.merge(fs, execute(fs, cmd)) end)
  end

  def execute(fs, command) do
    case command do
      {:cd, path: path} -> FileSystem.cd(fs, path)
      {:file, file_node} -> FileSystem.add(fs, file_node)
      {:ls, []} -> fs
    end
  end
end
input
|> Executer.commands()
|> Executer.execute()
|> then(fn fs ->
  fs
  |> FileSystem.nodes()
  |> Enum.filter(fn node -> node.type == :dir end)
  |> Enum.map(&amp;FileSystem.size(fs, &amp;1))
  |> Enum.filter(&amp;(&amp;1 <= 100_000))
  |> Enum.sum()
end)

Part 2

fs =
  input
  |> Executer.commands()
  |> Executer.execute()

max_size = 70_000_000
require_size = 30_000_000
total_size = FileSystem.size(fs)
free_size = max_size - total_size
delete_size = require_size - free_size

fs
|> FileSystem.nodes()
|> Enum.filter(fn node -> node.type == :dir end)
|> Enum.map(&amp;FileSystem.size(fs, &amp;1))
|> Enum.filter(&amp;(&amp;1 >= delete_size))
|> Enum.sort()
|> Enum.at(0)