use std::env; use std::fs; use std::io::{self, BufReader, BufRead}; use std::collections::HashMap; use regex::Regex; use chrono::NaiveDateTime; use itertools::Itertools; fn main() { // Hash Maps (akin to Python Dictionaires) let mut start: HashMap> = HashMap::new(); let mut finish: HashMap> = HashMap::new(); let mut ind: HashMap = HashMap::new(); // Regular Expressions let empty = Regex::new(r"^\s*$").unwrap(); // regex for finding empty lines (and skipping them) let comment = Regex::new(r"^\s*#").unwrap(); // regex for finding empty lines (and skipping them) let delim = Regex::new(r":\s{2}").unwrap(); // regex for splitting timestamp entry let begin = Regex::new(r"Begin\s+(?P.*)\s*$").unwrap(); let end = Regex::new(r"End\s+(?P.*)\s*$").unwrap(); let category = Regex::new(r"\[(?P.*)\]\s+(?P.*)\s*$").unwrap(); let do_process = Regex::new(r"do_process").unwrap(); let mut dp: bool = false; let mut categories: Vec = Vec::new(); // Determine if we'r the do_process variant let params: Vec = env::args().collect(); if do_process.is_match(¶ms[0]) { dp = true; }; // Process the file (stdin or file argument) let input = env::args().nth(1); let reader: Box = match input { None => Box::new(BufReader::new(io::stdin())), Some(filename) => Box::new(BufReader::new(fs::File::open(filename).unwrap())) }; // For each line of input (build internal data structures) for line in reader.lines() { // need to get the string out of line (which is an option enum) let l: String = line.unwrap(); // if the line is empty or is a comment if empty.is_match(&l) || comment.is_match(&l) { continue }; // Parse the timestamp and entry let splits: Vec<_> = delim.split(&l.trim_end()).into_iter().collect(); // splits should only ever have two elements (only one delimiter per log line) let datetime = splits[0]; let entry = splits[1]; let date_time = NaiveDateTime::parse_from_str(datetime,"%Y-%m-%d %H:%M:%S").unwrap(); let timestamp = date_time.timestamp(); // Process a Begin if begin.is_match(&entry) { let caps = begin.captures(&entry).unwrap(); let action = caps.name("action").unwrap().as_str(); start.entry(action.to_string()).or_insert(Vec::new()).push(timestamp); let cats = category.captures(&entry).unwrap(); let cat = cats.name("category").unwrap().as_str(); categories.push(cat.to_string()); }; // Process an End if end.is_match(&entry) { let caps = end.captures(&entry).unwrap(); let action = caps.name("action").unwrap().as_str(); finish.entry(action.to_string()).or_insert(Vec::new()).push(timestamp); let cats = category.captures(&entry).unwrap(); let cat = cats.name("category").unwrap().as_str(); categories.push(cat.to_string()); }; }; // end for // sort and dedup categories categories.sort_unstable(); categories.dedup(); let mut gtoth: f64 = 0.0; for (act, tstamps) in start.iter_mut().sorted_by_key(|x| x.0) { let bc: i64 = tstamps.len().try_into().unwrap(); let ec: i64 = finish.get_mut(&act.to_string()).expect("Somehow, not a vector!").len().try_into().unwrap(); // Debugging // println!("bc: {}, ec: {}", bc, ec); // This block ensures the lengths of start and finish for this action are equal if bc - ec > 1 { panic!("ERROR: Missing more than one End"); // need to fix log file } else if bc > ec { // bc should be exactly one greater tstamps.pop(); } else if ec > bc { panic!("ERROR: Missing a Begin"); // need to fix log file }; // Debugging //for t in tstamps.into_iter() { // print!("{} ", t); //}; //println!(); //for t in finish.get_mut(&act.to_string()).expect("Somehow, not a vector!").into_iter() { // print!("{} ", t); //}; //println!(); let mut delta: i64 = 0; for i in 0..tstamps.len() { let beg = tstamps[i]; let en = finish.get_mut(&act.to_string()).unwrap()[i]; delta += en - beg; }; ind.insert(act.to_string(), delta); let d: f64 = nearest(delta as f64 / 3600.00); if d == 0.0 { gtoth += 0.08; } else { gtoth += d; }; } // print the output if dp { let mut running_total: f64 = 0.0; for cat in categories { let mut subtotal: f64 = 0.0; let catre = Regex::new(&format!(r"\[{}\]\s+.*\s*$", cat).to_string()).unwrap(); for (act, duration) in ind.iter().sorted_by_key(|x| x.0) { if ! catre.is_match(&act.to_string()) { continue }; let mut f: f64 = nearest(*duration as f64/3600.00); if f == 0.0 { f = 0.08; }; let fhrs: String = format!("{:.2}hrs", f); println!("{}", format!("{:<75}{:>10}", act.to_string(), fhrs.to_string())); subtotal += f; }; running_total += subtotal; println!(); println!("{}", format!("{:<20}{:.2}hrs", "Section total:", subtotal)); println!("{}", format!("{:<20}{:.2}hrs", "Subtotal:", running_total)); println!(); println!(); }; } else { for (act, duration) in ind.iter().sorted_by_key(|x| x.0) { let f: f64 = nearest(*duration as f64/3600.00); let mut fhrs: String = format!("{:2}hrs", 0.08); if f > 0.0 { fhrs = format!("{:.2}hrs", f); }; println!("{}", format!("{:<75}{:>10}", act.to_string(), fhrs.to_string())); }; }; println!(); println!("{}", format!("Grand total: {:.2}", gtoth)); } fn nearest(hr: f64) -> f64 { let t = hr * 4.0; t.round() / 4.0 }