diff --git a/puzzles/solutions/2022/d07/consts.py b/puzzles/solutions/2022/d07/consts.py new file mode 100644 index 00000000..191b3401 --- /dev/null +++ b/puzzles/solutions/2022/d07/consts.py @@ -0,0 +1,9 @@ +COMMANDS_SEPARATOR = "$" +DIRECTORY_SYMBOL = "dir" +PARENT_DIRECTORY_SYMBOL = ".." +ROOT_DIRECTORY_SYMBOL = "/" +CHANGE_DIRECTORY_COMMAND = "cd" +LIST_CONTENTS_COMMAND = "ls" +SMALL_DIRECTORY_SIZE_THRESHOLD = 100_000 +TOTAL_DISK_SPACE = 70_000_000 +REQUIRED_UNUSED_SPACE = 30_000_000 diff --git a/puzzles/solutions/2022/d07/directory.py b/puzzles/solutions/2022/d07/directory.py new file mode 100644 index 00000000..4f8c3de0 --- /dev/null +++ b/puzzles/solutions/2022/d07/directory.py @@ -0,0 +1,64 @@ +from typing import Self + +from file import File + + +class Directory: + """A directory which can store files and subdirectories.""" + + def __init__(self, name: str, parent: Self = None): + """ + Instantiate a directory. + :param name: name of the directory + :param parent: parent directory, if exists + """ + self.name = name + self.parent = parent + self._files = [] + self._subdirectories = [] + + def __str__(self): + """ + :return: string representation of the directory, including its name, size, files, and subdirectories + """ + files = ", ".join(file.name for file in self.files) or "no files" + subdirectories = ( + ", ".join(subdirectory.name for subdirectory in self.subdirectories) + or "no subdirs" + ) + return f"{self.name} ({self.size}): {files}; {subdirectories}" + + def add_file(self, file: File) -> None: + """ + :param file: file to add to the directory + """ + self._files.append(file) + + def add_subdirectory(self, subdirectory: Self) -> None: + """ + :param subdirectory: subdirectory to add to the directory + """ + self._subdirectories.append(subdirectory) + + @property + def files(self) -> tuple[File, ...]: + """ + :return: files in the directory + """ + return tuple(self._files) + + @property + def subdirectories(self) -> tuple[Self, ...]: + """ + :return: subdirectories of the directory + """ + return tuple(self._subdirectories) + + @property + def size(self) -> int: + """ + :return: size of the files and subdirectories in the directory + """ + return sum(file.size for file in self.files) + sum( + subdirectory.size for subdirectory in self.subdirectories + ) diff --git a/puzzles/solutions/2022/d07/file.py b/puzzles/solutions/2022/d07/file.py new file mode 100644 index 00000000..c3a9dbcd --- /dev/null +++ b/puzzles/solutions/2022/d07/file.py @@ -0,0 +1,9 @@ +import dataclasses + + +@dataclasses.dataclass +class File: + """A file with a name and size.""" + + name: str + size: int diff --git a/puzzles/solutions/2022/d07/input_parsing.py b/puzzles/solutions/2022/d07/input_parsing.py new file mode 100644 index 00000000..abd206c6 --- /dev/null +++ b/puzzles/solutions/2022/d07/input_parsing.py @@ -0,0 +1,33 @@ +import consts +from directory import Directory +from file import File + + +def get_next_directory(current_directory: Directory, command: str) -> Directory: + """ + :param current_directory: current directory + :param command: `cd` command to parse + :return: `cd` command object + """ + destination = command.split()[1] + if destination == consts.PARENT_DIRECTORY_SYMBOL: + return current_directory.parent + for subdirectory in current_directory.subdirectories: + if subdirectory.name == destination: + return subdirectory + + +def handle_ls_command(directory: Directory, command: str) -> None: + """ + Add contents to the given directory, according to the given `ls` command output. + :param command: `ls` command output + :param directory: directory to add contents to + """ + contents = command.splitlines()[1:] + for content in contents: + if content.startswith(consts.DIRECTORY_SYMBOL): + directory_name = content.split()[1] + directory.add_subdirectory(Directory(directory_name, directory)) + else: + size, filename = content.split() + directory.add_file(File(filename, int(size))) diff --git a/puzzles/solutions/2022/d07/p1.py b/puzzles/solutions/2022/d07/p1.py new file mode 100644 index 00000000..3ca7a829 --- /dev/null +++ b/puzzles/solutions/2022/d07/p1.py @@ -0,0 +1,81 @@ +import sys +from typing import Sequence + +import consts +import input_parsing +from directory import Directory + + +def get_commands(input_text: str) -> tuple[str, ...]: + """ + :param input_text: puzzle input + :return: tuple of commands from the terminal output + """ + # Remove the first `$` so we don't have an empty string after running `split()`. + input_text = input_text[1:] + # First command is `cd /` which we can skip. + commands = input_text.split(consts.COMMANDS_SEPARATOR)[1:] + return tuple(command.strip() for command in commands) + + +def build_filesystem(commands: Sequence[str]) -> Directory: + """ + Build the filesystem according to the given commands and return the root directory. + :param commands: commands output + :return: root directory + """ + root = Directory(consts.ROOT_DIRECTORY_SYMBOL) + current_directory = root + for command in commands: + if command.startswith(consts.CHANGE_DIRECTORY_COMMAND): + current_directory = input_parsing.get_next_directory( + current_directory, command + ) + elif command.startswith(consts.LIST_CONTENTS_COMMAND): + input_parsing.handle_ls_command(current_directory, command) + + return root + + +def get_all_subdirectories( + directory: Directory, subdirectories=None +) -> list[Directory]: + """ + :param directory: directory to traverse + :param subdirectories: list to add subdirectories to + :return: subdirectories under the given directory + """ + if subdirectories is None: + subdirectories = list() + subdirectories.append(directory) + for subdirectory in directory.subdirectories: + subdirectories.extend(get_all_subdirectories(subdirectory)) + return subdirectories + + +def get_filesystem(terminal_output: str) -> list[Directory]: + """ + :param terminal_output: puzzle input + :return: filesystem built according to the terminal output + """ + commands = get_commands(terminal_output) + root = build_filesystem(commands) + filesystem = get_all_subdirectories(root) + return filesystem + + +def get_answer(input_text: str): + """Return the sum of the total sizes of all the directories with a total size of at most 100000.""" + filesystem = get_filesystem(input_text) + return sum( + directory.size + for directory in filesystem + if directory.size < consts.SMALL_DIRECTORY_SIZE_THRESHOLD + ) + + +if __name__ == "__main__": + try: + print(get_answer(sys.argv[1])) + except IndexError: + pass # Don't crash if no input was passed through command line arguments. diff --git a/puzzles/solutions/2022/d07/p2.py b/puzzles/solutions/2022/d07/p2.py new file mode 100644 index 00000000..18aa3959 --- /dev/null +++ b/puzzles/solutions/2022/d07/p2.py @@ -0,0 +1,28 @@ +import sys + +import consts +import p1 + + +def get_answer(input_text: str): + """Return total size of the smallest directory that, if deleted, would free up enough space on the filesystem.""" + filesystem = p1.get_filesystem(input_text) + root = filesystem[0] + total_used_space = root.size + unused_space_needed = consts.REQUIRED_UNUSED_SPACE - ( + consts.TOTAL_DISK_SPACE - total_used_space + ) + candidate_directories = ( + directory for directory in filesystem if directory.size >= unused_space_needed + ) + directory_to_delete = min( + candidate_directories, key=lambda directory: directory.size + ) + return directory_to_delete.size + + +if __name__ == "__main__": + try: + print(get_answer(sys.argv[1])) + except IndexError: + pass # Don't crash if no input was passed through command line arguments.