diff --git a/src/main.rs b/src/main.rs index 0c3de7c..fd437ae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,405 +1,495 @@ -use serde::{Serialize, Deserialize}; -use std::io::{Read, Write}; -use std::fs::File; -use std::time::{SystemTime, UNIX_EPOCH, Duration}; -use std::ops::{Add, Sub}; +use chrono::{Local, TimeZone, Timelike}; use clap::Parser; -use chrono::{TimeZone, Local, Timelike}; use regex::Regex; +use serde::{Deserialize, Serialize}; use serde_yaml::from_str; +use std::fs::File; +use std::io::{Read, Write}; +use std::ops::{Add, Sub}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; #[derive(Debug, Parser)] struct Flags { - #[arg(short = 'f', long = "file")] - f: String, + #[arg(short = 'f', long = "file")] + f: String, - #[arg(short = 'l', long = "log")] - log: bool, + #[arg(short = 'l', long = "log")] + log: bool, - #[arg(short = 's', long = "since")] - since: Option, + #[arg(short = 's', long = "since")] + since: Option, - #[arg(short = 'a', long = "add")] - add: Option, + #[arg(short = 'a', long = "add")] + add: Option, - #[arg(short = 't', long = "tag")] - tag: Option, + #[arg(short = 't', long = "tag")] + tag: Option, - #[arg(short = 'c', long = "clock")] - clock: bool, + #[arg(short = 'c', long = "clock")] + clock: bool, - #[arg(short = 'd', long = "duration")] - duration: Option, + #[arg(short = 'd', long = "duration")] + duration: Option, - #[arg(short = 'v', long = "verbose")] - verbose: bool, + #[arg(short = 'v', long = "verbose")] + verbose: bool, - #[arg(short = 'p', long = "precision")] - precision: Option, + #[arg(short = 'p', long = "precision")] + precision: Option, } fn main() { - let mut flags = Flags::parse(); - flags.log = flags.log || flags.since.is_some(); + let mut flags = Flags::parse(); + flags.log = flags.log || flags.since.is_some(); - let duration = parse_duration(&flags.duration).unwrap(); + let duration = parse_duration(&flags.duration).unwrap(); - clock(&flags.f, &(flags.clock || flags.duration.is_some()), &duration).unwrap(); - add(&flags.f, &flags.add, &flags.tag, &0).unwrap(); - log(&flags.f, &flags.log, &flags.since, &flags.verbose, &flags.precision.unwrap_or(0)).unwrap(); + clock( + &flags.f, + &(flags.clock || flags.duration.is_some()), + &duration, + ) + .unwrap(); + add(&flags.f, &flags.add, &flags.tag, &0).unwrap(); + log( + &flags.f, + &flags.log, + &flags.since, + &flags.verbose, + &flags.precision.unwrap_or(0), + ) + .unwrap(); } fn clock(f: &String, clock: &bool, duration: &u64) -> Result<(), String> { - match clock { - true => add(&f, &Some("".to_string()), &None, duration), - false => Ok(()), - } + match clock { + true => add(&f, &Some("".to_string()), &None, duration), + false => Ok(()), + } } fn add(f: &String, x: &Option, tag: &Option, duration: &u64) -> Result<(), String> { - match x { - Some(x) => { - let mut tsheet = load(&f)?; - tsheet.add( - x.to_string(), - tag.clone().unwrap_or("".to_string()), - *duration, - ); - save(&f, tsheet)?; - }, - None => {}, - }; - Ok(()) + match x { + Some(x) => { + let mut tsheet = load(&f)?; + tsheet.add( + x.to_string(), + tag.clone().unwrap_or("".to_string()), + *duration, + ); + save(&f, tsheet)?; + } + None => {} + }; + Ok(()) } #[derive(Debug, Serialize, Clone)] struct Log { - t: String, - d: f32, - xs: Vec, + t: String, + d: f32, + xs: Vec, } #[derive(Debug, Serialize, Clone)] struct LogX { - d: f32, - x: String, + d: f32, + x: String, } -fn log(f: &String, enabled: &bool, since: &Option, verbose: &bool, precision: &u32) -> Result<(), String> { - if !enabled { - return Ok(()); - } - let since = parse_time(since)?; - if *verbose { - eprintln!("since = {} ({})", system_time_to_unix_seconds(&since), timestamp(&system_time_to_unix_seconds(&since), &2)); - } +fn log( + f: &String, + enabled: &bool, + since: &Option, + verbose: &bool, + precision: &u32, +) -> Result<(), String> { + if !enabled { + return Ok(()); + } + let since = parse_time(since)?; + if *verbose { + eprintln!( + "since = {} ({})", + system_time_to_unix_seconds(&since), + timestamp(&system_time_to_unix_seconds(&since), &2) + ); + } - let tsheet = load(&f)?; - let tsheet = tsheet.since(since); - if *verbose { - eprintln!("tsheet = {:?}", &tsheet); - } + let tsheet = load(&f)?; + let tsheet = tsheet.since(since); + if *verbose { + eprintln!("tsheet = {:?}", &tsheet); + } - let mut result = vec![]; - let mut curr = Log{t: "".to_string(), d: 0.0, xs: vec![]}; - let mut currt = "".to_string(); - for i in 0..tsheet.xs.len() { - let x = &tsheet.xs[i]; - if *verbose { - eprintln!("{} != {}?", &curr.t, x.timestamp(&precision)); - } - if currt != x.timestamp(&0) { - if curr.xs.len() > 0 { - if *verbose { - eprintln!("push {:?}", &curr.xs); + let mut result = vec![]; + let mut curr = Log { + t: "".to_string(), + d: 0.0, + xs: vec![], + }; + let mut currt = "".to_string(); + for i in 0..tsheet.xs.len() { + let x = &tsheet.xs[i]; + if *verbose { + eprintln!("{} != {}?", &curr.t, x.timestamp(&precision)); + } + if currt != x.timestamp(&0) { + if curr.xs.len() > 0 { + if *verbose { + eprintln!("push {:?}", &curr.xs); + } + result.push(curr.clone()); } - result.push(curr.clone()); - } - curr.xs.truncate(0); - curr.t = x.timestamp(&precision); - currt = x.timestamp(&0); - curr.d = 0.0; - } - let mut d = 1.0; - if x.x.len() == 0 { // clock ins get duration zero - d = 0.0; - } else if i > 0 && tsheet.xs[i].timestamp(&0) == tsheet.xs[i-1].timestamp(&0) { // if same day as previous then use elapsed as duration - d = ((tsheet.xs[i].t - tsheet.xs[i-1].t) as f32 / (60.0*60.0)) as f32; - } - if *verbose { - eprintln!("d={} x='{}'", &x.x, &d); - } - match x.x.len() { - 0 => {}, - _ => { + curr.xs.truncate(0); curr.t = x.timestamp(&precision); currt = x.timestamp(&0); - curr.xs.push(LogX{d: d, x: x.x.clone()}); - }, - }; - } - if curr.xs.len() > 0 { - if *verbose { - eprintln!("push {:?} (final)", &curr.xs); - } - result.push(curr.clone()); - } - let mut total_d = 0.0; - for i in result.iter_mut() { - i.d = i.xs.iter().map(|x| x.d).sum(); - total_d += i.d; - if *verbose { - eprintln!("{} = {:?}", &i.d, &i.xs.iter().map(|x| x.d).collect::>()); - } - } - - for log in &result { - for x in log.xs.clone() { - if x.x.len() > 0 { - match precision { - 0 => println!("{} ({:.0}) {} ({:.1})", log.t, log.d, x.x, x.d), - 1 => println!("{} ({:.1}) {} ({:.2})", log.t, log.d, x.x, x.d), - _ => println!("{} ({:.2}) {} ({:.3})", log.t, log.d, x.x, x.d), + curr.d = 0.0; + } + let mut d = 1.0; + if x.x.len() == 0 { + // clock ins get duration zero + d = 0.0; + } else if i > 0 && tsheet.xs[i].timestamp(&0) == tsheet.xs[i - 1].timestamp(&0) { + // if same day as previous then use elapsed as duration + d = ((tsheet.xs[i].t - tsheet.xs[i - 1].t) as f32 / (60.0 * 60.0)) as f32; + } + if *verbose { + eprintln!("d={} x='{}'", &x.x, &d); + } + match x.x.len() { + 0 => {} + _ => { + curr.t = x.timestamp(&precision); + currt = x.timestamp(&0); + curr.xs.push(LogX { + d: d, + x: x.x.clone(), + }); } - } - } - } + }; + } + if curr.xs.len() > 0 { + if *verbose { + eprintln!("push {:?} (final)", &curr.xs); + } + result.push(curr.clone()); + } + let mut total_d = 0.0; + for i in result.iter_mut() { + i.d = i.xs.iter().map(|x| x.d).sum(); + total_d += i.d; + if *verbose { + eprintln!( + "{} = {:?}", + &i.d, + &i.xs.iter().map(|x| x.d).collect::>() + ); + } + } - if result.len() > 1 { - match precision { - 0 => eprintln!("({:.0}h over {} dates)", total_d, result.len()), - 1 => eprintln!("({:.1}h over {} dates)", total_d, result.len()), - _ => eprintln!("({:.2}h over {} dates)", total_d, result.len()), - } - } + for log in &result { + for x in log.xs.clone() { + if x.x.len() > 0 { + match precision { + 0 => println!("{} ({:.0}) {} ({:.1})", log.t, log.d, x.x, x.d), + 1 => println!("{} ({:.1}) {} ({:.2})", log.t, log.d, x.x, x.d), + _ => println!("{} ({:.2}) {} ({:.3})", log.t, log.d, x.x, x.d), + } + } + } + } - Ok(()) + if result.len() > 1 { + match precision { + 0 => eprintln!( + "({:.0}h of {}h over {} dates)", + total_d, + 8 * result.len(), + result.len() + ), + 1 => eprintln!( + "({:.1}h of {}h over {} dates)", + total_d, + 8 * result.len(), + result.len() + ), + _ => eprintln!( + "({:.2}h of {}h over {} dates)", + total_d, + 8 * result.len(), + result.len() + ), + } + } + + Ok(()) } #[cfg(test)] mod test_parse { - use super::*; + use super::*; - #[test] - fn test_duration() { - assert_eq!(0, parse_duration(&None).unwrap()); - assert_eq!(1, parse_duration(&Some("1s".to_string())).unwrap()); - assert_eq!(33, parse_duration(&Some("33s".to_string())).unwrap()); - assert_eq!(666, parse_duration(&Some("666s".to_string())).unwrap()); - assert_eq!(60, parse_duration(&Some("1m".to_string())).unwrap()); - assert_eq!(62, parse_duration(&Some("1m2s".to_string())).unwrap()); - assert_eq!(3600, parse_duration(&Some("1h".to_string())).unwrap()); - assert_eq!(3723, parse_duration(&Some("1h2m3s".to_string())).unwrap()); - } + #[test] + fn test_duration() { + assert_eq!(0, parse_duration(&None).unwrap()); + assert_eq!(1, parse_duration(&Some("1s".to_string())).unwrap()); + assert_eq!(33, parse_duration(&Some("33s".to_string())).unwrap()); + assert_eq!(666, parse_duration(&Some("666s".to_string())).unwrap()); + assert_eq!(60, parse_duration(&Some("1m".to_string())).unwrap()); + assert_eq!(62, parse_duration(&Some("1m2s".to_string())).unwrap()); + assert_eq!(3600, parse_duration(&Some("1h".to_string())).unwrap()); + assert_eq!(3723, parse_duration(&Some("1h2m3s".to_string())).unwrap()); + } } fn parse_duration(d: &Option) -> Result { - match d { - Some(d) => { - let mut sum: u64 = 0; - let re = Regex::new(r"^(?[0-9]+h)?(?[0-9]+m)?(?[0-9]+s)?$").unwrap(); - match re.captures(d) { - Some(captures) => { - match captures.name("seconds") { - Some(n) => { - let n = n.as_str().to_string(); - let n = n.trim_end_matches('s'); - sum += from_str::(n).unwrap(); - }, - None => {}, - }; - match captures.name("minutes") { - Some(n) => { - let n = n.as_str().to_string(); - let n = n.trim_end_matches('m'); - sum += 60 * from_str::(n).unwrap(); - }, - None => {}, - }; - match captures.name("hours") { - Some(n) => { - let n = n.as_str().to_string(); - let n = n.trim_end_matches('h'); - sum += 60 * 60 * from_str::(n).unwrap(); - }, - None => {}, - }; - Ok(sum) - }, - None => Ok(0), - } - }, - None => Ok(0), - } + match d { + Some(d) => { + let mut sum: u64 = 0; + let re = Regex::new(r"^(?[0-9]+h)?(?[0-9]+m)?(?[0-9]+s)?$") + .unwrap(); + match re.captures(d) { + Some(captures) => { + match captures.name("seconds") { + Some(n) => { + let n = n.as_str().to_string(); + let n = n.trim_end_matches('s'); + sum += from_str::(n).unwrap(); + } + None => {} + }; + match captures.name("minutes") { + Some(n) => { + let n = n.as_str().to_string(); + let n = n.trim_end_matches('m'); + sum += 60 * from_str::(n).unwrap(); + } + None => {} + }; + match captures.name("hours") { + Some(n) => { + let n = n.as_str().to_string(); + let n = n.trim_end_matches('h'); + sum += 60 * 60 * from_str::(n).unwrap(); + } + None => {} + }; + Ok(sum) + } + None => Ok(0), + } + } + None => Ok(0), + } } fn parse_time(since: &Option) -> Result { - match since { - Some(since) => { - match chrono::NaiveDate::parse_from_str(since, "%Y-%m-%d") { + match since { + Some(since) => match chrono::NaiveDate::parse_from_str(since, "%Y-%m-%d") { Ok(nd) => { - let ndt = nd.and_hms_opt(1, 1, 1).unwrap(); - let dt = Local.from_local_datetime(&ndt).unwrap(); - Ok(UNIX_EPOCH.add( - Duration::from_secs(dt.timestamp() as u64) - )) - }, + let ndt = nd.and_hms_opt(1, 1, 1).unwrap(); + let dt = Local.from_local_datetime(&ndt).unwrap(); + Ok(UNIX_EPOCH.add(Duration::from_secs(dt.timestamp() as u64))) + } Err(msg) => Err(format!("failed to parse {}: {}", since, msg)), - } - }, - None => Ok(SystemTime::now().sub(Duration::from_secs(Local::now().hour() as u64*60*60))), - } + }, + None => { + Ok(SystemTime::now().sub(Duration::from_secs(Local::now().hour() as u64 * 60 * 60))) + } + } } #[derive(Debug, PartialEq, Serialize, Deserialize)] struct TSheet { - xs: Vec, + xs: Vec, } #[derive(Debug, PartialEq, Serialize, Deserialize, Clone, Ord, Eq, PartialOrd)] struct X { - t: i64, - x: String, - tag: String, + t: i64, + x: String, + tag: String, } fn save(path: &String, tsheet: TSheet) -> Result<(), String> { - match File::create(path.clone()) { - Ok(mut writer) => _save(&mut writer, tsheet), - Err(reason) => Err(format!("failed to open {} to save tsheet: {}", path, reason)), - } + match File::create(path.clone()) { + Ok(mut writer) => _save(&mut writer, tsheet), + Err(reason) => Err(format!( + "failed to open {} to save tsheet: {}", + path, reason + )), + } } fn _save(writer: &mut dyn Write, tsheet: TSheet) -> Result<(), String> { - let mut w = serde_yaml::Serializer::new(writer); - match tsheet.serialize(&mut w) { - Ok(_) => Ok(()), - Err(reason) => Err(format!("failed to serialize tsheet: {}", reason)), - } + let mut w = serde_yaml::Serializer::new(writer); + match tsheet.serialize(&mut w) { + Ok(_) => Ok(()), + Err(reason) => Err(format!("failed to serialize tsheet: {}", reason)), + } } fn load(path: &String) -> Result { - match File::open(path.clone()) { - Ok(mut reader) => _load(&mut reader), - Err(reason) => Err(format!("failed to read tsheet {}: {}", path, reason)), - } + match File::open(path.clone()) { + Ok(mut reader) => _load(&mut reader), + Err(reason) => Err(format!("failed to read tsheet {}: {}", path, reason)), + } } fn _load(reader: &mut dyn Read) -> Result { - match serde_yaml::from_reader::<&mut dyn Read, TSheet>(reader) { - Ok(tsheet) => Ok(tsheet), - Err(err) => Err(format!("failed to parse tsheet: {}", err)), - } + match serde_yaml::from_reader::<&mut dyn Read, TSheet>(reader) { + Ok(tsheet) => Ok(tsheet), + Err(err) => Err(format!("failed to parse tsheet: {}", err)), + } } #[cfg(test)] mod test_save_load { - use super::*; + use super::*; - #[test] - fn test_empty() { - let got = _load(&mut "xs: []".as_bytes()).expect("failed to parse 'xs: []' tsheet"); - assert_eq!(got, TSheet{xs: vec![]}); + #[test] + fn test_empty() { + let got = _load(&mut "xs: []".as_bytes()).expect("failed to parse 'xs: []' tsheet"); + assert_eq!(got, TSheet { xs: vec![] }); - let mut w = vec![]; - _save(&mut w, got).expect("failed saving tsheet to writer"); - assert_eq!(String::from_utf8(w).unwrap(), "xs: []\n".to_string()); - } + let mut w = vec![]; + _save(&mut w, got).expect("failed saving tsheet to writer"); + assert_eq!(String::from_utf8(w).unwrap(), "xs: []\n".to_string()); + } - #[test] - fn test_testdata_standalone_yaml() { - let want = TSheet{xs: vec![ - X{t: 1, x: "def".to_string(), tag: "abc".to_string()}, - X{t: 2, x: "ghi".to_string(), tag: "".to_string()}, - ]}; - assert_eq!( - load(&"./src/testdata/standalone.yaml".to_string()).expect("cant load standalone.yaml"), - want, - ); + #[test] + fn test_testdata_standalone_yaml() { + let want = TSheet { + xs: vec![ + X { + t: 1, + x: "def".to_string(), + tag: "abc".to_string(), + }, + X { + t: 2, + x: "ghi".to_string(), + tag: "".to_string(), + }, + ], + }; + assert_eq!( + load(&"./src/testdata/standalone.yaml".to_string()).expect("cant load standalone.yaml"), + want, + ); - let mut w = vec![]; - _save(&mut w, want).expect("failed saving tsheet to writer"); - assert_eq!(String::from_utf8(w).unwrap(), "xs:\n- t: 1\n x: def\n tag: abc\n- t: 2\n x: ghi\n tag: ''\n".to_string()); - } + let mut w = vec![]; + _save(&mut w, want).expect("failed saving tsheet to writer"); + assert_eq!( + String::from_utf8(w).unwrap(), + "xs:\n- t: 1\n x: def\n tag: abc\n- t: 2\n x: ghi\n tag: ''\n".to_string() + ); + } } impl TSheet { - fn since(&self, t: SystemTime) -> TSheet { - let mut result = TSheet{xs: vec![]}; - self.xs.iter() - .filter(|x| x.ts() >= t) - .for_each(|x| result.xs.push(x.clone())); - result - } + fn since(&self, t: SystemTime) -> TSheet { + let mut result = TSheet { xs: vec![] }; + self.xs + .iter() + .filter(|x| x.ts() >= t) + .for_each(|x| result.xs.push(x.clone())); + result + } - fn add(&mut self, x: String, tag: String, duration: u64) { - let now = system_time_to_unix_seconds(&SystemTime::now()); - self.xs.push(new_x( - now - duration as i64, - x, - tag, - )); - } + fn add(&mut self, x: String, tag: String, duration: u64) { + let now = system_time_to_unix_seconds(&SystemTime::now()); + self.xs.push(new_x(now - duration as i64, x, tag)); + } } impl X { - fn ts(&self) -> SystemTime { - UNIX_EPOCH.add(Duration::from_secs(self.t.try_into().unwrap())) - } + fn ts(&self) -> SystemTime { + UNIX_EPOCH.add(Duration::from_secs(self.t.try_into().unwrap())) + } - fn timestamp(&self, precision: &u32) -> String { - timestamp(&self.t, &precision) - } + fn timestamp(&self, precision: &u32) -> String { + timestamp(&self.t, &precision) + } } fn system_time_to_unix_seconds(st: &SystemTime) -> i64 { - st.duration_since(UNIX_EPOCH).unwrap().as_secs() as i64 + st.duration_since(UNIX_EPOCH).unwrap().as_secs() as i64 } fn timestamp(t: &i64, precision: &u32) -> String { - let dt = Local.timestamp_opt(*t, 0).unwrap(); - match precision { - 0 => dt.format("%Y-%m-%d").to_string(), - 1 => dt.format("%Y-%m-%dT%H:%M").to_string(), - _ => dt.format("%Y-%m-%dT%H:%M:%S").to_string(), - } + let dt = Local.timestamp_opt(*t, 0).unwrap(); + match precision { + 0 => dt.format("%Y-%m-%d").to_string(), + 1 => dt.format("%Y-%m-%dT%H:%M").to_string(), + _ => dt.format("%Y-%m-%dT%H:%M:%S").to_string(), + } } fn new_x(t: i64, x: String, tag: String) -> X { - X{ - t: t, - x: x, - tag: tag, - } + X { + t: t, + x: x, + tag: tag, + } } #[cfg(test)] mod test_tsheet { - use super::*; + use super::*; - #[test] - fn test_add() { - let mut given = TSheet{xs: vec![ - X{t: 1, x: "def".to_string(), tag: "abc".to_string()}, - ]}; - given.add("ghi".to_string(), "".to_string(), 0); - assert_eq!(given.xs.len(), 2); - assert!(given.xs[1].t != 1); - assert_eq!(given.xs[1].x, "ghi".to_string()); - assert_eq!(given.xs[1].tag, "".to_string()); - } + #[test] + fn test_add() { + let mut given = TSheet { + xs: vec![X { + t: 1, + x: "def".to_string(), + tag: "abc".to_string(), + }], + }; + given.add("ghi".to_string(), "".to_string(), 0); + assert_eq!(given.xs.len(), 2); + assert!(given.xs[1].t != 1); + assert_eq!(given.xs[1].x, "ghi".to_string()); + assert_eq!(given.xs[1].tag, "".to_string()); + } - #[test] - fn test_since_date() { - let given = TSheet{xs: vec![ - X{t: 1, x: "def".to_string(), tag: "abc".to_string()}, - X{t: 2, x: "ghi".to_string(), tag: "".to_string()}, - X{t: 3, x: "jkl".to_string(), tag: "".to_string()}, - ]}; - let want = TSheet{xs: vec![ - X{t: 2, x: "ghi".to_string(), tag: "".to_string()}, - X{t: 3, x: "jkl".to_string(), tag: "".to_string()}, - ]}; - let got = given.since(UNIX_EPOCH.add(Duration::from_secs(2))); - assert_eq!(got, want); - } + #[test] + fn test_since_date() { + let given = TSheet { + xs: vec![ + X { + t: 1, + x: "def".to_string(), + tag: "abc".to_string(), + }, + X { + t: 2, + x: "ghi".to_string(), + tag: "".to_string(), + }, + X { + t: 3, + x: "jkl".to_string(), + tag: "".to_string(), + }, + ], + }; + let want = TSheet { + xs: vec![ + X { + t: 2, + x: "ghi".to_string(), + tag: "".to_string(), + }, + X { + t: 3, + x: "jkl".to_string(), + tag: "".to_string(), + }, + ], + }; + let got = given.since(UNIX_EPOCH.add(Duration::from_secs(2))); + assert_eq!(got, want); + } }