176 lines
6.2 KiB
Rust
176 lines
6.2 KiB
Rust
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<String, Vec<i64>> = HashMap::new();
|
|
let mut finish: HashMap<String, Vec<i64>> = HashMap::new();
|
|
let mut ind: HashMap<String, i64> = 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<action>.*)\s*$").unwrap();
|
|
let end = Regex::new(r"End\s+(?P<action>.*)\s*$").unwrap();
|
|
let category = Regex::new(r"\[(?P<category>.*)\]\s+(?P<activity>.*)\s*$").unwrap();
|
|
let do_process = Regex::new(r"do_process").unwrap();
|
|
|
|
let mut dp: bool = false;
|
|
|
|
let mut categories: Vec<String> = Vec::new();
|
|
|
|
// Determine if we'r the do_process variant
|
|
let params: Vec<String> = 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<dyn BufRead> = 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
|
|
}
|