time entry representation change, and basic statistics
This commit is contained in:
parent
1d04723bd7
commit
43f8bc043b
12
README.md
12
README.md
@ -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
|
||||||
|
21
src/main.rs
21
src/main.rs
@ -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
59
src/stats.rs
Normal 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 × {
|
||||||
|
|
||||||
|
table.add_row(
|
||||||
|
vec![
|
||||||
|
tag.clone(),
|
||||||
|
duration.to_string(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{}", table);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
115
src/tasks.rs
115
src/tasks.rs
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user