time entry representation change, and basic statistics
This commit is contained in:
		
							
								
								
									
										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,
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user