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 clap::Parser; use chrono::{TimeZone, Local, Timelike}; #[derive(Debug, Parser)] struct Flags { #[arg(short = 'f', long = "file")] f: String, #[arg(short = 'l', long = "log")] log: bool, #[arg(short = 's', long = "since")] since: Option, #[arg(short = 'a', long = "add")] add: Option, #[arg(short = 't', long = "tag")] tag: Option, } fn main() { let flags = Flags::parse(); add(&flags.f, &flags.add, &flags.tag).unwrap(); log(&flags.f, &flags.log, &flags.since).unwrap(); } fn add(f: &String, x: &Option, tag: &Option) -> Result<(), String> { match x { Some(x) => { let mut tsheet = load(&f)?; tsheet.add( x.to_string(), tag.clone().unwrap_or("".to_string()), ); save(&f, tsheet)?; }, None => {}, }; Ok(()) } #[derive(Debug, Serialize, Clone)] struct Log { t: String, d: f32, xs: Vec, } #[derive(Debug, Serialize, Clone)] struct LogX { d: f32, x: String, } fn log(f: &String, enabled: &bool, since: &Option) -> Result<(), String> { if !enabled { return Ok(()); } let since = parse_time(since)?; let tsheet = load(&f)?; let tsheet = tsheet.since(since); let tsheet = tsheet.sorted(); let mut result = vec![]; let mut curr = Log{t: "".to_string(), d: 0.0, xs: vec![]}; for i in 0..tsheet.xs.len() { let x = &tsheet.xs[i]; if curr.t != x.timestamp() { if curr.xs.len() > 0 { result.push(curr.clone()); } curr.xs.truncate(0); curr.t = x.timestamp(); curr.d = 0.0; } let d = match curr.xs.len() { 0 if x.x.len() == 0 => 0.0, 0 => 1.0, _ => ((tsheet.xs[i].t - tsheet.xs[i-1].t) as f32 / (60.0*60.0)) as f32, }; curr.t = x.timestamp(); curr.xs.push(LogX{d: d, x: x.x.clone()}); } if curr.xs.len() > 0 { result.push(curr.clone()); } for i in result.iter_mut() { i.d = i.xs.iter().map(|x| x.d).sum(); } for log in result { for x in log.xs { if x.x.len() > 0 { println!("{} ({}) {} ({:.1})", log.t, (0.5 + log.d) as i64, x.x, x.d + 0.05); } } } Ok(()) } fn parse_time(since: &Option) -> Result { 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) )) }, Err(msg) => Err(format!("failed to parse {}: {}", since, msg)), } }, None => Ok(SystemTime::now().sub(Duration::from_secs(Local::now().hour() as u64*60*60))), } } #[derive(Debug, PartialEq, Serialize, Deserialize)] struct TSheet { xs: Vec, } #[derive(Debug, PartialEq, Serialize, Deserialize, Clone, Ord, Eq, PartialOrd)] struct X { 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)), } } 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)), } } 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)), } } 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)), } } #[cfg(test)] mod test_save_load { 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![]}); 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, ); 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 add(&mut self, x: String, tag: String) { self.xs.push(new_x(SystemTime::now(), x, tag)); } fn sorted(&self) -> TSheet { let mut result = TSheet{xs: self.xs.clone()}; result.xs.sort(); return result; } } impl X { fn ts(&self) -> SystemTime { UNIX_EPOCH.add(Duration::from_secs(self.t.try_into().unwrap())) } fn timestamp(&self) -> String { let dt = Local.timestamp_opt(self.t, 0).unwrap(); dt.format("%Y-%m-%d").to_string() } } fn new_x(t: SystemTime, x: String, tag: String) -> X { X{ t: t.duration_since(UNIX_EPOCH).unwrap().as_secs() as i64, x: x, tag: tag, } } #[cfg(test)] mod test_tsheet { 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()); 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); } }