report command

This commit is contained in:
Nikolas Schmidt-Voigt 2021-09-12 17:23:01 +02:00
parent 2034cd78a7
commit 1ec7b00c4a
6 changed files with 193 additions and 86 deletions

View file

@ -60,12 +60,6 @@ This continues the last activity. If an activity is currently tracked, bartib st
The `-t/--time` option specifies at which time of the current day the activity (re-)starts.
### List all currently running activities
```
bartib current
```
### List activities
All activities:
@ -99,6 +93,12 @@ bartib list --today
bartib list --yesterday
```
### List all currently running activities
```
bartib current
```
### Edit activities
To change tracked activities, just open the file with your activities log in any text editor. To facilitate this, bartib offers the `edit` subcommand:

View file

@ -25,8 +25,7 @@ pub fn list(file_name: &str, filter: getter::ActivityFilter, do_group_activities
filtered_activities.sort_by_key(|activity| activity.start);
let first_element =
get_index_of_first_element(filtered_activities.len(), filter.number_of_activities);
let first_element =filtered_activities.len().saturating_sub(filter.number_of_activities.unwrap_or(filtered_activities.len()));
if do_group_activities {
list::list_activities_grouped_by_date(
@ -74,12 +73,4 @@ pub fn display_last_activity(file_name: &str) -> Result<()> {
}
Ok(())
}
fn get_index_of_first_element(length: usize, sub: Option<usize>) -> usize {
if let Some(s) = sub {
length.saturating_sub(s)
} else {
0
}
}

View file

@ -1,2 +1,3 @@
pub mod list;
pub mod manipulation;
pub mod manipulation;
pub mod report;

22
src/controller/report.rs Normal file
View file

@ -0,0 +1,22 @@
use anyhow::Result;
use crate::data::activity;
use crate::data::bartib_file;
use crate::data::getter;
use crate::view::report;
pub fn show_report(file_name: &str, filter: getter::ActivityFilter) -> Result<()> {
let file_content = bartib_file::get_file_content(file_name)?;
let activities = getter::get_activities(&file_content);
let mut filtered_activities: Vec<&activity::Activity> =
getter::filter_activities(activities, &filter).collect();
filtered_activities.sort_by_key(|activity| activity.start);
let first_element = filtered_activities.len().saturating_sub(filter.number_of_activities.unwrap_or(filtered_activities.len()));
report::show_activities(&filtered_activities[first_element..filtered_activities.len()]);
Ok(())
}

View file

@ -18,6 +18,41 @@ fn main() -> Result<()> {
.help("the time for changing the activity status (HH:MM)")
.takes_value(true);
let arg_from_date = Arg::with_name("from_date")
.long("from")
.value_name("FROM_DATE")
.help("begin of date range (inclusive)")
.takes_value(true);
let arg_to_date = Arg::with_name("to_date")
.long("to")
.value_name("TO_DATE")
.help("end of date range (inclusive)")
.takes_value(true);
let arg_date = Arg::with_name("date")
.short("d")
.long("date")
.value_name("DATE")
.help("show activities of a certain date only")
.required(false)
.conflicts_with_all(&["from_date", "to_date"])
.takes_value(true);
let arg_today = Arg::with_name("today")
.long("today")
.help("show activities of the current day")
.required(false)
.conflicts_with_all(&["from_date", "to_date", "date", "yesterday"])
.takes_value(false);
let arg_yesterday = Arg::with_name("yesterday")
.long("yesterday")
.help("show yesterdays activities")
.required(false)
.conflicts_with_all(&["from_date", "to_date", "date", "today"])
.takes_value(false);
let matches = App::new("bartib")
.version("0.1")
.author("Nikolas Schmidt-Voigt <nikolas.schmidt-voigt@posteo.de>")
@ -83,6 +118,16 @@ fn main() -> Result<()> {
.subcommand(
SubCommand::with_name("list")
.about("list recent activities")
.arg(&arg_from_date)
.arg(&arg_to_date)
.arg(&arg_date)
.arg(&arg_today)
.arg(&arg_yesterday)
.arg(
Arg::with_name("no_grouping")
.long("no_grouping")
.help("do not group activities by date in list"),
)
.arg(
Arg::with_name("number")
.short("n")
@ -91,53 +136,17 @@ fn main() -> Result<()> {
.help("maximum number of activities to display")
.required(false)
.takes_value(true),
)
.arg(
Arg::with_name("from_date")
.long("from")
.value_name("FROM_DATE")
.help("begin of date range (inclusive)")
.takes_value(true),
)
.arg(
Arg::with_name("to_date")
.long("to")
.value_name("TO_DATE")
.help("end of date range (inclusive)")
.takes_value(true),
)
.arg(
Arg::with_name("date")
.short("d")
.long("date")
.value_name("DATE")
.help("show activities of a certain date only")
.required(false)
.conflicts_with_all(&["from_date", "to_date"])
.takes_value(true),
)
.arg(
Arg::with_name("today")
.long("today")
.help("show activities of the current day")
.required(false)
.conflicts_with_all(&["from_date", "to_date", "date", "yesterday"])
.takes_value(false),
)
.arg(
Arg::with_name("yesterday")
.long("yesterday")
.help("show yesterdays activities")
.required(false)
.conflicts_with_all(&["from_date", "to_date", "date", "today"])
.takes_value(false),
)
.arg(
Arg::with_name("no_grouping")
.long("no_grouping")
.help("do not group activities by date in list"),
),
)
.subcommand(
SubCommand::with_name("report")
.about("reports duration of tracked activities")
.arg(&arg_from_date)
.arg(&arg_to_date)
.arg(&arg_date)
.arg(&arg_today)
.arg(&arg_yesterday),
)
.subcommand(SubCommand::with_name("last").about("displays last finished acitivity"))
.subcommand(SubCommand::with_name("projects").about("list all projects"))
.subcommand(
@ -186,6 +195,25 @@ fn run_subcommand(matches: &ArgMatches, file_name: &str) -> Result<()> {
}
("current", Some(_)) => bartib::controller::list::list_running(file_name),
("list", Some(sub_m)) => {
let mut filter = bartib::data::getter::ActivityFilter {
number_of_activities: None,
from_date: get_date_argument_or_ignore(sub_m.value_of("from_date"), "--from"),
to_date: get_date_argument_or_ignore(sub_m.value_of("to_date"), "--to"),
date: get_date_argument_or_ignore(sub_m.value_of("date"), "-d/--date"),
};
if sub_m.is_present("today") {
filter.date = Some(Local::now().naive_local().date());
}
if sub_m.is_present("yesterday") {
filter.date = Some(Local::now().naive_local().date() - Duration::days(1));
}
let do_group_activities = !sub_m.is_present("no_grouping") && !filter.date.is_some();
bartib::controller::list::list(file_name, filter, do_group_activities)
}
("report", Some(sub_m)) => {
let mut filter = bartib::data::getter::ActivityFilter {
number_of_activities: get_number_argument_or_ignore(
sub_m.value_of("number"),
@ -204,10 +232,9 @@ fn run_subcommand(matches: &ArgMatches, file_name: &str) -> Result<()> {
filter.date = Some(Local::now().naive_local().date() - Duration::days(1));
}
let do_group_activities = !sub_m.is_present("no_grouping") && !filter.date.is_some();
bartib::controller::list::list(file_name, filter, do_group_activities)
bartib::controller::report::show_report(file_name, filter)
}
("projects", Some(_)) => bartib::controller::list::list_projects(file_name),
("projects", Some(_)) => bartib::controller::list::list_projects(file_name),
("last", Some(_)) => bartib::controller::list::display_last_activity(file_name),
("edit", Some(sub_m)) => {
let optional_editor_command = sub_m.value_of("editor");

View file

@ -1,19 +1,19 @@
use std::collections::BTreeMap;
use std::fmt;
use std::ops::Add;
use chrono::Duration;
use nu_ansi_term::Style;
use crate::data::activity;
use crate::view::format_util;
struct Report<'a> {
activities : Vec<&'a activity::Activity>
activities : &'a[&'a activity::Activity]
}
impl<'a> Report<'a> {
fn new() -> Report<'a> {
Report { activities : Vec::new() }
}
fn add(&mut self, a : &'a activity::Activity) {
self.activities.push(&a);
fn new(activities : &'a[&'a activity::Activity]) -> Report<'a> {
Report { activities }
}
fn get_project_map(&self) -> BTreeMap<&str, Vec<&'a activity::Activity>> {
@ -33,20 +33,62 @@ impl<'a> fmt::Display for Report<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let project_map = self.get_project_map();
let longest_line = get_longest_line(&project_map);
let longest_line = get_longest_line(&project_map).unwrap_or(0);
let mut total_duration = Duration::seconds(0);
for (project, activities) in project_map.iter() {
write!(f, "{}", project)?;
let project_duration = sum_duration(&activities);
total_duration = total_duration.add(project_duration);
writeln!(f, "{prefix}{project:.<width$} {duration}{suffix}",
prefix = Style::new().bold().prefix(),
project = project,
width = longest_line,
duration = format_util::format_duration(&project_duration),
suffix = Style::new().bold().infix(Style::new())
)?;
for activity in activities.iter() {
write!(f, "{}", activity)?;
writeln!(f, " {activity:.<width$} {duration}",
activity = activity.description,
width = longest_line - 4,
duration = format_util::format_duration(&activity.get_duration())
)?;
}
writeln!(f, "")?;
}
if self.activities.is_empty() {
writeln!(f, "You have not tracked any activities in the given time range")?;
} else {
writeln!(f, "{prefix}{total:.<width$} {duration}{suffix}",
prefix = Style::new().bold().prefix(),
total = "Total",
width = longest_line,
duration = format_util::format_duration(&total_duration),
suffix = Style::new().bold().infix(Style::new())
)?;
}
Ok(())
}
}
pub fn show_activities<'a>(activities : &'a[&'a activity::Activity]) {
let report = Report::new(activities);
println!("\n{}", report);
}
fn sum_duration(activities : &[&activity::Activity]) -> Duration {
let mut duration = Duration::seconds(0);
for activity in activities {
duration = duration.add(activity.get_duration());
}
duration
}
fn get_longest_line(project_map : &BTreeMap<&str, Vec<&activity::Activity>>) -> Option<usize> {
let longest_project_line = project_map.keys().map(|p| p.chars().count()).max();
let longest_activity_line = project_map.values().flatten().map(|a| a.description.chars().count() + 4).max();
@ -68,6 +110,26 @@ fn get_max_option(o1 : Option<usize>, o2: Option<usize>) -> Option<usize> {
#[cfg(test)]
mod tests {
use super::*;
use chrono::NaiveDateTime;
#[test]
fn sum_duration_test() {
let mut activities : Vec<&activity::Activity> = Vec::new();
assert_eq!(sum_duration(&activities).num_seconds(), 0);
let mut a1 = activity::Activity::start("p1".to_string(), "d1".to_string(), Some(NaiveDateTime::parse_from_str("2021-09-01 15:00:00", "%Y-%m-%d %H:%M:%S").unwrap()));
a1.end = Some(NaiveDateTime::parse_from_str("2021-09-01 15:20:00", "%Y-%m-%d %H:%M:%S").unwrap()); // 20 * 60 = 1,200 seconds
let mut a2 = activity::Activity::start("p1".to_string(), "d2".to_string(), Some(NaiveDateTime::parse_from_str("2021-09-01 15:21:00", "%Y-%m-%d %H:%M:%S").unwrap()));
a2.end = Some(NaiveDateTime::parse_from_str("2021-09-01 16:21:00", "%Y-%m-%d %H:%M:%S").unwrap()); // 60 * 60 = 3,600 seconds
let mut a3 = activity::Activity::start("p2".to_string(), "d1".to_string(), Some(NaiveDateTime::parse_from_str("2021-09-01 16:21:00", "%Y-%m-%d %H:%M:%S").unwrap()));
a3.end = Some(NaiveDateTime::parse_from_str("2021-09-02 16:21:00", "%Y-%m-%d %H:%M:%S").unwrap()); // 24 * 60 * 60 = 86,400 seconds
activities.push(&a1);
activities.push(&a2);
activities.push(&a3);
assert_eq!(sum_duration(&activities).num_seconds(), 91200);
}
#[test]
fn get_project_map() {
@ -75,10 +137,11 @@ mod tests {
let a2 = activity::Activity::start("p1".to_string(), "d2".to_string(), None);
let a3 = activity::Activity::start("p2".to_string(), "d1".to_string(), None);
let mut r = Report::new();
r.add(&a1);
r.add(&a2);
r.add(&a3);
let mut activities : Vec<&activity::Activity> = Vec::new();
activities.push(&a1);
activities.push(&a2);
activities.push(&a3);
let r = Report::new(&activities);
let m = r.get_project_map();
@ -87,7 +150,8 @@ mod tests {
#[test]
fn get_longest_line_test() {
let mut r = Report::new();
let mut activities : Vec<&activity::Activity> = Vec::new();
let mut r = Report::new(&activities);
let project_map1 = r.get_project_map();
// keine Einträge -> keine Längste Zeile
@ -99,11 +163,12 @@ mod tests {
let a4 = activity::Activity::start("p2".to_string(), "d1".to_string(), None);
let a5 = activity::Activity::start("p2".to_string(), "d1".to_string(), None);
r.add(&a1);
r.add(&a2);
r.add(&a3);
r.add(&a4);
r.add(&a5);
activities.push(&a1);
activities.push(&a2);
activities.push(&a3);
activities.push(&a4);
activities.push(&a5);
r = Report::new(&activities);
// längste Zeile ist Description + 4
let project_map2 = r.get_project_map();
@ -111,7 +176,8 @@ mod tests {
// längste Zeile ist Projektname mit 8 Zeichen
let a6 = activity::Activity::start("p1234567".to_string(), "d1".to_string(), None);
r.add(&a6);
activities.push(&a6);
r = Report::new(&activities);
let project_map3 = r.get_project_map();
assert_eq!(get_longest_line(&project_map3).unwrap(), 8);