Powered by AppSignal & Oban Pro

Day 7

2022/day7.livemd

Day 7

Mix.install([{:kino, "~>0.7.0"}])

Setup

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

Common

defmodule Day7 do
  defmodule Directory do
    defstruct directories: %{}, total_size: 0, file_sizes: %{}

    def new() do
      %Directory{}
    end

    def add_file(self, current_dir, file_name, file_size) do
      parsed_dir = current_dir |> String.split("/", trim: true)

      add_file_in(self, parsed_dir, file_name, file_size)
    end

    def add_directory(self, current_dir, dir_name) do
      parsed_dir =
        current_dir
        |> String.split("/", trim: true)

      add_directory_in(self, parsed_dir, dir_name)
    end

    # private methods
    defp add_file_in(self, [], file_name, file_size) do
      %{self | file_sizes: Map.put(self.file_sizes, file_name, file_size)}
    end

    defp add_file_in(self, location, file_name, file_size) do
      [dir | location_without_last] = location

      directory_to_change = self.directories[dir]

      %{
        self
        | total_size: 0,
          directories:
            Map.put(
              self.directories,
              dir,
              add_file_in(directory_to_change, location_without_last, file_name, file_size)
            )
      }
    end

    defp add_directory_in(self, [], dir_name) do
      %{self | directories: Map.put(self.directories, dir_name, Directory.new())}
    end

    defp add_directory_in(self, location, dir_name) do
      [dir | location_without_last] = location

      directory_to_change = self.directories[dir]

      %{
        self
        | directories:
            Map.put(
              self.directories,
              dir,
              add_directory_in(directory_to_change, location_without_last, dir_name)
            )
      }
    end
  end

  def process_command(fs_information, "$ ls") do
    # This is a noop
    fs_information
  end

  def process_command(%{:fs => fs}, "$ cd /") do
    # go to root directory
    %{current_dir: "/", fs: fs}
  end

  def process_command(%{:current_dir => dir, :fs => fs}, "$ cd ..") do
    # go up one directory
    new_dir =
      dir
      |> String.split(~r/\//, include_captures: true, trim: true)
      |> Enum.drop(-2)
      |> Enum.join("")

    %{current_dir: new_dir, fs: fs}
  end

  def process_command(%{:current_dir => dir, :fs => fs}, "$ cd " <> move_action) do
    # go inside a directory
    %{current_dir: dir <> move_action <> "/", fs: fs}
  end

  def process_command(%{:current_dir => dir, :fs => fs}, "dir " <> dir_name) do
    # This is a dir command...Adding the directory to the correct location
    new_fs = Directory.add_directory(fs, dir, dir_name)

    %{current_dir: dir, fs: new_fs}
  end

  def process_command(%{:current_dir => dir, :fs => fs}, file_info) do
    # This is a line with a file information
    [file_size, file_name] =
      file_info
      |> String.split(" ", trim: true)

    # new_fs = Directory.add_file(fs, dir, file_name, String.to_integer(file_size))
    new_fs = Directory.add_file(fs, dir, file_name, String.to_integer(file_size))
    %{current_dir: dir, fs: new_fs}
  end

  def calculate_size_for_directories(%{:current_dir => _, fs: fs}) do
    fs
    |> calculate_file_size()
  end

  def get_dir_sizes(fs) do
    [{"/", fs.total_size}] ++ get_directories_dir_sizes(fs.directories)
  end

  # Private methods
  def get_directories_dir_sizes(dir_map) when dir_map == %{} do
    []
  end

  def get_directories_dir_sizes(dir_map) do
    dir_map
    |> Enum.reduce([], fn {key, dir}, acc ->
      acc ++ [{key, dir.total_size}] ++ get_directories_dir_sizes(dir.directories)
    end)
  end

  defp calculate_file_size(fs) when fs == %{} do
    %{}
  end

  defp calculate_file_size(fs) do
    file_size =
      fs.file_sizes
      |> Enum.reduce(0, fn {_, size}, acc -> acc + size end)

    directories =
      fs.directories
      |> Enum.reduce(%{}, fn {dir_name, dir}, acc ->
        Map.put(acc, dir_name, calculate_file_size(dir))
      end)

    dir_size =
      directories
      |> Enum.reduce(0, fn {_, dir}, acc ->
        acc + dir.total_size
      end)

    %{fs | total_size: file_size + dir_size, directories: directories}
  end
end

Part 1

input
|> Kino.Input.read()
|> String.split("\n", trim: true)
|> Enum.reduce(%{current_dir: "/", fs: Day7.Directory.new()}, fn command, acc ->
  acc
  |> Day7.process_command(command)
end)
|> Day7.calculate_size_for_directories()
|> Day7.get_dir_sizes()
|> Enum.filter(fn {_, val} ->
  val <= 100_000
end)
|> Enum.map(fn {_, val} -> val end)
|> Enum.sum()

Part 2

Total Disk Size: 70.000.000

Needed unused Space: 30.000.000

dir_sizes =
  input
  |> Kino.Input.read()
  |> String.split("\n", trim: true)
  |> Enum.reduce(%{current_dir: "/", fs: Day7.Directory.new()}, fn command, acc ->
    acc
    |> Day7.process_command(command)
  end)
  |> Day7.calculate_size_for_directories()
  |> Day7.get_dir_sizes()

# Find used size by root directory
{_, size} =
  dir_sizes
  |> Enum.find(fn {dir, _} -> dir == "/" end)

space_total = 70_000_000
free_space_required = 30_000_000
free_space = 70_000_000 - size
space_to_free_up = free_space_required - free_space

dir_sizes
|> Enum.map(fn {_, size} -> size end)
|> Enum.filter(fn size -> size >= space_to_free_up end)
|> Enum.sort(fn size1, size2 -> size1 <= size2 end)
|> Enum.min()