# -*- coding: utf-8 -*- from abc import ABC from dataclasses import dataclass, field from enum import Enum from typing import Set, List, Optional, Tuple, Dict, Iterator, Any from aoc import BaseAssignment class Action(Enum): ls = "ls" cd = "cd" @dataclass(kw_only=True) class Command: action: Tuple[Action, List[str]] output: Optional[List[str]] @classmethod def from_input(cls, input: str) -> "Command": action, *args = input[2:].split(" ") return Command(action=(Action(action), args), output=[]) @dataclass(kw_only=True) class Inode(ABC): name: str parent: "Folder" = None size: int = NotImplemented @dataclass(kw_only=True) class Folder(Inode): children: Dict[str, Inode] = field(default_factory=dict) @property def size(self) -> int: return sum([inode.size for inode in self.children.values()]) @size.setter def size(self, value: int): pass def __iter__(self) -> Iterator[Inode]: yield self for inode in self.children.values(): if isinstance(inode, Folder): for item in iter(inode): yield item else: yield inode @dataclass(kw_only=True) class File(Inode): pass @dataclass class FileSystem(Folder): pwd: Folder = None capacity = 70000000 @classmethod def from_commands(cls, commands: List[Command]) -> "FileSystem": file_system = cls(name="/") for command in commands: file_system.parse_command(command) return file_system def __post_init__(self): self.pwd = self def parse_command(self, command: Command): match command.action: case [Action.ls, _]: self.create_inodes(command.output or []) case [Action.cd, arguments]: self.change_directory(*arguments) def create_inodes(self, inodes: List[str]): for item in inodes: size, name = item.split(" ") inode = ( Folder(name=name, parent=self.pwd) if size == "dir" else File(name=name, parent=self.pwd, size=int(size)) ) if name in self.pwd.children: raise Exception(f"File/Folder already exists {name}") self.pwd.children[name] = inode def change_directory(self, name: str): match name: case "..": new_dir = self.pwd.parent case "/": new_dir = self case name: new_dir = self.pwd.children.get(name) if new_dir is None or isinstance(new_dir, File): raise Exception(f"No such folder {name}") self.pwd = new_dir @property def free_space(self): return self.capacity - self.size class Assignment(BaseAssignment, ABC): @staticmethod def parse_terminal_output(output: Iterator[str]) -> List[Command]: commands = [] command = None for item in output: if item.startswith("$"): if command is not None: commands.append(command) command = Command.from_input(item) else: command.output.append(item) if command is not None: commands.append(command) return commands class AssignmentOne(Assignment): example_result = 95437 def run(self, input: Iterator) -> Any: commands = self.parse_terminal_output(input) file_system = FileSystem.from_commands(commands) items = [ inode.size for inode in file_system if isinstance(inode, Folder) and inode.name != "/" and inode.size <= 100000 ] return sum(items) class AssignmentTwo(Assignment): example_result = 24933642 def run(self, input: Iterator) -> Any: commands = self.parse_terminal_output(input) file_system = FileSystem.from_commands(commands) update_size = 30000000 space_to_free = update_size - file_system.free_space eligible_for_deletion = sorted( [ inode for inode in file_system if isinstance(inode, Folder) and inode.size >= space_to_free ], key=lambda node: node.size, ) return eligible_for_deletion[0].size