|
| 1 | +// |
| 2 | +// ChallengeName.swift |
| 3 | +// |
| 4 | +// Created by Author on 2023-01-03. |
| 5 | +// |
| 6 | + |
| 7 | + |
| 8 | +import ArgumentParser |
| 9 | +import Foundation |
| 10 | +import Shared |
| 11 | + |
| 12 | + |
| 13 | +/** |
| 14 | + Day 3: No Space Left On Device |
| 15 | + |
| 16 | + # Part One |
| 17 | + |
| 18 | + You can hear birds chirping and raindrops hitting leaves as the expedition |
| 19 | + proceeds. Occasionally, you can even hear much louder sounds in the distance; |
| 20 | + how big do the animals get out here, anyway? |
| 21 | + |
| 22 | + The device the Elves gave you has problems with more than just its |
| 23 | + communication system. You try to run a system update: |
| 24 | + |
| 25 | + ``` |
| 26 | + $ system-update --please --pretty-please-with-sugar-on-top |
| 27 | + Error: No space left on device |
| 28 | + ``` |
| 29 | + |
| 30 | + Perhaps you can delete some files to make space for the update? |
| 31 | + |
| 32 | + You browse around the filesystem to assess the situation and save the |
| 33 | + resulting terminal output (your puzzle input). For example: |
| 34 | + |
| 35 | + ``` |
| 36 | + $ cd / |
| 37 | + $ ls |
| 38 | + dir a |
| 39 | + 14848514 b.txt |
| 40 | + 8504156 c.dat |
| 41 | + dir d |
| 42 | + $ cd a |
| 43 | + $ ls |
| 44 | + dir e |
| 45 | + 29116 f |
| 46 | + 2557 g |
| 47 | + 62596 h.lst |
| 48 | + $ cd e |
| 49 | + $ ls |
| 50 | + 584 i |
| 51 | + $ cd .. |
| 52 | + $ cd .. |
| 53 | + $ cd d |
| 54 | + $ ls |
| 55 | + 4060174 j |
| 56 | + 8033020 d.log |
| 57 | + 5626152 d.ext |
| 58 | + 7214296 k |
| 59 | + ``` |
| 60 | + |
| 61 | + The filesystem consists of a tree of files (plain data) and directories (which |
| 62 | + can contain other directories or files). The outermost directory iscalled |
| 63 | + `/`. You can navigate around the filesystem, moving into or out of |
| 64 | + directories and listing the contents of the directory you're currently in. |
| 65 | + |
| 66 | + Within the terminal output, lines that begin with `$` are commands you |
| 67 | + executed, very much like some modern computers: |
| 68 | + |
| 69 | + * `cd` means **change directory.** This changes which directory is the |
| 70 | + current directory, but the specific result depends on the argument: |
| 71 | + * `cd x` moves **in** one level: it looks in the current directory for the |
| 72 | + directory named `x` and makes it the current directory. |
| 73 | + * `cd ..` moves **out** one level: it finds the directory that contains the |
| 74 | + current directory, then makes that directory the current directory. |
| 75 | + * `cd /` switches the current directory to the outermost directory, `/`. |
| 76 | + * `ls` means list. It prints out all of the files and directories immediately |
| 77 | + contained by the current directory: |
| 78 | + * `123 abc` means that the current directory contains a file named |
| 79 | + `abc` with size `123`. |
| 80 | + * `dir xyz` means that the current directory contains a directory |
| 81 | + named `xyz`. |
| 82 | + |
| 83 | + Given the commands and output in the example above, you can determine that the |
| 84 | + filesystem looks visually like this: |
| 85 | + |
| 86 | + ``` |
| 87 | + - / (dir) |
| 88 | + - a (dir) |
| 89 | + - e (dir) |
| 90 | + - i (file, size=584) |
| 91 | + - f (file, size=29116) |
| 92 | + - g (file, size=2557) |
| 93 | + - h.lst (file, size=62596) |
| 94 | + - b.txt (file, size=14848514) |
| 95 | + - c.dat (file, size=8504156) |
| 96 | + - d (dir) |
| 97 | + - j (file, size=4060174) |
| 98 | + - d.log (file, size=8033020) |
| 99 | + - d.ext (file, size=5626152) |
| 100 | + - k (file, size=7214296) |
| 101 | + ``` |
| 102 | + |
| 103 | + Here, there are four directories: |
| 104 | + |
| 105 | + * `/` (the outermost directory), |
| 106 | + * `a` and `d` (which are in `/`), and |
| 107 | + * `e` (which is in a). |
| 108 | + |
| 109 | + These directories also contain files of various sizes. |
| 110 | + |
| 111 | + Since the disk is full, your first step should probably be to find directories |
| 112 | + that are good candidates for deletion. To do this, you need to determine |
| 113 | + the **total size** of each directory. The total size of a directory is the sum |
| 114 | + of the sizes of the files it contains, directly or indirectly. |
| 115 | + (Directories themselves do not count as having any intrinsic size.) |
| 116 | + |
| 117 | + The total sizes of the directories above can be found as follows: |
| 118 | + |
| 119 | + * The total size of directory `e` is `584` because it contains a single file `i` |
| 120 | + of size `584` and no other directories. |
| 121 | + * The directory `a` has total size `94853` because it contains files `f` |
| 122 | + (size `29116`), `g` (size `2557`), and `h.lst` (size `62596`), plus file `i` |
| 123 | + indirectly (`a` contains `e` which contains `i`). |
| 124 | + * Directory `d` has total size `24933642`. |
| 125 | + * As the outermost directory, `/` contains every file. Its total size is |
| 126 | + `48381165`, the sum of the size of every file. |
| 127 | + |
| 128 | + To begin, find all of the directories with a total size of `at most 100000`, |
| 129 | + then calculate the sum of their total sizes. In the example above, these |
| 130 | + directories are `a` and `e`; the sum of their total sizes is |
| 131 | + `95437` (94853 + 584). (As in this example, this process can count files |
| 132 | + more than once!) |
| 133 | + |
| 134 | + Find all of the directories with a total size of at most 100000. |
| 135 | + What is **the sum of the total sizes of those directories?** |
| 136 | + |
| 137 | + # Part Two |
| 138 | + |
| 139 | + Now, you're ready to choose a directory to delete. |
| 140 | + |
| 141 | + The total disk space available to the filesystem is `70000000`. To run the |
| 142 | + update, you need unused space of at least `30000000`. You need to find a |
| 143 | + directory you can delete that will **free up enough space** to run the update. |
| 144 | + |
| 145 | + In the example above, the total size of the outermost directory (and thus the |
| 146 | + total amount of used space) is `48381165`; this means that the size of the |
| 147 | + unused space must currently be `21618835`, which isn't quite the `30000000` |
| 148 | + required by the update. Therefore, the update still requires a directory with |
| 149 | + total size of at least `8381165` to be deleted before it can run. |
| 150 | + |
| 151 | + To achieve this, you have the following options: |
| 152 | + |
| 153 | + * Delete directory `e`, which would increase unused space by `584`. |
| 154 | + * Delete directory `a`, which would increase unused space by `94853`. |
| 155 | + * Delete directory `d`, which would increase unused space by `24933642`. |
| 156 | + * Delete directory `/`, which would increase unused space by `48381165`. |
| 157 | + |
| 158 | + Directories e and a are both too small; deleting them would not free up enough space. However, directories d and / are both big enough! Between these, choose the smallest: d, increasing unused space by 24933642. |
| 159 | + |
| 160 | + Find the smallest directory that, if deleted, would free up enough space on the filesystem to run the update. What is the total size of that directory? |
| 161 | + */ |
| 162 | +@main |
| 163 | +struct NoSpaceLeftOnDevice: ParsableCommand |
| 164 | +{ |
| 165 | + /// Enumeration for argument which activates "Part Two" behavior |
| 166 | + enum Mode: String, ExpressibleByArgument, CaseIterable |
| 167 | + { |
| 168 | + case sumOf100kOrLess |
| 169 | + case sizeOfSmallestBigEnough |
| 170 | + } |
| 171 | + |
| 172 | + @Option(help: "'sumOf100kOrLess' or 'sizeOfSmallestBigEnough'") |
| 173 | + var mode: Mode |
| 174 | +} |
| 175 | + |
| 176 | + |
| 177 | +protocol FilesystemItem |
| 178 | +{ |
| 179 | + var name: String { get } |
| 180 | + var size: Int { get } |
| 181 | +} |
| 182 | + |
| 183 | + |
| 184 | +class Directory: FilesystemItem |
| 185 | +{ |
| 186 | + class File: FilesystemItem |
| 187 | + { |
| 188 | + let name : String |
| 189 | + let size : Int |
| 190 | + |
| 191 | + init(name: String, size: Int) |
| 192 | + { |
| 193 | + self.name = name |
| 194 | + self.size = size |
| 195 | + } |
| 196 | + } |
| 197 | + |
| 198 | + let name : String |
| 199 | + var subdirectories : [Directory] |
| 200 | + var files : [File] |
| 201 | + |
| 202 | + var size: Int |
| 203 | + { |
| 204 | + let allItems: [FilesystemItem] = (self.subdirectories + self.files) |
| 205 | + return allItems.map(\.size).sum() |
| 206 | + } |
| 207 | + |
| 208 | + init(name: String) |
| 209 | + { |
| 210 | + self.name = name |
| 211 | + self.subdirectories = [] |
| 212 | + self.files = [] |
| 213 | + } |
| 214 | + |
| 215 | + |
| 216 | + func findDirectoriesOfSize(within range: any RandomAccessCollection<Int>) -> [Directory] |
| 217 | + { |
| 218 | + var results : [Directory] = [] |
| 219 | + |
| 220 | + if ( range.contains(self.size) ) |
| 221 | + { |
| 222 | + results.append(self) |
| 223 | + } |
| 224 | + |
| 225 | + for subdirectory in self.subdirectories |
| 226 | + { |
| 227 | + results.append(contentsOf: subdirectory.findDirectoriesOfSize(within: range)) |
| 228 | + } |
| 229 | + |
| 230 | + return results |
| 231 | + } |
| 232 | +} |
| 233 | + |
| 234 | + |
| 235 | +// MARK: - Command Execution |
| 236 | + |
| 237 | +extension NoSpaceLeftOnDevice |
| 238 | +{ |
| 239 | + mutating func run() throws |
| 240 | + { |
| 241 | + let diskCapacity = 70000000 |
| 242 | + let updateSize = 30000000 |
| 243 | + |
| 244 | + var currentPath: [Directory] = [] |
| 245 | + |
| 246 | + while let inputLine = readLine() |
| 247 | + { |
| 248 | + let components = inputLine.split(separator: " ") |
| 249 | + |
| 250 | + switch ( components[0], components[1] ) |
| 251 | + { |
| 252 | + case ( "$", "cd" ) : |
| 253 | + switch components[2] |
| 254 | + { |
| 255 | + case "..": |
| 256 | + currentPath.removeLast() |
| 257 | + |
| 258 | + default: |
| 259 | + let newDirectory = Directory(name: String(components[2])) |
| 260 | + if let currentDirectory = currentPath.last |
| 261 | + { |
| 262 | + currentDirectory.subdirectories.append(newDirectory) |
| 263 | + } |
| 264 | + currentPath.append(newDirectory) |
| 265 | + } |
| 266 | + |
| 267 | + case ( let size, let name ) where (try! /\d+/.wholeMatch(in: size) != nil): |
| 268 | + let file = Directory.File(name: String(name), size: Int(size)!) |
| 269 | + let currentDirectory: Directory = currentPath.last! |
| 270 | + currentDirectory.files.append(file) |
| 271 | + |
| 272 | + case ( "$", "ls" ) : break |
| 273 | + case ( "dir", _ ) : break |
| 274 | + default : fatalError() |
| 275 | + } |
| 276 | + } |
| 277 | + |
| 278 | + let root = currentPath.first! |
| 279 | + |
| 280 | + switch self.mode |
| 281 | + { |
| 282 | + case .sumOf100kOrLess: |
| 283 | + print(root.findDirectoriesOfSize(within: 0 ... 100000) |
| 284 | + .map(\.size) |
| 285 | + .sum()) |
| 286 | + |
| 287 | + case .sizeOfSmallestBigEnough: |
| 288 | + let diskUsage = root.size |
| 289 | + let minSize = (updateSize - (diskCapacity - diskUsage)) |
| 290 | + |
| 291 | + print(root.findDirectoriesOfSize(within: minSize ... .max) |
| 292 | + .sorted(by: { $0.size < $1.size }) |
| 293 | + .first(where: { $0.size >= minSize })! |
| 294 | + .size) |
| 295 | + } |
| 296 | + } |
| 297 | +} |
| 298 | + |
0 commit comments