#![feature(iterator_try_collect, absolute_path, hash_set_entry)] use std::collections::HashMap; use std::error::Error; use std::fmt::Display; use std::fs; use std::hash::{Hash, Hasher}; use std::io::{self, BufRead, Read}; use std::path::{Path, PathBuf}; mod parsing { use nom::{ branch::alt, bytes::complete::{is_not, tag}, character::complete::{digit1, multispace0}, combinator::{into, map, map_res}, sequence::{delimited, pair, preceded, separated_pair}, IResult, }; #[derive(Debug, PartialEq)] pub enum Command { Ls, Cd(String), } fn cd(input: &str) -> IResult<&str, Command> { let (input, dir) = preceded(delimited(multispace0, tag("cd"), multispace0), filename)(input)?; Ok((input, Command::Cd(dir))) } fn ls(input: &str) -> IResult<&str, Command> { let (input, _) = tag("ls")(input)?; Ok((input, Command::Ls)) } fn command(input: &str) -> IResult<&str, Command> { preceded(delimited(multispace0, tag("$"), multispace0), alt((cd, ls)))(input) } #[derive(Debug, PartialEq)] pub enum Listing { Dir(String), File(String, usize), } fn dir(input: &str) -> IResult<&str, Listing> { let (input, f_name) = preceded(pair(tag("dir"), multispace0), filename)(input)?; Ok((input, Listing::Dir(f_name))) } fn filename(input: &str) -> IResult<&str, String> { map(is_not(" "), |r: &str| r.to_string())(input) } fn file(input: &str) -> IResult<&str, Listing> { let (input, (f_size, f_name)) = separated_pair( map_res(digit1, |s: &str| s.parse::()), multispace0, filename, )(input)?; Ok((input, Listing::File(f_name, f_size))) } fn listing(input: &str) -> IResult<&str, Listing> { alt((dir, file))(input) } #[derive(Debug, PartialEq)] pub enum Line { Result(Listing), Command(Command), } impl From for Line { fn from(value: Listing) -> Self { Self::Result(value) } } impl From for Line { fn from(value: Command) -> Self { Self::Command(value) } } pub fn line(input: &str) -> IResult<&str, Line> { alt((into(listing), into(command)))(input) } #[cfg(test)] mod tests { use super::*; #[test] fn test_command() { assert_eq!(Ok(("", Command::Ls)), command("$ ls")); assert_eq!( Ok(("", Command::Cd("poo".to_string()))), command("$ cd poo") ); } #[test] fn test_listing() { assert_eq!( Ok(("", Listing::Dir("cabbage".to_string()))), listing("dir cabbage") ); assert_eq!( Ok(("", Listing::File("boob.txt".to_string(), 424242))), listing("424242 boob.txt") ); } } } use parsing::{line, Command, Line, Listing}; use path_clean::clean; #[derive(Debug)] struct State { cur_path: PathBuf, filesystem: FileNode, } #[derive(Debug)] struct FileNode { name: String, children: HashMap, size: Option, } impl Display for FileNode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{} ", if self.is_dir() { "dir" } else { "file" })?; if let Some(size) = self.size { write!(f, "{} ", size)?; } write!(f, "{}", self.name) } } impl PartialEq for FileNode { fn eq(&self, other: &Self) -> bool { self.name.eq(&other.name) } } impl Eq for FileNode {} impl Hash for FileNode { fn hash(&self, state: &mut H) { self.name.hash(state); } } impl From for FileNode { fn from(value: String) -> Self { FileNode::new(value) } } impl<'a> IntoIterator for &'a FileNode { type Item = &'a FileNode; type IntoIter = std::vec::IntoIter; fn into_iter(self) -> Self::IntoIter { fn append<'a>(tree: &'a FileNode, v: &mut Vec<&'a FileNode>) { v.push(tree); for child in tree.children.values() { append(child, v); } } let mut result = vec![]; append(self, &mut result); result.into_iter() } } impl FileNode { fn is_dir(&self) -> bool { !self.children.is_empty() } fn new(name: String) -> Self { Self { name, children: HashMap::new(), size: None, } } fn calculate_size(&mut self) -> usize { *self.size.get_or_insert_with(|| { self.children .iter_mut() .fold(0, |acc, (_, v)| acc + v.calculate_size()) }) } fn add(&mut self, path: String, size: Option) -> Result<(), Box> { let path = Path::new(&path); if path.components().count() == 1 { self.size = self.size.or(size); return Ok(()); } let path = path.strip_prefix(&self.name)?; let first_comp = path .components() .next() .ok_or("Expected at least one component")? .as_os_str() .to_string_lossy() .to_string(); let path_str = path.as_os_str().to_string_lossy().to_string(); if let Some(child) = self.children.get_mut(&first_comp) { child.add(path_str, size)?; } else { let mut child = FileNode::new(first_comp.clone()); child.add(path_str, size)?; self.children.insert(first_comp, child); } Ok(()) } } impl State { fn new() -> Self { State { cur_path: PathBuf::from("/"), filesystem: FileNode::new("/".to_owned()), } } fn process(&mut self, line: Line) -> Result<(), Box> { match line { Line::Command(cmd) => self.run(cmd), Line::Result(listing) => self.process_result(listing)?, } Ok(()) } fn run(&mut self, cmd: Command) { if let Command::Cd(p) = cmd { self.cur_path.push(p) } } fn process_result(&mut self, res: Listing) -> Result<(), Box> { let (path, maybe_size) = match res { Listing::File(path, size) => (path, Some(size)), Listing::Dir(path) => (path, None), }; let filename = clean(&self.cur_path.join(path).to_string_lossy()); self.filesystem.add(filename, maybe_size)?; Ok(()) } } fn main() { let filename = "etc/p07.txt"; let file = fs::File::open(filename).expect("Can't open file"); let lines: Vec = io::BufReader::new(file) .by_ref() .lines() .flatten() .collect(); println!("PART ONE ---------------"); part1(&lines); println!("\n PART TWO ---------------"); part2(&lines); } fn part1(lines: &[String]) { let mut state = State::new(); lines .iter() .flat_map(|l| line(l)) .for_each(|(_, l)| state.process(l).unwrap()); let full_size = state.filesystem.calculate_size(); dbg!(full_size); const MAX_DIR_SIZE: usize = 100_000; dbg!(state .filesystem .into_iter() .filter(|f| f.is_dir()) .flat_map(|f| f.size) .filter(|&s| s < MAX_DIR_SIZE) .sum::()); } fn part2(_lines: &[String]) {}