extern crate chrono; extern crate regex; use std::error::Error; use std::fs::File; use std::io::{BufRead, BufReader}; use std::fmt; use std::collections::{HashMap, HashSet}; use std::collections::hash_map::Entry; use std::iter::FromIterator; use chrono::prelude::*; use regex::{Regex, Captures}; const INPUT: &str = "inputs/4.txt"; #[derive(Debug, PartialEq)] enum Record { Start { time: NaiveDateTime, guard_id: u32, }, Sleep { time: NaiveDateTime, }, Wake { time: NaiveDateTime, }, } impl Record { fn time(&self) -> NaiveDateTime { match *self { Record::Start { time, guard_id: _ } => time, Record::Sleep { time } => time, Record::Wake { time } => time, } } } #[derive(Debug, Clone, PartialEq)] struct MalformedRecord { details: String } impl MalformedRecord { fn new(msg: &str) -> MalformedRecord { MalformedRecord{ details: msg.to_string() } } } impl fmt::Display for MalformedRecord { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.details) } } impl Error for MalformedRecord { fn description(&self) -> &str { &self.details } } pub fn solve_part1() -> Result> { Ok(get_part1(INPUT)?) } pub fn solve_part2() -> Result> { Ok(get_part2(INPUT)?) } fn get_part1(filename: &str) -> Result> { let records = read_records(filename)?; let minutes_asleep = minutes_asleep_per_guard(records); let sleepiest_guard = minutes_asleep.iter().max_by_key(|&(_, mins)| mins.len()).unwrap(); let sleepiest_minute = mode(sleepiest_guard.1); Ok(sleepiest_guard.0 * sleepiest_minute) } fn get_part2(filename: &str) -> Result> { let records = read_records(filename)?; let minutes_asleep = minutes_asleep_per_guard(records); let all_mins: Vec = minutes_asleep .values() .flat_map(|mins| mins.iter()) .cloned() .collect(); let sleepiest_minute = mode(&all_mins[..]); let sleepiest_guard = minutes_asleep .iter() .max_by_key(|(_, mins)| mins.into_iter().filter(|min| **min == sleepiest_minute).count()) .unwrap(); Ok(sleepiest_guard.0 * sleepiest_minute) } fn mode(numbers: &[u32]) -> u32 { let mut occurences = HashMap::new(); for &value in numbers { *occurences.entry(value).or_insert(0) += 1; } occurences .into_iter() .max_by_key(|&(_, count)| count) .map(|(val, _)| val) .unwrap_or(0) } fn minutes_asleep_per_guard(mut records: Vec) -> HashMap> { let mut minutes_asleep: HashMap> = HashMap::new(); records.sort_by_key(|r| r.time()); let mut current_guard = 0; let mut fell_asleep = 0; for record in records { match record { Record::Start { time: _, guard_id } => current_guard = guard_id, Record::Sleep { time } => fell_asleep = time.minute(), Record::Wake { time } => { let mut slept_minutes = (fell_asleep..time.minute()).collect(); match minutes_asleep.entry(current_guard) { Entry::Vacant(e) => { e.insert(slept_minutes); }, Entry::Occupied(mut e) => { e.get_mut().append(&mut slept_minutes); }, } } } } minutes_asleep } fn read_records(filename: &str) -> Result, Box> { let mut records: Vec = Vec::new(); let record_regex = Regex::new(concat!( r"\[(?P\d{4}-\d{2}-\d{2}\s\d{2}:\d{2})\]\s(?:", r"(?PGuard #(?P\d+) begins shift)|", r"(?Pfalls asleep)|", r"(?Pwakes up))"))?; let file = File::open(filename)?; for line in BufReader::new(file).lines() { match record_regex.captures(&line?) { Some(captures) => { let time = NaiveDateTime::parse_from_str( &get_captured_field(&captures, "timestamp")?, "%Y-%m-%d %H:%M")?; if has_captured_field(&captures, "start")? { records.push(Record::Start { time: time, guard_id: get_captured_field(&captures, "guard_id")?.parse()?, }); } else if has_captured_field(&captures, "sleep")? { records.push(Record::Sleep { time: time, }); } else { records.push(Record::Wake { time: time, }); } }, None => return Err(Box::new(MalformedRecord { details: "Malformed record line, no fields could be found".to_string() })), }; } Ok(records) } fn get_captured_field(captures: &Captures, field: &str) -> Result> { match captures.name(field) { Some(capture) => Ok(String::from(capture.as_str())), None => return Err(Box::new(MalformedRecord { details: format!("Malformed record line, field {} could not be found", field) })) } } fn has_captured_field(captures: &Captures, field: &str) -> Result> { match captures.name(field) { Some(_) => Ok(true), None => Ok(false) } } #[cfg(test)] mod tests { use super::*; const TEST_INPUT: &str = "inputs/4_test.txt"; const TEST_INPUT_MALFORMED: &str = "inputs/4_test_malformed.txt"; #[test] fn reads_records_file() { assert_eq!(read_records(TEST_INPUT).unwrap(), vec![ Record::Start { time: NaiveDateTime::parse_from_str( "1518-11-01 00:00", "%Y-%m-%d %H:%M").unwrap(), guard_id: 10, }, Record::Sleep { time: NaiveDateTime::parse_from_str( "1518-11-01 00:05", "%Y-%m-%d %H:%M").unwrap(), }, Record::Wake { time: NaiveDateTime::parse_from_str( "1518-11-01 00:25", "%Y-%m-%d %H:%M").unwrap(), }, Record::Sleep { time: NaiveDateTime::parse_from_str( "1518-11-01 00:30", "%Y-%m-%d %H:%M").unwrap(), }, Record::Wake { time: NaiveDateTime::parse_from_str( "1518-11-01 00:55", "%Y-%m-%d %H:%M").unwrap(), }, Record::Start { time: NaiveDateTime::parse_from_str( "1518-11-01 23:58", "%Y-%m-%d %H:%M").unwrap(), guard_id: 99, }, Record::Sleep { time: NaiveDateTime::parse_from_str( "1518-11-02 00:40", "%Y-%m-%d %H:%M").unwrap(), }, Record::Wake { time: NaiveDateTime::parse_from_str( "1518-11-02 00:50", "%Y-%m-%d %H:%M").unwrap(), }, Record::Start { time: NaiveDateTime::parse_from_str( "1518-11-03 00:05", "%Y-%m-%d %H:%M").unwrap(), guard_id: 10, }, Record::Sleep { time: NaiveDateTime::parse_from_str( "1518-11-03 00:24", "%Y-%m-%d %H:%M").unwrap(), }, Record::Wake { time: NaiveDateTime::parse_from_str( "1518-11-03 00:29", "%Y-%m-%d %H:%M").unwrap(), }, Record::Start { time: NaiveDateTime::parse_from_str( "1518-11-04 00:02", "%Y-%m-%d %H:%M").unwrap(), guard_id: 99, }, Record::Sleep { time: NaiveDateTime::parse_from_str( "1518-11-04 00:36", "%Y-%m-%d %H:%M").unwrap(), }, Record::Wake { time: NaiveDateTime::parse_from_str( "1518-11-04 00:46", "%Y-%m-%d %H:%M").unwrap(), }, Record::Start { time: NaiveDateTime::parse_from_str( "1518-11-05 00:03", "%Y-%m-%d %H:%M").unwrap(), guard_id: 99, }, Record::Sleep { time: NaiveDateTime::parse_from_str( "1518-11-05 00:45", "%Y-%m-%d %H:%M").unwrap(), }, Record::Wake { time: NaiveDateTime::parse_from_str( "1518-11-05 00:55", "%Y-%m-%d %H:%M").unwrap(), }, ]); } #[test] fn errors_on_malformed_records_file() { match read_records(TEST_INPUT_MALFORMED) { Ok(_) => assert!(false, "read_records should have returned an error"), Err(err) => assert_eq!( (*err).description(), "Malformed record line, no fields could be found".to_string(), ), } } #[test] fn gets_minutes_asleep_per_guard() { let mut expected: HashMap> = HashMap::new(); expected.insert(10, vec![5, 6, 7, 8, 9]); assert_eq!(minutes_asleep_per_guard(vec![ Record::Sleep { time: NaiveDateTime::parse_from_str( "1518-11-01 00:05", "%Y-%m-%d %H:%M").unwrap(), }, Record::Start { time: NaiveDateTime::parse_from_str( "1518-11-01 00:00", "%Y-%m-%d %H:%M").unwrap(), guard_id: 10, }, Record::Wake { time: NaiveDateTime::parse_from_str( "1518-11-01 00:10", "%Y-%m-%d %H:%M").unwrap(), }, ]), expected); } #[test] fn solves_part1() { assert_eq!(get_part1(TEST_INPUT).unwrap(), 240); } #[test] fn solves_part2() { assert_eq!(get_part2(TEST_INPUT).unwrap(), 4455); } }