use std::convert::TryFrom; use std::io::Write; use std::str::FromStr; use clap::ArgMatches; use chrono::{DateTime, Utc, Local, LocalResult, TimeZone}; use ansi_term::Color::Yellow; use regex::Regex; use crate::error::{Result, Error}; use crate::database::{Database, DBVersion}; use crate::formatters::Formatter; use crate::config::Config; use crate::timeparse::parse_time; use crate::models::Entry; use crate::regex::parse_regex; use super::Command; // ---------------------------------------------------------------- // Things that are general to all commands that display in some way // ---------------------------------------------------------------- fn local_to_utc(t: DateTime) -> Result> { let local_time = match Local.from_local_datetime(&t.naive_utc()) { LocalResult::None => return Err(Error::NoneLocalTime(t.naive_utc().to_string())), LocalResult::Single(t) => t, LocalResult::Ambiguous(t1, t2) => return Err(Error::AmbiguousLocalTime { orig: t.naive_utc().to_string(), t1: t1.naive_local(), t2: t2.naive_local(), }), }; Ok(Utc.from_utc_datetime(&local_time.naive_utc())) } fn local_to_utc_vec(entries: Vec) -> Result> { entries .into_iter() .map(|e| { Ok(Entry { start: local_to_utc(e.start)?, end: e.end.map(|t| local_to_utc(t)).transpose()?, ..e }) }) .collect() } pub enum Sheet { All, Full, Sheet(String), } impl FromStr for Sheet { type Err = Error; fn from_str(name: &str) -> Result { Ok(match name { "all" => Sheet::All, "full" => Sheet::Full, name => Sheet::Sheet(name.into()), }) } } pub fn entries_for_display( start: Option>, end: Option>, sheet: Option, db: &mut D, out: &mut O, err: &mut E, format: Formatter, ids: bool, grep: Option, ) -> Result<()> where D: Database, O: Write, E: Write, { let mut entries = match sheet { Some(Sheet::All) => db.entries_all_visible(start, end)?, Some(Sheet::Full) => db.entries_full(start, end)?, Some(Sheet::Sheet(name)) => db.entries_by_sheet(&name, start, end)?, None => { let current_sheet = db.current_sheet()?.unwrap_or("default".into()); db.entries_by_sheet(¤t_sheet, start, end)? } }; if let Some(re) = grep { entries.retain(|e| re.is_match(e.note.as_ref().unwrap_or(&String::new()))); } let (entries, needs_warning) = if let DBVersion::Timetrap = db.version()? { // this indicates that times in the database are specified in the // local time and need to be converted to utc before displaying (local_to_utc_vec(entries)?, true) } else { (entries, false) }; format.print_formatted( entries, out, Utc::now(), ids, )?; if needs_warning { writeln!( err, "{} You are using the old timetrap format, it is advised that \ you update your database using t migrate", Yellow.bold().paint("[WARNING]"), )?; } Ok(()) } // ------------------------------------ // The actual implementation of display // ------------------------------------ #[derive(Default)] pub struct Args { ids: bool, start: Option>, end: Option>, format: Formatter, grep: Option, sheet: Option, } impl<'a> TryFrom<&'a ArgMatches<'a>> for Args { type Error = Error; fn try_from(matches: &'a ArgMatches) -> Result { Ok(Args { ids: matches.is_present("ids"), start: matches.value_of("start").map(|s| parse_time(s)).transpose()?, end: matches.value_of("end").map(|s| parse_time(s)).transpose()?, format: matches.value_of("format").unwrap().parse()?, grep: matches.value_of("grep").map(parse_regex).transpose()?, sheet: matches.value_of("sheet").map(|s| s.parse()).transpose()?, }) } } pub struct DisplayCommand { } impl<'a> Command<'a> for DisplayCommand { type Args = Args; fn handle(args: Self::Args, db: &mut D, out: &mut O, err: &mut E, _config: &Config) -> Result<()> where D: Database, O: Write, E: Write, { entries_for_display(args.start, args.end, args.sheet, db, out, err, args.format, args.ids, args.grep) } } #[cfg(test)] mod tests { use chrono::TimeZone; use ansi_term::Color::Yellow; use pretty_assertions::assert_eq; use crate::database::SqliteDatabase; use crate::test_utils::PrettyString; use super::*; #[test] fn display_as_local_time_if_previous_version() { let args = Default::default(); let mut db = SqliteDatabase::from_path("assets/test_old_db.db").unwrap(); let mut out = Vec::new(); let mut err = Vec::new(); let config = Default::default(); std::env::set_var("TZ", "UTC"); DisplayCommand::handle(args, &mut db, &mut out, &mut err, &config).unwrap(); assert_eq!(PrettyString(&String::from_utf8_lossy(&out)), PrettyString("Timesheet: default Day Start End Duration Notes Tue Jun 29, 2021 06:26:49 - 07:26:52 1:00:03 lets do some rust 1:00:03 ------------------------------------------------------------------- Total 1:00:03 ")); assert_eq!( String::from_utf8_lossy(&err), format!("{} You are using the old timetrap format, it is advised that you update your database using t migrate\n", Yellow.bold().paint("[WARNING]")), ); } #[test] fn correctly_use_utc_if_new_version() { assert!(false, "start with a newly created database"); assert!(false, "correctly display times in local timezone"); } #[test] fn filter_by_start() { let args = Args { format: Formatter::Csv, start: Some(Utc.ymd(2021, 6, 30).and_hms(10, 5, 0)), ..Default::default() }; let mut db = SqliteDatabase::from_memory().unwrap(); let mut out = Vec::new(); let mut err = Vec::new(); let config = Default::default(); db.init().unwrap(); db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default".into()).unwrap(); db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("hola".into()), "default".into()).unwrap(); DisplayCommand::handle(args, &mut db, &mut out, &mut err, &config).unwrap(); assert_eq!(PrettyString(&String::from_utf8_lossy(&out)), PrettyString("start,end,note,sheet 2021-06-30T10:10:00Z,,hola,default ")); assert_eq!( String::from_utf8_lossy(&err), String::new(), ); } #[test] fn filter_by_match() { let mut db = SqliteDatabase::from_memory().unwrap(); let mut out = Vec::new(); let mut err = Vec::new(); db.init().unwrap(); db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 0, 0), None, Some("hola".into()), "default".into()).unwrap(); db.entry_insert(Utc.ymd(2021, 6, 30).and_hms(10, 10, 0), None, Some("adios".into()), "default".into()).unwrap(); entries_for_display(None, None, None, &mut db, &mut out, &mut err, Formatter::Csv, true, Some("io".parse().unwrap())).unwrap(); assert_eq!(PrettyString(&String::from_utf8_lossy(&out)), PrettyString("id,start,end,note,sheet 2,2021-06-30T10:10:00Z,,adios,default ")); assert_eq!( String::from_utf8_lossy(&err), String::new(), ); } #[test] fn displays_grand_total_when_multiple_sheets() { unimplemented!() } }