/// --- Day 7: No Space Left On Device --- /// /// You can hear birds chirping and raindrops hitting leaves as the expedition proceeds. /// Occasionally, you can even hear much louder sounds in the distance; how big do the /// animals get out here, anyway? /// /// The device the Elves gave you has problems with more than just its communication system. You /// try to run a system update: /// /// ``` /// $ system-update --please --pretty-please-with-sugar-on-top /// Error: No space left on device /// ``` /// /// Perhaps you can delete some files to make space for the update? /// /// You browse around the filesystem to assess the situation and save the resulting terminal output /// (your puzzle input). For example: /// /// ``` /// $ cd / /// $ ls /// dir a /// 14848514 b.txt /// 8504156 c.dat /// dir d /// $ cd a /// $ ls /// dir e /// 29116 f /// 2557 g /// 62596 h.lst /// $ cd e /// $ ls /// 584 i /// $ cd .. /// $ cd .. /// $ cd d /// $ ls /// 4060174 j /// 8033020 d.log /// 5626152 d.ext /// 7214296 k /// ``` /// /// The filesystem consists of a tree of files (plain data) and directories (which can contain /// other directories or files). The outermost directory is called /. You can navigate around the /// filesystem, moving into or out of directories and listing the contents of the directory you're /// currently in. /// /// Within the terminal output, lines that begin with $ are commands you executed, very much like /// some modern computers: /// /// cd means change directory. This changes which directory is the current directory, but the /// specific result depends on the argument: /// cd x moves in one level: it looks in the current directory for the directory named x /// and makes it the current directory. /// cd .. moves out one level: it finds the directory that contains the current directory, /// then makes that directory the current directory. /// cd / switches the current directory to the outermost directory, /. /// ls means list. It prints out all of the files and directories immediately contained by the /// current directory: /// 123 abc means that the current directory contains a file named abc with size 123. /// dir xyz means that the current directory contains a directory named xyz. /// /// Given the commands and output in the example above, you can determine that the filesystem looks /// visually like this: /// /// ``` /// - / (dir) /// - a (dir) /// - e (dir) /// - i (file, size=584) /// - f (file, size=29116) /// - g (file, size=2557) /// - h.lst (file, size=62596) /// - b.txt (file, size=14848514) /// - c.dat (file, size=8504156) /// - d (dir) /// - j (file, size=4060174) /// - d.log (file, size=8033020) /// - d.ext (file, size=5626152) /// - k (file, size=7214296) /// ``` /// /// Here, there are four directories: / (the outermost directory), a and d (which are in /), and e /// (which is in a). These directories also contain files of various sizes. /// /// Since the disk is full, your first step should probably be to find directories that are good /// candidates for deletion. To do this, you need to determine the total size of each directory. /// The total size of a directory is the sum of the sizes of the files it contains, directly or /// indirectly. (Directories themselves do not count as having any intrinsic size.) /// /// The total sizes of the directories above can be found as follows: /// /// The total size of directory e is 584 because it contains a single file i of size 584 and no /// other directories. /// The directory a has total size 94853 because it contains files f (size 29116), g (size /// 2557), and h.lst (size 62596), plus file i indirectly (a contains e which contains i). /// Directory d has total size 24933642. /// As the outermost directory, / contains every file. Its total size is 48381165, the sum of /// the size of every file. /// /// To begin, find all of the directories with a total size of at most 100000, then calculate the /// sum of their total sizes. In the example above, these directories are a and e; the sum of /// their total sizes is 95437 (94853 + 584). (As in this example, this process can count files /// more than once!) /// /// Find all of the directories with a total size of at most 100000. What is the sum of the total /// sizes of those directories? use clap::Parser; use nom::branch::alt; use nom::bytes::streaming::tag; use nom::character::streaming::{char, newline, not_line_ending, space1, u64}; use nom::combinator::{map, peek}; use nom::error::{context, ContextError, ErrorKind as NomErrorKind, ParseError}; use nom::multi::many_till; use nom::sequence::{delimited, preceded, terminated, tuple}; use nom::IResult; use std::collections::HashMap; use std::fs::File; use std::io::prelude::*; use std::io::BufReader; use std::iter::*; use std::path::PathBuf; pub type Input<'a> = &'a str; pub type Result<'a, T> = IResult, T, Error>>; #[derive(Clone, Debug, PartialEq, Eq)] pub enum ErrorKind { Nom(NomErrorKind), Context(&'static str), Custom(String), } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Error { pub errors: Vec<(I, ErrorKind)>, } impl ParseError for Error { fn from_error_kind(input: I, kind: NomErrorKind) -> Self { let errors = vec![(input, ErrorKind::Nom(kind))]; Self { errors } } fn append(input: I, kind: NomErrorKind, mut other: Self) -> Self { other.errors.push((input, ErrorKind::Nom(kind))); other } } impl ContextError for Error { fn add_context(input: I, ctx: &'static str, mut other: Self) -> Self { other.errors.push((input, ErrorKind::Context(ctx))); other } } const FILEPATH: &'static str = "examples/input.txt"; const EOF_CHAR: char = '\0'; const DIRMAXSIZE: u64 = 100000; #[derive(Parser, Debug)] #[clap(author, version, about, long_about = None)] struct Cli { #[clap(short, long, default_value = FILEPATH)] file: PathBuf, } #[derive(Clone, Debug)] enum Commands { ChangeDir(String), List(Vec), } #[derive(Clone, Debug)] enum ListEntry { File(FileEntry), Folder(String), } #[derive(Clone, Debug)] struct FileEntry { name: String, size: u64, } #[derive(Clone, Debug)] struct Arena(Vec); #[derive(Clone, Debug)] struct Node { size: u64, parent: Option, children: HashMap, } impl Node { fn new() -> Self { Node { size: 0, parent: None, children: HashMap::new(), } } fn with_parent(idx: usize) -> Self { Node { size: 0, parent: Some(idx), children: HashMap::new(), } } } impl Arena { // NOTE: only call this once per node fn consume(&mut self, idx: usize, ls: Vec) { for le in ls.into_iter() { match le { ListEntry::File(f) => { let mut iteridx = idx; loop { self.0[iteridx].size += f.size; iteridx = if let Some(pidx) = self.0[iteridx].parent { pidx } else { break; }; } } ListEntry::Folder(f) => { let fidx = self.0.len(); self.0.push(Node::with_parent(idx)); self.0[idx].children.insert(f, fidx); } } } } fn get(&mut self, idx: usize) -> &mut Node { &mut self.0[idx] } } fn parse_cd(input: &str) -> Result { context( "parse_cd", map( delimited(tuple((tag("cd"), space1)), not_line_ending, newline), |s: &str| Commands::ChangeDir(s.to_string()), ), )(input) } fn end_of_file_or_ls(input: &str) -> Result { alt((char(EOF_CHAR), peek(char('$'))))(input) } fn parse_ls(input: &str) -> Result { let (input, _) = context("parse_ls", terminated(tag("ls"), newline))(input)?; let (input, (lsv, _)) = many_till(parse_ls_entry, end_of_file_or_ls)(input)?; Ok((input, Commands::List(lsv))) } fn parse_ls_entry(input: &str) -> Result { context("parse_ls_entry", alt((parse_file, parse_folder)))(input) } fn parse_file(input: &str) -> Result { map( terminated(tuple((u64, preceded(tag(" "), not_line_ending))), newline), |(size, name): (u64, &str)| { ListEntry::File(FileEntry { name: name.to_string(), size, }) }, )(input) } fn parse_folder(input: &str) -> Result { map( delimited(tag("dir "), not_line_ending, newline), |s: &str| ListEntry::Folder(s.to_string()), )(input) } fn parse_cmd(input: &str) -> Result { context( "parse_cmd", preceded(tuple((tag("$"), space1)), alt((parse_cd, parse_ls))), )(input) } fn main() { let args = Cli::parse(); let file = File::open(&args.file).unwrap(); let mut reader = BufReader::new(file); let mut buf = String::new(); let mut cmds = Vec::new(); 'block: loop { let len = reader.read_line(&mut buf).unwrap(); if len == 0 { buf.push(EOF_CHAR); } loop { match parse_cmd(buf.as_str()) { Ok((input, cmd)) => { cmds.push(cmd); if len == 0 && input.len() == 0 { break 'block; } let _ = buf.drain(..(buf.len() - input.len())); } Err(err) => match err { nom::Err::Incomplete(_) => continue 'block, e @ _ => panic!("buf={} err={}", buf, e), }, } } } let mut loc = 0; let mut arena = Arena(Vec::new()); arena.0.push(Node::new()); for cmd in cmds.into_iter() { match cmd { Commands::ChangeDir(dir) => match dir.as_str() { "/" => loc = 0, ".." => loc = arena.get(loc).parent.unwrap(), _ => loc = arena.get(loc).children[&dir], }, Commands::List(lsv) => arena.consume(loc, lsv), } } let res: u64 = arena .0 .into_iter() .filter(|n| n.size <= DIRMAXSIZE) .map(|n| n.size) .sum(); println!("{res:?}"); }