mirror of
https://github.com/nikolassv/bartib
synced 2024-09-20 22:21:57 +00:00
report command
This commit is contained in:
parent
2034cd78a7
commit
1ec7b00c4a
6 changed files with 193 additions and 86 deletions
12
README.md
12
README.md
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
pub mod list;
|
||||
pub mod manipulation;
|
||||
pub mod manipulation;
|
||||
pub mod report;
|
22
src/controller/report.rs
Normal file
22
src/controller/report.rs
Normal 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(())
|
||||
}
|
123
src/main.rs
123
src/main.rs
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Reference in a new issue