From 8c1a8a909df6b03569cbb9eb96f22cd1833b735e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A8r=20Kessels?= Date: Tue, 14 Nov 2023 18:26:22 +0100 Subject: [PATCH 1/3] Introduce an option to round to neares N minutes or hours. --- src/conf.rs | 7 +++++ src/controller/list.rs | 10 ++++-- src/controller/report.rs | 16 ++++++++-- src/data/activity.rs | 2 +- src/data/getter.rs | 6 ++-- src/data/mod.rs | 2 ++ src/data/processor.rs | 43 +++++++++++++++++++++++++ src/data/round_util.rs | 68 ++++++++++++++++++++++++++++++++++++++++ src/main.rs | 54 +++++++++++++++++++++++++++++-- 9 files changed, 198 insertions(+), 10 deletions(-) create mode 100644 src/data/processor.rs create mode 100644 src/data/round_util.rs diff --git a/src/conf.rs b/src/conf.rs index d3a55ca..710fb09 100644 --- a/src/conf.rs +++ b/src/conf.rs @@ -1,5 +1,12 @@ +use chrono::Duration; + pub static FORMAT_DATETIME: &str = "%F %R"; pub static FORMAT_TIME: &str = "%R"; pub static FORMAT_DATE: &str = "%F"; pub static DEFAULT_WIDTH: usize = usize::MAX; pub static REPORT_INDENTATION: usize = 4; + +#[derive(Debug)] +pub struct ProcessConfig { + pub round: Option, +} diff --git a/src/controller/list.rs b/src/controller/list.rs index 3e5dbad..dfbd8e4 100644 --- a/src/controller/list.rs +++ b/src/controller/list.rs @@ -6,6 +6,7 @@ use crate::data::activity; use crate::data::activity::Activity; use crate::data::bartib_file; use crate::data::getter; +use crate::data::processor; use crate::view::list; // lists all currently running activities. @@ -25,11 +26,16 @@ pub fn list( file_name: &str, filter: getter::ActivityFilter, do_group_activities: bool, + processors: processor::ProcessorList, ) -> Result<()> { let file_content = bartib_file::get_file_content(file_name)?; - let activities = getter::get_activities(&file_content); + let activities = getter::get_activities(&file_content).collect(); + let processed_activities_bind: Vec = + processor::process_activities(activities, processors); + let processed_activities: Vec<&activity::Activity> = processed_activities_bind.iter().collect(); + let mut filtered_activities: Vec<&activity::Activity> = - getter::filter_activities(activities, &filter).collect(); + getter::filter_activities(processed_activities, &filter); filtered_activities.sort_by_key(|activity| activity.start); diff --git a/src/controller/report.rs b/src/controller/report.rs index 0e9e220..53b2499 100644 --- a/src/controller/report.rs +++ b/src/controller/report.rs @@ -3,13 +3,23 @@ use anyhow::Result; use crate::data::activity; use crate::data::bartib_file; use crate::data::getter; +use crate::data::processor; use crate::view::report; -pub fn show_report(file_name: &str, filter: getter::ActivityFilter) -> Result<()> { +pub fn show_report( + file_name: &str, + filter: getter::ActivityFilter, + processors: processor::ProcessorList, +) -> Result<()> { let file_content = bartib_file::get_file_content(file_name)?; - let activities = getter::get_activities(&file_content); + let activities = getter::get_activities(&file_content).collect(); + + let processed_activities_bind: Vec = + processor::process_activities(activities, processors); + let processed_activities: Vec<&activity::Activity> = processed_activities_bind.iter().collect(); + let mut filtered_activities: Vec<&activity::Activity> = - getter::filter_activities(activities, &filter).collect(); + getter::filter_activities(processed_activities, &filter); filtered_activities.sort_by_key(|activity| activity.start); diff --git a/src/data/activity.rs b/src/data/activity.rs index 837269a..09e7eda 100644 --- a/src/data/activity.rs +++ b/src/data/activity.rs @@ -5,7 +5,7 @@ use thiserror::Error; use crate::conf; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Activity { pub start: NaiveDateTime, pub end: Option, diff --git a/src/data/getter.rs b/src/data/getter.rs index d3a3a08..dcf237f 100644 --- a/src/data/getter.rs +++ b/src/data/getter.rs @@ -75,9 +75,9 @@ pub fn get_activities( } pub fn filter_activities<'a>( - activities: impl Iterator, + activities: Vec<&'a activity::Activity>, filter: &'a ActivityFilter, -) -> impl Iterator { +) -> Vec<&'a activity::Activity> { let from_date: NaiveDate; let to_date: NaiveDate; @@ -90,10 +90,12 @@ pub fn filter_activities<'a>( } activities + .into_iter() .filter(move |activity| { activity.start.date() >= from_date && activity.start.date() <= to_date }) .filter(move |activity| filter.project.map_or(true, |p| activity.project == *p)) + .collect() } #[must_use] diff --git a/src/data/mod.rs b/src/data/mod.rs index 287e742..1d5701a 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -1,3 +1,5 @@ pub mod activity; pub mod bartib_file; pub mod getter; +pub mod processor; +pub mod round_util; diff --git a/src/data/processor.rs b/src/data/processor.rs new file mode 100644 index 0000000..8aca7bb --- /dev/null +++ b/src/data/processor.rs @@ -0,0 +1,43 @@ +use chrono::Duration; + +use crate::data::activity; +use crate::data::round_util::round_datetime; + +pub type ProcessorList = Vec>; + +pub trait ActivityProcessor { + fn process(&self, activity: &activity::Activity) -> activity::Activity; +} + +pub struct RoundProcessor { + pub round: Duration, +} + +impl ActivityProcessor for RoundProcessor { + fn process(&self, activity: &activity::Activity) -> activity::Activity { + let start = round_datetime(&activity.start, &self.round); + let end = activity.end.map(|end| round_datetime(&end, &self.round)); + + activity::Activity { + start, + end, + project: activity.project.clone(), + description: activity.description.clone(), + } + } +} + +pub fn process_activities( + activities: Vec<&activity::Activity>, + processors: ProcessorList, +) -> Vec { + activities + .into_iter() + .cloned() + .map(|activity| { + processors + .iter() + .fold(activity, |activity, processor| processor.process(&activity)) + }) + .collect() +} diff --git a/src/data/round_util.rs b/src/data/round_util.rs new file mode 100644 index 0000000..b7ba08d --- /dev/null +++ b/src/data/round_util.rs @@ -0,0 +1,68 @@ +// Utility functions for rounding datetimes. +// Limitations: +// - Cannot handle days properly. +// - Does not consider DST changes, so rounding to hours may be off by an hour. +// - Does not consider leap seconds. +pub fn round_datetime( + datetime: &chrono::NaiveDateTime, + round: &chrono::Duration, +) -> chrono::NaiveDateTime { + let timestamp = datetime.timestamp(); + let round_seconds = round.num_seconds(); + + let rounded_timestamp = + (timestamp as f64 / round_seconds as f64).round() as i64 * round_seconds; + + chrono::NaiveDateTime::from_timestamp_opt(rounded_timestamp, 0).unwrap() +} + +#[cfg(test)] +mod tests { + use chrono::{Duration, NaiveDate}; + + use super::*; + + fn fake_date() -> NaiveDate { + NaiveDate::from_ymd_opt(2020, 1, 1).unwrap() + } + + #[test] + fn test_round_to_5_minutes() { + let round = Duration::minutes(5); + + assert_eq!( + round_datetime(&fake_date().and_hms_opt(13, 32, 30).unwrap(), &round), + fake_date().and_hms_opt(13, 35, 0).unwrap() + ); + + assert_eq!( + round_datetime(&fake_date().and_hms_opt(13, 31, 1).unwrap(), &round), + fake_date().and_hms_opt(13, 30, 0).unwrap() + ); + } + + #[test] + fn test_round_to_8_hours() { + let round = Duration::hours(8); + + assert_eq!( + round_datetime(&fake_date().and_hms_opt(4, 0, 1).unwrap(), &round), + fake_date().and_hms_opt(8, 0, 0).unwrap() + ); + + assert_eq!( + round_datetime(&fake_date().and_hms_opt(3, 59, 59).unwrap(), &round), + fake_date().and_hms_opt(0, 0, 0).unwrap() + ); + } + + #[test] + fn test_round_middle_rounds_up() { + let round = Duration::minutes(10); + + assert_eq!( + round_datetime(&fake_date().and_hms_opt(13, 5, 0).unwrap(), &round), + fake_date().and_hms_opt(13, 10, 0).unwrap() + ) + } +} diff --git a/src/main.rs b/src/main.rs index c30810d..f3e162e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,8 @@ use chrono::{Datelike, Duration, Local, NaiveDate, NaiveTime}; use clap::{crate_version, App, AppSettings, Arg, ArgMatches, SubCommand}; use bartib::data::getter::ActivityFilter; +use bartib::data::processor; + #[cfg(windows)] use nu_ansi_term::enable_ansi_support; @@ -75,6 +77,12 @@ fn main() -> Result<()> { ]) .takes_value(false); + let arg_group = Arg::with_name("round") + .long("round") + .help("rounds the time to the nearest duration. Durations can be in minutes or hours. E.g. 15m or 4h") + .required(false) + .takes_value(true); + let arg_description = Arg::with_name("description") .short("d") .long("description") @@ -153,6 +161,7 @@ fn main() -> Result<()> { .arg(&arg_yesterday) .arg(&arg_current_week) .arg(&arg_last_week) + .arg(&arg_group) .arg( Arg::with_name("project") .short("p") @@ -187,6 +196,7 @@ fn main() -> Result<()> { .arg(&arg_yesterday) .arg(&arg_current_week) .arg(&arg_last_week) + .arg(&arg_group) .arg( Arg::with_name("project") .short("p") @@ -299,12 +309,14 @@ fn run_subcommand(matches: &ArgMatches, file_name: &str) -> Result<()> { ("current", Some(_)) => bartib::controller::list::list_running(file_name), ("list", Some(sub_m)) => { let filter = create_filter_for_arguments(sub_m); + let processors = create_processors_for_arguments(sub_m); let do_group_activities = !sub_m.is_present("no_grouping") && filter.date.is_none(); - bartib::controller::list::list(file_name, filter, do_group_activities) + bartib::controller::list::list(file_name, filter, do_group_activities, processors) } ("report", Some(sub_m)) => { let filter = create_filter_for_arguments(sub_m); - bartib::controller::report::show_report(file_name, filter) + let processors = create_processors_for_arguments(sub_m); + bartib::controller::report::show_report(file_name, filter, processors) } ("projects", Some(sub_m)) => { bartib::controller::list::list_projects(file_name, sub_m.is_present("current")) @@ -324,6 +336,16 @@ fn run_subcommand(matches: &ArgMatches, file_name: &str) -> Result<()> { } } +fn create_processors_for_arguments(sub_m: &ArgMatches) -> processor::ProcessorList { + let mut processors: Vec> = Vec::new(); + + if let Some(round) = get_duration_argument_or_ignore(sub_m.value_of("round"), "--round") { + processors.push(Box::new(processor::RoundProcessor { round })); + } + + processors +} + fn create_filter_for_arguments<'a>(sub_m: &'a ArgMatches) -> ActivityFilter<'a> { let mut filter = ActivityFilter { number_of_activities: get_number_argument_or_ignore( @@ -432,3 +454,31 @@ fn get_time_argument_or_ignore( None } } + +fn get_duration_argument_or_ignore( + duration_argument: Option<&str>, + argument_name: &str, +) -> Option { + if let Some(duration_string) = duration_argument { + // extract last character, leave the rest as number + let (number_string, duration_unit) = duration_string.split_at(duration_string.len() - 1); + + let number: Option = number_string.parse().ok(); + + match number { + Some(number) => match duration_unit { + "m" => Some(chrono::Duration::minutes(number)), + "h" => Some(chrono::Duration::hours(number)), + _ => { + println!( + "Can not parse \"{duration_string}\" as duration. Argument for {argument_name} is ignored" + ); + None + } + }, + None => None, + } + } else { + None + } +} From 24d54e654125c20d9839862879e40f85dfe8c150 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A8r=20Kessels?= Date: Mon, 27 Nov 2023 14:28:29 +0100 Subject: [PATCH 2/3] Document the rounding feature in the README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index a4c1d7c..5a2174d 100644 --- a/README.md +++ b/README.md @@ -268,6 +268,7 @@ bartib report --last_week # create a report for the last week bartib report --date 2021-09-03 # create a report for a given day bartib report --from 2021-09-01 --to 2021-09-05 # create a report for a given time range bartib report --project "The most exciting project" # create a report for a given project +bartib report --round 15m # rounds the start and end time to the nearest duration. Durations can be in minutes or hours. E.g. 15m or 4h bartib list # list all activities grouped by day bartib list --no_grouping # list all activities but do not group them by day @@ -279,6 +280,7 @@ bartib list --last_week # list activities of the last week bartib list --date 2021-09-03 # list activities on a given day bartib list --from 2021-09-01 --to 2021-09-05 # list activities in a given time range bartib list --project "The most exciting project" # list activities for a given project +bartib list --round 15m # rounds the start and end time to the nearest duration. Durations can be in minutes or hours. E.g. 15m or 4h ``` ### Edit activities From 711b54aac8386f5b2217ab6ef6679ce1cea6a27a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A8r=20Kessels?= Date: Mon, 27 Nov 2023 14:29:37 +0100 Subject: [PATCH 3/3] Documents that its start and end time being rounded in the --help --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index f3e162e..81a02b0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -79,7 +79,7 @@ fn main() -> Result<()> { let arg_group = Arg::with_name("round") .long("round") - .help("rounds the time to the nearest duration. Durations can be in minutes or hours. E.g. 15m or 4h") + .help("rounds the start and end time to the nearest duration. Durations can be in minutes or hours. E.g. 15m or 4h") .required(false) .takes_value(true);