From 26af37bff2dd513a76c634393d2c2155b2f61319 Mon Sep 17 00:00:00 2001 From: Abraham Toriz Date: Thu, 21 Jul 2022 21:36:05 +0800 Subject: [PATCH] tabulate interactive's output --- src/commands/resume.rs | 23 -------- src/database.rs | 1 + src/formatters/text.rs | 14 ++--- src/interactive.rs | 121 +++++++++++++++++++++++++++++++++-------- 4 files changed, 107 insertions(+), 52 deletions(-) diff --git a/src/commands/resume.rs b/src/commands/resume.rs index adfb08b..0af11d8 100644 --- a/src/commands/resume.rs +++ b/src/commands/resume.rs @@ -171,27 +171,4 @@ No entry to resume in the sheet 'default'. Perhaps start a new one? Hint: use t in "); } - - #[test] - fn resume_interactive() { - let args = Args { - entry: SelectedEntry::Interactive, - at: None, - }; - let mut streams = Streams::fake(b"1\n"); - let facts = Facts::new(); - let one_hour_ago = facts.now - Duration::hours(1); - let two_hours_ago = facts.now - Duration::hours(2); - - // insert some entries to pick from - streams.db.entry_insert(two_hours_ago, Some(one_hour_ago), Some("fake note".into()), "default").unwrap(); - - // call the command interactively - ResumeCommand::handle(args, &mut streams, &facts).unwrap(); - - // check the output - assert_str_eq!(&String::from_utf8_lossy(&streams.out), "Resuming \"fake note\" -Checked into sheet \"default\".\n"); - assert_str_eq!(&String::from_utf8_lossy(&streams.err), ""); - } } diff --git a/src/database.rs b/src/database.rs index 1c87311..1832995 100644 --- a/src/database.rs +++ b/src/database.rs @@ -59,6 +59,7 @@ pub trait Database { // ------------- // Entry queries // ------------- + /// Return entries from a sheet ordered by the start date ascending fn entries_by_sheet(&self, sheet: &str, start: Option>, end: Option>) -> Result> { match (start, end) { (Some(start), Some(end)) => { diff --git a/src/formatters/text.rs b/src/formatters/text.rs index a989a4a..5cc3992 100644 --- a/src/formatters/text.rs +++ b/src/formatters/text.rs @@ -46,20 +46,20 @@ pub fn print_formatted(entries: Vec, out: &mut W, facts: &Facts // A vector of lines to be printed, with all the components let mut tabs = Tabulate::with_columns(vec![ - Col::min_width(3).and_alignment(Right), - Col::min_width(18).and_alignment(Left), - Col::min_width(21).and_alignment(Left), - Col::min_width(8).and_alignment(Right), - Col::min_width(5).max_width(44).and_alignment(Left), + Col::new().min_width(3).and_alignment(Right), + Col::new().min_width(18).and_alignment(Left), + Col::new().min_width(21).and_alignment(Left), + Col::new().min_width(8).and_alignment(Right), + Col::new().min_width(5).max_width(44).and_alignment(Left), ]); if ids { tabs.feed(vec![ - "ID".into(), "Day".into(), "Start End".into(), "Duration".into(), "Notes".into(), + "ID", "Day", "Start End", "Duration", "Notes", ]); } else { tabs.feed(vec![ - "".into(), "Day".into(), "Start End".into(), "Duration".into(), "Notes".into(), + "", "Day", "Start End", "Duration", "Notes", ]); } diff --git a/src/interactive.rs b/src/interactive.rs index 1571aad..b017d0a 100644 --- a/src/interactive.rs +++ b/src/interactive.rs @@ -1,13 +1,15 @@ use std::io::{self, BufRead, Write}; -use std::collections::{HashMap, hash_map}; +use std::collections::HashMap; -use chrono::{DateTime, Utc}; +use chrono::{DateTime, Utc, Duration}; use crate::io::Streams; use crate::database::Database; use crate::error::Result; use crate::commands::Facts; use crate::models::Entry; +use crate::tabulate::{Tabulate, Col, Align}; +use crate::formatters::text::format_duration; fn read_line(mut r#in: I) -> io::Result { let mut pre_n = String::new(); @@ -64,32 +66,72 @@ where let entries = streams.db.entries_by_sheet(current_sheet, None, None)?; let mut uniques = HashMap::new(); - entries - .into_iter().rev() - .filter_map(|e| e.note.map(|n| (n, e.start))) - .map(|(n, s)| if let hash_map::Entry::Vacant(e) = uniques.entry(n) { - e.insert(s); - true - } else { - false - }) - .filter(|&i| i) - .take(facts.config.interactive_entries) - .count(); + struct GroupedEntry { + note: String, + last_start: DateTime, + accumulated_time: Duration, + } - let mut uniques: Vec<_> = uniques.into_iter().collect(); - uniques.sort_unstable_by_key(|(_n, s)| *s); + // From all possible entries belonging to this sheet keep only those with a + // note + let entries_with_notes = entries + // Iterate all entries + .into_iter() + // preserve only those with a text note + .filter_map(|e| e.note.map(|n| GroupedEntry { + note: n, + last_start: e.start, + accumulated_time: e.end.unwrap_or(facts.now) - e.start, + })); + + // iterate over the entries with a note and group them into `uniques` + // accumulating their elapsed times and recording the last time it was + // started + for entry in entries_with_notes { + let mut e = uniques.entry(entry.note.clone()).or_insert(GroupedEntry { + accumulated_time: Duration::seconds(0), + ..entry + }); + + if entry.last_start > e.last_start { + e.last_start = entry.last_start; + } + + e.accumulated_time = e.accumulated_time + entry.accumulated_time; + } + + // turn uniques into a vector and sort it by the time it was last started + let mut uniques: Vec<_> = uniques.into_values().collect(); + uniques.sort_unstable_by_key(|e| e.last_start); writeln!(streams.out, "Latest entries of sheet '{current_sheet}':\n")?; let formatter = timeago::Formatter::new(); - for (i, (note, time)) in uniques.iter().enumerate() { - let i = i + 1; - let ago = formatter.convert_chrono(*time, facts.now); + // Create a table for nicer output + let mut table = Tabulate::with_columns(vec![ + Col::new().min_width(3).and_alignment(Align::Right), // option number + Col::new(), // note + Col::new().and_alignment(Align::Right), // acumulated time + Col::new().min_width(13).and_alignment(Align::Right), // last started + ]); + + table.feed(vec!["#", "Note", "Total time", "Last started"]); + table.separator(' '); + + for (i, entry) in uniques.iter().enumerate() { + let i = i + 1; + let ago = formatter.convert_chrono(entry.last_start, facts.now); + + table.feed(vec![ + i.to_string(), + entry.note.clone(), + format_duration(entry.accumulated_time), + ago, + ]); - writeln!(streams.out, " {i}) {note} ({ago})")?; } + write!(streams.out, "{}", table.print(false))?; writeln!(streams.out, "\nenter number or q to cancel")?; @@ -100,8 +142,8 @@ where let choice = to_choice(read_line(&mut streams.r#in)?); match choice { - Choice::Number(i) => if let Some((n, _s)) = uniques.get(i - 1) { - return Ok(Some(n.clone())); + Choice::Number(i) => if let Some(e) = uniques.get(i - 1) { + return Ok(Some(e.note.clone())); } else { writeln!(streams.out, "Not an option")?; } @@ -151,3 +193,38 @@ are you sure you want to delete entry {id} with note Ok(()) } + +#[cfg(test)] +mod tests { + use chrono::Duration; + use pretty_assertions::assert_str_eq; + + use super::*; + + #[test] + fn interactive_choice_of_tasks() { + let mut streams = Streams::fake(b"1\n"); + let facts = Facts::new(); + let one_hour_ago = facts.now - Duration::hours(1); + let two_hours_ago = facts.now - Duration::hours(2); + + // insert some entries to pick from + streams.db.entry_insert(two_hours_ago, Some(one_hour_ago), Some("first task".into()), "default").unwrap(); + streams.db.entry_insert(one_hour_ago, Some(facts.now), Some("second task".into()), "default").unwrap(); + + // call the command interactively + note_from_last_entries(&mut streams, &facts, "default").unwrap(); + + // check the output + assert_str_eq!(&String::from_utf8_lossy(&streams.out), "Latest entries of sheet 'default': + + # Note Total time Last started + + 1 first task 1:00:00 2 hours ago + 2 second task 1:00:00 1 hour ago + +enter number or q to cancel +>> "); + assert_str_eq!(&String::from_utf8_lossy(&streams.err), ""); + } +}