time entry representation change, and basic statistics

This commit is contained in:
aaron-jack-manning 2022-08-24 09:56:23 +10:00
parent 1d04723bd7
commit 43f8bc043b
4 changed files with 170 additions and 37 deletions

View File

@ -69,9 +69,15 @@ Then you can run `toru new` to create your first task.
- Error if any circular dependencies are introduced - Error if any circular dependencies are introduced
- Make sure dependencies written to file are only those that could be successfully created - Make sure dependencies written to file are only those that could be successfully created
- List dependencies as a tree on note view below info - List dependencies as a tree on note view below info
- Automatically added recurring notes system
- Time tracking
- Command to give statistics on time tracking (by tag, and for the last x days)
- Due dates - Due dates
- Taken as input when creating notes - Taken as input when creating notes
- Displayed in list view by default (with number of days remaining) - Displayed in list view by default (with number of days remaining)
- Completed Date
- Keep track of completed date, and correctly update upon marking as complete or manual edit
- Disallow removing it in a manual edit unless complete is also marked to false
- Add to statistics
- SVN integration
- Statistics
- Completed tasks over last x days
- Improve formatting to terminal to make easier to read for `tracked` command
- Automatically added recurring notes system

View File

@ -4,6 +4,7 @@ mod vault;
mod error; mod error;
mod tasks; mod tasks;
mod state; mod state;
mod stats;
mod config; mod config;
mod colour; mod colour;
@ -82,6 +83,9 @@ enum Command {
#[clap(short, default_value_t=0)] #[clap(short, default_value_t=0)]
minutes : u16, minutes : u16,
}, },
/// For statistics about the state of your vault.
#[clap(subcommand)]
Stats(StatsCommand),
/// For making changes to global configuration. /// For making changes to global configuration.
#[clap(subcommand)] #[clap(subcommand)]
Config(ConfigCommand), Config(ConfigCommand),
@ -94,6 +98,15 @@ enum Command {
}, },
} }
#[derive(clap::Subcommand, Debug, PartialEq, Eq)]
enum StatsCommand {
Tracked {
#[clap(short, long, default_value_t=7)]
days : u16,
},
}
#[derive(clap::Subcommand, Debug, PartialEq, Eq)] #[derive(clap::Subcommand, Debug, PartialEq, Eq)]
enum ConfigCommand { enum ConfigCommand {
/// For checking or changing default text editor command. /// For checking or changing default text editor command.
@ -251,6 +264,14 @@ fn program() -> Result<(), error::Error> {
task.data.time_entries.push(entry); task.data.time_entries.push(entry);
task.save()?; task.save()?;
}, },
Stats(command) => {
use StatsCommand::*;
match command {
Tracked { days } => {
stats::time_per_tag(days, vault_folder)?;
}
}
},
Discard { id_or_name } => { Discard { id_or_name } => {
let id = state.name_or_id_to_id(&id_or_name)?; let id = state.name_or_id_to_id(&id_or_name)?;
let mut task = tasks::Task::load(id, vault_folder, false)?; let mut task = tasks::Task::load(id, vault_folder, false)?;

59
src/stats.rs Normal file
View File

@ -0,0 +1,59 @@
use crate::tasks;
use crate::error;
use std::path;
use std::collections::HashMap;
pub fn time_per_tag(days : u16, vault_folder : &path::Path) -> Result<(), error::Error> {
let tasks = tasks::Task::load_all(vault_folder, true)?;
let mut times = HashMap::<String, tasks::Duration>::new();
for task in &tasks {
if !task.data.discarded {
let mut time = tasks::Duration::zero();
for entry in &task.data.time_entries {
if chrono::Utc::now().naive_local().date() - entry.logged_date < chrono::Duration::days(i64::from(days)) {
time = time + entry.duration;
}
}
let tag_count = task.data.tags.len();
let time_per_tag = time / tag_count;
for tag in &task.data.tags {
match times.get_mut(tag) {
Some(time) => {
*time = *time + time_per_tag;
},
None => {
times.insert(tag.clone(), time);
}
}
}
}
}
let mut table = comfy_table::Table::new();
table
.load_preset(comfy_table::presets::UTF8_FULL)
.apply_modifier(comfy_table::modifiers::UTF8_ROUND_CORNERS)
.set_content_arrangement(comfy_table::ContentArrangement::Dynamic);
table.set_header(vec!["Tag", "Time"]);
for (tag, duration) in &times {
table.add_row(
vec![
tag.clone(),
duration.to_string(),
]
);
}
println!("{}", table);
Ok(())
}

View File

@ -5,6 +5,7 @@ use crate::colour;
use std::io; use std::io;
use std::fs; use std::fs;
use std::fmt; use std::fmt;
use std::ops;
use std::mem; use std::mem;
use std::path; use std::path;
use std::io::{Write, Seek}; use std::io::{Write, Seek};
@ -53,41 +54,17 @@ impl Priority {
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TimeEntry { pub struct TimeEntry {
logged_date : chrono::NaiveDate, pub logged_date : chrono::NaiveDate,
pub duration : Duration,
}
// Needs to preserve representation invariant of minutes < 60
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Duration {
hours : u16, hours : u16,
minutes : u16, minutes : u16,
} }
impl TimeEntry {
/// Adds up a collection of time entries.
fn total(entries : &Vec<TimeEntry>) -> (u16, u16) {
let (hours, minutes) =
entries
.into_iter()
.fold((0, 0), |a, e| (a.0 + e.hours, a.1 + e.minutes));
let (hours, minutes) = {
(hours + minutes / 60, minutes % 60)
};
(hours, minutes)
}
}
impl TimeEntry {
pub fn new(hours : u16, minutes : u16) -> Self {
let (hours, minutes) = {
(hours + minutes / 60, minutes % 60)
};
Self {
logged_date : chrono::Utc::now().naive_local().date(),
hours,
minutes,
}
}
}
#[derive(Debug, serde::Serialize, serde::Deserialize)] #[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct InternalTask { pub struct InternalTask {
@ -291,7 +268,7 @@ impl Task {
println!("Time Entries:"); println!("Time Entries:");
for entry in &entries { for entry in &entries {
println!(" {}:{:0>2} [{}]", entry.hours, entry.minutes, entry.logged_date); println!(" {} [{}]", entry.duration, entry.logged_date);
} }
} }
@ -332,7 +309,7 @@ pub fn list(vault_folder : &path::Path) -> Result<(), error::Error> {
for task in tasks { for task in tasks {
if !task.data.discarded && !task.data.complete { if !task.data.discarded && !task.data.complete {
let (hours, minutes) = TimeEntry::total(&task.data.time_entries); let duration = TimeEntry::total(&task.data.time_entries);
table.add_row( table.add_row(
vec![ vec![
@ -340,7 +317,7 @@ pub fn list(vault_folder : &path::Path) -> Result<(), error::Error> {
task.data.name, task.data.name,
format_hash_set(&task.data.tags)?, format_hash_set(&task.data.tags)?,
task.data.priority.to_string(), task.data.priority.to_string(),
if (hours, minutes) == (0, 0) { String::new() } else { format!("{}:{:0>2}", hours, minutes) }, if duration == Duration::zero() { String::new() } else { duration.to_string() },
] ]
); );
} }
@ -364,4 +341,74 @@ pub fn clean(vault_folder : &path::Path) -> Result<(), error::Error> {
Ok(()) Ok(())
} }
impl ops::Add for Duration {
type Output = Self;
fn add(self, other : Self) -> Self::Output {
Self {
hours : self.hours + other.hours + (self.minutes + other.minutes) / 60,
minutes : (self.minutes + other.minutes) % 60,
}
}
}
impl Duration {
pub fn zero() -> Self {
Self {
minutes : 0,
hours : 0,
}
}
}
impl ops::Div<usize> for Duration {
type Output = Self;
fn div(self, divisor : usize) -> Self::Output {
let total_mins = f64::from(self.hours * 60 + self.minutes);
let divided_mins = total_mins / divisor as f64;
let divided_mins = divided_mins.round() as u16;
Self {
hours : divided_mins / 60,
minutes : divided_mins % 60,
}
}
}
impl fmt::Display for Duration {
fn fmt(&self, f : &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{:0>2}", self.hours, self.minutes)
}
}
impl TimeEntry {
/// Adds up the times from a collection of time entries.
fn total(entries : &Vec<TimeEntry>) -> Duration {
entries
.into_iter()
.map(|e| e.duration)
.fold(Duration::zero(), |a, d| a + d)
}
}
impl TimeEntry {
pub fn new(hours : u16, minutes : u16) -> Self {
let (hours, minutes) = {
(hours + minutes / 60, minutes % 60)
};
Self {
logged_date : chrono::Utc::now().naive_local().date(),
duration : Duration {
hours,
minutes,
}
}
}
}