mirror of
https://github.com/nikolassv/bartib
synced 2024-11-28 06:20:28 +00:00
Merge pull request #32 from berkes/round
Introduce an option to round to neares N minutes or hours.
This commit is contained in:
commit
4485d79316
10 changed files with 200 additions and 10 deletions
|
@ -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
|
||||
|
|
|
@ -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<Duration>,
|
||||
}
|
||||
|
|
|
@ -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<activity::Activity> =
|
||||
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);
|
||||
|
||||
|
|
|
@ -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<activity::Activity> =
|
||||
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);
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ use thiserror::Error;
|
|||
|
||||
use crate::conf;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Activity {
|
||||
pub start: NaiveDateTime,
|
||||
pub end: Option<NaiveDateTime>,
|
||||
|
|
|
@ -75,9 +75,9 @@ pub fn get_activities(
|
|||
}
|
||||
|
||||
pub fn filter_activities<'a>(
|
||||
activities: impl Iterator<Item = &'a activity::Activity>,
|
||||
activities: Vec<&'a activity::Activity>,
|
||||
filter: &'a ActivityFilter,
|
||||
) -> impl Iterator<Item = &'a activity::Activity> {
|
||||
) -> 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]
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
pub mod activity;
|
||||
pub mod bartib_file;
|
||||
pub mod getter;
|
||||
pub mod processor;
|
||||
pub mod round_util;
|
||||
|
|
43
src/data/processor.rs
Normal file
43
src/data/processor.rs
Normal file
|
@ -0,0 +1,43 @@
|
|||
use chrono::Duration;
|
||||
|
||||
use crate::data::activity;
|
||||
use crate::data::round_util::round_datetime;
|
||||
|
||||
pub type ProcessorList = Vec<Box<dyn ActivityProcessor>>;
|
||||
|
||||
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<activity::Activity> {
|
||||
activities
|
||||
.into_iter()
|
||||
.cloned()
|
||||
.map(|activity| {
|
||||
processors
|
||||
.iter()
|
||||
.fold(activity, |activity, processor| processor.process(&activity))
|
||||
})
|
||||
.collect()
|
||||
}
|
68
src/data/round_util.rs
Normal file
68
src/data/round_util.rs
Normal file
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
54
src/main.rs
54
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 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);
|
||||
|
||||
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<Box<dyn processor::ActivityProcessor>> = 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<chrono::Duration> {
|
||||
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<i64> = 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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue