Skip to content

Commit

Permalink
Better time format handling, fixed parse error
Browse files Browse the repository at this point in the history
  • Loading branch information
nielsing committed Jun 12, 2020
1 parent 7dfb4a3 commit eba171e
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 62 deletions.
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 36 additions & 9 deletions src/arguments.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
use std::str::FromStr;

use structopt::StructOpt;

use crate::error::{AppError, ErrorKind};

#[derive(StructOpt, Debug)]
#[structopt(name = "Work - Terminal Time Tracker!")]
pub struct Args {
Expand Down Expand Up @@ -36,15 +40,9 @@ pub enum SubCommand {
/// Set output format to JSON
#[structopt(short, long)]
json: bool,
/// Set time format to total number of minutes
#[structopt(long)]
minutes: bool,
/// Set time format to approximate number of minutes
#[structopt(short, long)]
minutes_approx: bool,
/// Set time format to approximate number of hours
#[structopt(short, long)]
hours_approx: bool,
/// Specify the time format of the output
#[structopt(short, long, possible_values = &["m", "minutes", "ma", "minutes-approx", "h", "hours", "hr", "human-readable"], default_value = "human-readable")]
time_format: TimeFormat,
},
/// Appends a new event to the log that started at a given time
Since {
Expand Down Expand Up @@ -82,3 +80,32 @@ pub enum SubCommand {
description: Option<String>,
},
}

#[derive(StructOpt, Debug)]
pub enum TimeFormat {
Minutes,
MinutesApprox,
HoursApprox,
HumanReadable,
}

impl FromStr for TimeFormat {
type Err = AppError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"m" => Ok(TimeFormat::Minutes),
"minutes" => Ok(TimeFormat::Minutes),
"h" => Ok(TimeFormat::HoursApprox),
"hours" => Ok(TimeFormat::HoursApprox),
"ma" => Ok(TimeFormat::MinutesApprox),
"minutes-approx" => Ok(TimeFormat::MinutesApprox),
"hr" => Ok(TimeFormat::HumanReadable),
"human-readable" => Ok(TimeFormat::HumanReadable),
_ => Err(AppError::new(ErrorKind::User(
"Valid values are [m, minutes, ma, minutes-approx, h, hours, hr, human-readable]"
.to_string(),
))),
}
}
}
14 changes: 2 additions & 12 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,8 @@ fn run_app(args: Args) -> Result<i32, AppError> {
interval,
csv,
json,
minutes,
minutes_approx,
hours_approx,
} => of(
&mut log,
&interval,
csv,
json,
minutes,
minutes_approx,
hours_approx,
),
time_format,
} => of(&mut log, &interval, csv, json, time_format),
SubCommand::Since {
time,
project,
Expand Down
37 changes: 26 additions & 11 deletions src/project_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ use std::collections::HashMap;

use serde_json;

use crate::arguments::TimeFormat;
use crate::log_file::Event;
use crate::time::format_time;

/// These constants are used to add clarity to the `add_events` function for the ProjectMap.
const START: usize = 0;
const STOP: usize = 1;

// ProjectMap maps projects to descriptions which in turn is mapped to total spent time.
//
// A project is mapped to a map which maps descriptions to the total time spent on a given project
// with a given description.
/// ProjectMap maps projects to descriptions which in turn is mapped to total spent time.
///
/// A project is mapped to a map which maps descriptions to the total time spent on a given project
/// with a given description.
pub type ProjectMap = HashMap<String, HashMap<String, i64>>;

pub trait ProjectMapMethods {
Expand All @@ -21,8 +23,8 @@ pub trait ProjectMapMethods {
fn add_clean_event(&mut self, time: &i64, event: &Event);

// Functions for output.
fn as_csv(&self) -> String;
fn as_json(&self) -> String;
fn as_csv(&self, time_format: &TimeFormat) -> String;
fn as_json(&self, time_format: &TimeFormat) -> String;
}

impl ProjectMapMethods for ProjectMap {
Expand All @@ -47,7 +49,7 @@ impl ProjectMapMethods for ProjectMap {
events.chunks(2).for_each(|pair| {
let time = pair[STOP].0 - pair[START].0;
self.add_event(&time, &pair[START].1);
})
});
}

/// Assumes the given project does not exist within the ProjectMap and blindly inserts it.
Expand All @@ -60,18 +62,31 @@ impl ProjectMapMethods for ProjectMap {
}

/// Returns a CSV format of the ProjectMap as a string.
fn as_csv(&self) -> String {
fn as_csv(&self, time_format: &TimeFormat) -> String {
let mut csv = String::from("Project,Description,Time Spent\n");
self.iter().for_each(|(project, descs)| {
descs.iter().for_each(|(desc, time)| {
csv.push_str(&format!("{},{},{}\n", project, desc, time));
csv.push_str(&format!(
"{},{},{}\n",
project,
desc,
format_time(time_format, *time)
));
});
});
csv
}

/// Returns a JSON format of the ProjectMap as a string.
fn as_json(&self) -> String {
serde_json::to_string_pretty(&self).unwrap()
fn as_json(&self, time_format: &TimeFormat) -> String {
let mut tmp_map = HashMap::new();
for (project, descs) in self {
let mut tmp_descs = HashMap::new();
for (desc, time) in descs {
tmp_descs.insert(desc, format_time(time_format, *time));
}
tmp_map.insert(project, tmp_descs);
}
serde_json::to_string_pretty(&tmp_map).unwrap()
}
}
37 changes: 13 additions & 24 deletions src/subcommands.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::env;
use std::process::Command;

use crate::arguments::TimeFormat;
use crate::error::{AppError, ErrorKind};
use crate::log_file::*;
use crate::project_map::ProjectMapMethods;
Expand Down Expand Up @@ -137,9 +138,7 @@ pub fn of(
interval_input: &str,
csv: bool,
json: bool,
minutes: bool,
minutes_approx: bool,
hours_approx: bool,
time_format: TimeFormat,
) -> Result<i32, AppError> {
let mut interval = time::Interval::try_parse(interval_input, &time::Search::Backward)?;

Expand All @@ -148,38 +147,24 @@ pub fn of(
}

let project_times = log.tally_time(&interval)?;
let format_time = |time| {
if minutes {
format!("{}", time::get_minutes(time))
} else if minutes_approx {
format!("{}", time::approximate_minutes(time))
} else if hours_approx {
format!("{}", time::approximate_hours(time))
} else {
time::get_human_readable_form(time)
}
};
// TODO: Add option for different output formats
// show only projects
// show projects and descriptions
// raw option
// table option
// Create file: output.rs that handles this.
if let Some(map) = project_times {
if csv {
println!("{}", map.as_csv());
println!("{}", map.as_csv(&time_format));
} else if json {
println!("{}", map.as_json());
println!("{}", map.as_json(&time_format));
} else {
map.iter().for_each(|(key, val)| {
println!("{} => {}", key.to_string(), format_time(val.values().sum()))
println!(
"{} => {}",
key.to_string(),
time::format_time(&time_format, val.values().sum())
)
});
}
} else {
println!("No work done!");
return Ok(1);
}

Ok(0)
}

Expand Down Expand Up @@ -297,3 +282,7 @@ pub fn r#while(
}
}
}

