Coverage for src\aoc2022\days\day7.py: 100%

78 statements  

« prev     ^ index     » next       coverage.py v7.0.0, created at 2022-12-23 05:19 +0000

1from __future__ import annotations 

2 

3from typing import Dict, Generator, List, Optional 

4 

5from src.aoc2022 import utils 

6 

7 

8class DirectoryNode: 

9 """ 

10 Class representing a single directory 

11 

12 :param dirname: the name of the directory 

13 :type dirname: str 

14 

15 :param level: the depth of the directory in the tree structure 

16 :type level: int 

17 """ 

18 

19 def __init__(self, dirname: str, level: int): 

20 self.files: List[FileNode] = [] 

21 self.children: Dict[str, DirectoryNode] = {} 

22 self.dirname: str = dirname 

23 self.parent: Optional[DirectoryNode] = None 

24 self.level: int = level 

25 self.size: int = 0 

26 

27 def add_child(self, child: DirectoryNode): 

28 """ 

29 Add a child directory to this directory 

30 

31 :param child: child directory to add 

32 :type child: DirectoryNode 

33 """ 

34 if child.dirname not in self.children: 

35 self.children[child.dirname] = child 

36 child.parent = self 

37 

38 def get_child(self, dirname: str): 

39 """ 

40 Get a child directory by name 

41 

42 :param dirname: name of child directory to retrieve 

43 :type dirname: str 

44 """ 

45 return self.children[dirname] 

46 

47 def update_size(self, updated_size: int): 

48 """ 

49 Update the size of this directory 

50 

51 :param updated_size: number of bytes to add to current size 

52 :type updated_size: int 

53 """ 

54 self.size += updated_size 

55 if self.parent is not None: 

56 self.parent.update_size(updated_size) 

57 

58 def add_file(self, file: FileNode): 

59 """ 

60 Add a child file to this directory 

61 

62 :param child: child file to add 

63 :type child: FileNode 

64 """ 

65 self.files.append(file) 

66 self.update_size(file.filesize) 

67 

68 def __repr__(self) -> str: 

69 

70 num_spaces_per_tab = 4 

71 prefix = " " * self.level * num_spaces_per_tab 

72 

73 repr_str = f"DIR: {self.dirname} ({self.size})\n" 

74 if len(self.children) > 0: 

75 repr_str += f"{prefix}subdirs:\n" 

76 for k, v in self.children.items(): 

77 repr_str += f"{prefix}- {v}\n" 

78 if len(self.files) > 0: 

79 repr_str += f"{prefix}files:\n" 

80 for f in self.files: 

81 repr_str += f"{prefix}- {f}\n" 

82 return repr_str 

83 

84 

85class FileNode: 

86 """ 

87 Class representing a single file 

88 

89 :param filesize: the size of the file in bytes 

90 :type filesize: int 

91 

92 :param filename: the name of the file 

93 :type filename: str 

94 """ 

95 

96 def __init__(self, filesize: int, filename: str): 

97 self.filesize: int = filesize 

98 self.filename: str = filename 

99 

100 def __repr__(self) -> str: 

101 return f"FILE : {self.filename} {self.filesize}" 

102 

103 

104def parse_data_to_tree(raw_data: str) -> DirectoryNode: 

105 """ 

106 Parse the raw data into a tree-structure representing the nodes and files 

107 

108 :param raw_data: Raw data containing commands 

109 :type raw_data: str 

110 

111 :return: Root of the directory tree 

112 :rtype: DirectoryNode 

113 """ 

114 # get all commands, skip first line for root dir 

115 lines = raw_data.split("\n")[1:] 

116 

117 root = DirectoryNode("/", 0) 

118 current = root 

119 

120 # Type ignores since linting tools do not like structural pattern matching 

121 for line in lines: 

122 match line.split(" "): # noqa 

123 case ["$", "cd", ".."]: # noqa: E211 

124 current = current.parent # type: ignore 

125 case ["$", "cd", "/"]: # noqa: E211 

126 current = root 

127 case ["$", "cd", dirname]: # noqa: E211 

128 current = current.get_child(dirname) # noqa: F821 

129 case ["$", "ls"]: # noqa: E211 

130 pass 

131 case ["dir", str(dirname)]: # noqa: E211 

132 level = current.level + 1 

133 child_node = DirectoryNode(dirname, level) # noqa: F821 

134 current.add_child(child_node) # noqa: F821 

135 case [str(num_bytes), str(filename)]: # noqa: E211 

136 file_node = FileNode(int(num_bytes), filename) # noqa: F821 

137 current.add_file(file_node) 

138 return root 

139 

140 

141def tree_iterator(node: DirectoryNode) -> Generator[DirectoryNode, None, None]: 

142 """ 

143 Produces a generator for traversing the directory/file tree 

144 

145 :param node: node to traverse 

146 :type node: DirectoryNode 

147 

148 :return: Generator for traversing the directory/file tree 

149 :rtype: Generator[DirectoryNode] 

150 """ 

151 yield node 

152 for k, v in node.children.items(): 

153 yield from tree_iterator(v) 

154 

155 

156def get_directories_under_size(root: DirectoryNode, max_size: int) -> int: 

157 """ 

158 Calculates the sum of directory sizes for all directories with a max 

159 size under the given threshold 

160 

161 :param root: root node for the directory/file tree 

162 :type root: DirectoryNode 

163 

164 :param max_size: maximum allowable size for a directory to be kept 

165 :type max_size: int 

166 

167 :return: sum of total sizes for directories smaller than max_size 

168 :rtype: int 

169 """ 

170 valid_dirs = [d for d in tree_iterator(root) if d.size <= max_size] 

171 return sum([d.size for d in valid_dirs]) 

172 

173 

174def get_smallest_directory_to_delete( 

175 root: DirectoryNode, total_space: int, needed_space: int 

176) -> int: 

177 """ 

178 Find the smallest directory to delete to free the needed space 

179 

180 :param root: root node for the directory/file tree 

181 :type root: DirectoryNode 

182 

183 :param total_space: total space of the storage device 

184 :type total_space: int 

185 

186 :param needed_space: amount of free space required 

187 :type needed_space: int 

188 

189 :return: size of the directory to delete 

190 :rtype: int 

191 """ 

192 actual_capacity = total_space - root.size 

193 missing_capacity = needed_space - actual_capacity 

194 node_iter = tree_iterator(root) 

195 delete_candidates = [d for d in node_iter if d.size >= missing_capacity] 

196 return min([d.size for d in delete_candidates]) 

197 

198 

199if __name__ == "__main__": # pragma: no cover 

200 

201 data = utils.get_raw_data("./src/aoc2022/data/day7.txt") 

202 

203 tree_root = parse_data_to_tree(data) 

204 

205 res = get_directories_under_size(tree_root, 100_000) 

206 

207 print(f"The total size of directories under 100_000 bytes is {res}") 

208 

209 res = get_smallest_directory_to_delete(tree_root, 70_000_000, 30_000_000) 

210 

211 print(f"The size of the smallest directory that can be deleted is {res}")