timetracker/rust/chug/src/main.rs

162 lines
6.3 KiB
Rust

use std::env;
use std::fs;
use std::io::{BufReader, BufRead, ErrorKind};
use std::collections::HashMap;
use regex::Regex;
use chrono::{Datelike, Duration, offset::Local, NaiveDate, NaiveDateTime, Weekday};
use itertools::Itertools;
fn main() {
// 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 offset = Regex::new(r"^[+-]*\d+").unwrap();
// Process options
let params: Vec<String> = env::args().collect();
// If week offset is provided, grab it
let mut week_offset: i64 = 0;
for arg in &params {
if offset.is_match(&arg.to_string()) {
week_offset = arg.parse::<i64>().unwrap();
};
};
let day: NaiveDate = last_monday(week_offset);
let mut filenames: Vec<String> = Vec::new();
for d in 0..7 {
filenames.push((day + Duration::days(d)).format("%Y-%m-%d.log").to_string());
};
assert_eq!(filenames.len(), 7);
let mut grand_total: f64 = 0.0;
for filename in filenames {
println!("{} ", filename);
let mut gtoth: f64 = 0.0;
// Process the file
match fs::File::open(filename) {
Ok(file) => {
// 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 reader = BufReader::new(file);
// 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);
};
// 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);
};
}; // end for
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;
};
let d: f64 = nearest(delta as f64 / 3600.00);
if d == 0.0 {
gtoth += 0.08;
} else {
gtoth += d;
};
};
},
Err(error) => match error.kind() {
ErrorKind::NotFound => (),
_ => panic!("Error reading file! Error: {}", error)
}
};
if gtoth > 24.0 {
gtoth = 24.0;
};
// print the output
println!("{}", format!("{:.2}", gtoth));
println!();
grand_total += gtoth;
};
println!("Grand total: {:.2}", grand_total);
}
fn nearest(hr: f64) -> f64 {
let t = hr * 4.0;
t.round() / 4.0
}
fn last_monday(offset: i64) -> NaiveDate {
let day: NaiveDate = Local::today().naive_local() - Duration::days(7 * offset);
match day.weekday() {
Weekday::Mon => day - Duration::days(0),
Weekday::Tue => day - Duration::days(1),
Weekday::Wed => day - Duration::days(2),
Weekday::Thu => day - Duration::days(3),
Weekday::Fri => day - Duration::days(4),
Weekday::Sat => day - Duration::days(5),
Weekday::Sun => day - Duration::days(6)
}
}