pub fn between() {}

pub fn remove() {}
18 changes: 14 additions & 4 deletions src/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use chrono::{Datelike, Duration, Local, NaiveDate, NaiveDateTime, NaiveTime};
use lazy_static::*;
use regex::Regex;

use crate::arguments::TimeFormat;
use crate::error::{AppError, ErrorKind};

/// Full name for an hour unit
Expand Down Expand Up @@ -40,7 +41,7 @@ pub fn now() -> i64 {
/// assert_eq!(approximate_hours(16 * 60), 0.5);
/// assert_eq!(approximate_hours(14 * 60), 0.0);
/// ```
pub fn approximate_hours(duration: i64) -> f64 {
fn approximate_hours(duration: i64) -> f64 {
let duration = Duration::seconds(duration);
let mut answer: f64 = duration.num_hours() as f64;
let remainder_minutes = duration.num_minutes() - (duration.num_hours() * 60);
Expand All @@ -66,7 +67,7 @@ pub fn approximate_hours(duration: i64) -> f64 {
/// assert_eq!(approximate_minutes(31 * 60), 45);
/// assert_eq!(approximate_minutes(14 * 60), 15);
/// ```
pub fn approximate_minutes(duration: i64) -> i64 {
fn approximate_minutes(duration: i64) -> i64 {
let duration = Duration::seconds(duration);
let answer = duration.num_minutes();
let remainder_minutes = APPROX_MINUTES - (answer % APPROX_MINUTES);
Expand Down Expand Up @@ -128,13 +129,22 @@ fn format_human_readable(hours: i64, minutes: i64) -> String {
/// assert_eq!(get_human_readable_form(Duration::seconds(3720).num_seconds()), "1 hour and 2 minutes");
/// assert_eq!(get_human_readable_form(Duration::seconds(7320).num_seconds()), "2 hours and 2 minutes");
/// ```
pub fn get_human_readable_form(duration: i64) -> String {
fn get_human_readable_form(duration: i64) -> String {
let duration = Duration::seconds(duration);
let total_hours = duration.num_hours();
let total_minutes = duration.num_minutes() % MINUTES_IN_HOUR;
format_human_readable(total_hours, total_minutes)
}

pub fn format_time(format: &TimeFormat, time: i64) -> String {
match format {
TimeFormat::Minutes => format!("{}", get_minutes(time)),
TimeFormat::MinutesApprox => format!("{}", approximate_minutes(time)),
TimeFormat::HoursApprox => format!("{}", approximate_hours(time)),
TimeFormat::HumanReadable => get_human_readable_form(time),
}
}

/// Returns the number of minutes in a given duration of seconds
pub fn get_minutes(duration: i64) -> i64 {
Duration::seconds(duration).num_minutes()
Expand Down Expand Up @@ -355,7 +365,7 @@ fn parse_time_input(unit: &str, search_type: &Search) -> Result<NaiveDateTime, A
let now = now_date_time();
let units: Vec<&str> = unit.split(':').collect();
let hours = i64::from_str_radix(units[0], 10).unwrap();
let minutes = i64::from_str_radix(&units[1][..units[1].len()], 10).unwrap();
let minutes = i64::from_str_radix(&units[1][..units[1].len() - 1], 10).unwrap();
let total_minutes = hours * 60 + minutes;

match search_type {
Expand Down

0 comments on commit eba171e

Please sign in to comment.