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
|
||||
- Make sure dependencies written to file are only those that could be successfully created
|
||||
- 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
|
||||
- Taken as input when creating notes
|
||||
- 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 tasks;
|
||||
mod state;
|
||||
mod stats;
|
||||
mod config;
|
||||
mod colour;
|
||||
|
||||
@ -82,6 +83,9 @@ enum Command {
|
||||
#[clap(short, default_value_t=0)]
|
||||
minutes : u16,
|
||||
},
|
||||
/// For statistics about the state of your vault.
|
||||
#[clap(subcommand)]
|
||||
Stats(StatsCommand),
|
||||
/// For making changes to global configuration.
|
||||
#[clap(subcommand)]
|
||||
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)]
|
||||
enum ConfigCommand {
|
||||
/// For checking or changing default text editor command.
|
||||
@ -251,6 +264,14 @@ fn program() -> Result<(), error::Error> {
|
||||
task.data.time_entries.push(entry);
|
||||
task.save()?;
|
||||
},
|
||||
Stats(command) => {
|
||||
use StatsCommand::*;
|
||||
match command {
|
||||
Tracked { days } => {
|
||||
stats::time_per_tag(days, vault_folder)?;
|
||||
}
|
||||
}
|
||||
},
|
||||
Discard { id_or_name } => {
|
||||
let id = state.name_or_id_to_id(&id_or_name)?;
|
||||
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::fs;
|
||||
use std::fmt;
|
||||
use std::ops;
|
||||
use std::mem;
|
||||
use std::path;
|
||||
use std::io::{Write, Seek};
|
||||
@ -53,41 +54,17 @@ impl Priority {
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
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,
|
||||
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)]
|
||||
pub struct InternalTask {
|
||||
@ -291,7 +268,7 @@ impl Task {
|
||||
|
||||
println!("Time 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 {
|
||||
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(
|
||||
vec![
|
||||
@ -340,7 +317,7 @@ pub fn list(vault_folder : &path::Path) -> Result<(), error::Error> {
|
||||
task.data.name,
|
||||
format_hash_set(&task.data.tags)?,
|
||||
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(())
|
||||
}
|
||||
|
||||
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