Compare commits
No commits in common. "main" and "v1.1.0" have entirely different histories.
7 changed files with 240 additions and 237 deletions
70
Cargo.lock
generated
70
Cargo.lock
generated
|
|
@ -2,15 +2,6 @@
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 3
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aho-corasick"
|
|
||||||
version = "1.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "android-tzdata"
|
name = "android-tzdata"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
|
|
@ -81,6 +72,17 @@ version = "1.0.72"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854"
|
checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atty"
|
||||||
|
version = "0.2.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
||||||
|
dependencies = [
|
||||||
|
"hermit-abi 0.1.19",
|
||||||
|
"libc",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
|
@ -206,6 +208,15 @@ version = "0.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hermit-abi"
|
||||||
|
version = "0.1.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hermit-abi"
|
name = "hermit-abi"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
|
|
@ -241,7 +252,7 @@ version = "0.4.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
|
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hermit-abi",
|
"hermit-abi 0.3.2",
|
||||||
"rustix",
|
"rustix",
|
||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
@ -273,12 +284,6 @@ version = "0.4.19"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
|
checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "memchr"
|
|
||||||
version = "2.7.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
|
|
@ -312,35 +317,6 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "regex"
|
|
||||||
version = "1.11.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
|
||||||
dependencies = [
|
|
||||||
"aho-corasick",
|
|
||||||
"memchr",
|
|
||||||
"regex-automata",
|
|
||||||
"regex-syntax",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "regex-automata"
|
|
||||||
version = "0.4.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
|
|
||||||
dependencies = [
|
|
||||||
"aho-corasick",
|
|
||||||
"memchr",
|
|
||||||
"regex-syntax",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "regex-syntax"
|
|
||||||
version = "0.8.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "0.38.3"
|
version = "0.38.3"
|
||||||
|
|
@ -384,12 +360,12 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-track"
|
name = "time-track"
|
||||||
version = "2.1.4"
|
version = "1.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"atty",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"regex",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
[package]
|
[package]
|
||||||
name = "time-track"
|
name = "time-track"
|
||||||
version = "2.1.4"
|
version = "1.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.72"
|
anyhow = "1.0.72"
|
||||||
|
atty = "0.2.14"
|
||||||
chrono = "0.4.26"
|
chrono = "0.4.26"
|
||||||
clap = { version = "4.3.11", features = ["derive"] }
|
clap = { version = "4.3.11", features = ["derive"] }
|
||||||
regex = "1.11.1"
|
|
||||||
|
|
|
||||||
22
README.md
22
README.md
|
|
@ -1,22 +0,0 @@
|
||||||
# time-track
|
|
||||||
|
|
||||||
A simple CLI tool for tracking how much time you have left to work. I find myself stressing about whether I'm hitting 8 real hours, so this little tool helps me avoid wasting
|
|
||||||
time calculating when my work day will end.
|
|
||||||
|
|
||||||
Simply enter times, one per line and send an EOF character when you're done. The first lines opens a span of work and the next line closes it so that you can build up working
|
|
||||||
time be clocking in and out. Finally, you can send additional arguments to the program to configure how long you intend to work (the default is 8 hours).
|
|
||||||
|
|
||||||
```
|
|
||||||
❯ time-track
|
|
||||||
Working for 8 hours
|
|
||||||
Input times one per line. Send an EOF character to finish inputting...
|
|
||||||
8:30
|
|
||||||
9:30
|
|
||||||
11:15
|
|
||||||
12:30
|
|
||||||
13:16
|
|
||||||
18:20
|
|
||||||
19:30
|
|
||||||
20:11 # Send an EOF
|
|
||||||
Exactly done
|
|
||||||
```
|
|
||||||
19
src/args.rs
19
src/args.rs
|
|
@ -1,19 +0,0 @@
|
||||||
use clap::Parser;
|
|
||||||
|
|
||||||
/// A simple program to track time spans and calculate remaining hours to work
|
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
#[command(author, version, about, long_about = None)]
|
|
||||||
pub struct Args {
|
|
||||||
/// How many hours you intend to work (sums with `minutes`)
|
|
||||||
/// Usually defaults to 8 hours, but if discount is set then it defaults to 0
|
|
||||||
#[arg(long)]
|
|
||||||
pub hours: Option<i64>,
|
|
||||||
|
|
||||||
/// How many minutes you intend to work (sums with `hours`)
|
|
||||||
#[arg(long, default_value_t = 0)]
|
|
||||||
pub minutes: i64,
|
|
||||||
|
|
||||||
/// If true, the the hours and minutes fields are treated as subtracting from 8 hours
|
|
||||||
#[arg(long, default_value_t = false)]
|
|
||||||
pub discount: bool,
|
|
||||||
}
|
|
||||||
221
src/main.rs
221
src/main.rs
|
|
@ -1,65 +1,186 @@
|
||||||
use std::io::{self, BufRead};
|
use std::io::{stdin, BufRead};
|
||||||
use chrono::{DateTime, Local};
|
|
||||||
|
|
||||||
|
use chrono::{NaiveTime, Duration, Local, NaiveDateTime};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use atty::Stream;
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
mod args;
|
|
||||||
mod time;
|
|
||||||
|
|
||||||
use args::Args;
|
mod modes;
|
||||||
|
use modes::Modes;
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
/// A simple program to track time spans
|
||||||
let args = Args::parse();
|
#[derive(Parser, Debug)]
|
||||||
let stdin = io::stdin();
|
#[command(author, version, about, long_about = None)]
|
||||||
let hours = args.hours.unwrap_or(
|
struct Args {
|
||||||
if args.discount {
|
/// Specify how the program should behave
|
||||||
0
|
#[clap(value_enum, default_value_t = Modes::default())]
|
||||||
} else {
|
mode: Modes,
|
||||||
8
|
|
||||||
|
#[arg(long, default_value_t = 8)]
|
||||||
|
hours: i64,
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
let target_minutes = if args.discount {
|
fn epoch() -> NaiveTime {
|
||||||
(8 * 60) - ((hours * 60) + args.minutes)
|
NaiveTime::from_hms_opt(0, 0, 0).expect("0, 0, 0 to be valid arguments to from_hms_opt")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given an arbitrary string, attempt to parse it as a naive time in the format HH:mm
|
||||||
|
fn parse_time(time_str: &str) -> Result<NaiveDateTime> {
|
||||||
|
let time = NaiveTime::parse_from_str(time_str, "%H:%M")?;
|
||||||
|
let naive_date = NaiveDateTime::from_timestamp_millis(
|
||||||
|
time.signed_duration_since(epoch()).num_milliseconds(),
|
||||||
|
).ok_or(anyhow!("Should be able to make a datetime"))?;
|
||||||
|
Ok(naive_date)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn adjust_last(first: &NaiveTime, second: NaiveTime) -> NaiveTime {
|
||||||
|
// The following logic attempts to handle wrapping from AM -> PM and also from PM ->
|
||||||
|
// AM but cannot handle multiple days in a single span
|
||||||
|
if (second < *first) && first.signed_duration_since(epoch()).num_hours() < 12 {
|
||||||
|
// Assuming wrapped to PM
|
||||||
|
// first = 8:00
|
||||||
|
// second: 1:00
|
||||||
|
second + Duration::hours(12)
|
||||||
|
} else if second < *first {
|
||||||
|
// Wrapped to a new day because first was after noon.
|
||||||
|
// first = 14:40
|
||||||
|
// second = 1:30
|
||||||
|
second + Duration::hours(24)
|
||||||
} else {
|
} else {
|
||||||
hours * 60 + args.minutes
|
second
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The default way of calculating time. Time values are given one per line and subsequent pairs of
|
||||||
|
/// lines are considered time spans. If an odd number of spans is given, then the final time value
|
||||||
|
/// is ignored.
|
||||||
|
fn all_at_once(is_terminal: bool) -> Result<Vec<Duration>> {
|
||||||
|
if is_terminal {
|
||||||
|
println!("Enter simple times one per line. Send an EOF character to sum all time spans");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut durations = vec![];
|
||||||
|
let mut seen: Option<String> = None;
|
||||||
|
for line in stdin().lock().lines() {
|
||||||
|
let cleaned = line?.trim().to_string();
|
||||||
|
if let Some(previous) = &seen {
|
||||||
|
let first = parse_time(previous)?;
|
||||||
|
let second = adjust_last(&first.time(), parse_time(&cleaned)?.time());
|
||||||
|
|
||||||
|
durations.push(second - first.time());
|
||||||
|
seen = None;
|
||||||
|
} else {
|
||||||
|
seen = Some(cleaned.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(unpaired) = seen {
|
||||||
|
let now = Local::now().naive_local().time();
|
||||||
|
let last = adjust_last(&now, parse_time(&unpaired)?.time());
|
||||||
|
println!("Ended with an open span from {unpaired}... assuming now: {}", now.format("%H:%M"));
|
||||||
|
durations.push(now - last);
|
||||||
|
}
|
||||||
|
return Ok(durations);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A "live" time tracker. Each time the user presses enter a new span is started or an existing
|
||||||
|
/// span is closed. The user may end the tracking at any time by sending an EOF command.
|
||||||
|
fn live_spans() -> Result<Vec<Duration>> {
|
||||||
|
println!("Tracking Spans Live. Press ENTER to start a span\n");
|
||||||
|
let mut durations = vec![];
|
||||||
|
let mut seen: Option<NaiveTime> = None;
|
||||||
|
for _line in stdin().lock().lines() {
|
||||||
|
let mut now = Local::now().naive_local().time();
|
||||||
|
if let Some(previous) = &seen {
|
||||||
|
now = adjust_last(previous, now);
|
||||||
|
let prev_time = previous.format("%H:%M");
|
||||||
|
let now_time = now.format("%H:%M");
|
||||||
|
println!("Closing span from {prev_time} - {now_time}");
|
||||||
|
println!("Status: Away");
|
||||||
|
durations.push(now - *previous);
|
||||||
|
seen = None;
|
||||||
|
} else {
|
||||||
|
let now_time = now.format("%H:%M");
|
||||||
|
println!("Starting span at {now_time}");
|
||||||
|
println!("Status: Working");
|
||||||
|
seen = Some(now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(unpaired) = seen {
|
||||||
|
println!("Closing unpaired span now");
|
||||||
|
let mut now = Local::now().naive_local().time();
|
||||||
|
now = adjust_last(&unpaired, now);
|
||||||
|
durations.push(now - unpaired);
|
||||||
|
}
|
||||||
|
return Ok(durations);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_hrs_minutes(total_minutes: i64) -> (i64, i64) {
|
||||||
|
let minutes = total_minutes % 60;
|
||||||
|
let hours = total_minutes / 60;
|
||||||
|
(hours, minutes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_time(hours: i64, minutes: i64) -> String {
|
||||||
|
let pluralized_hours = match hours {
|
||||||
|
1 => "1 hour".to_string(),
|
||||||
|
_ => format!("{hours} hours"),
|
||||||
};
|
};
|
||||||
let (hrs, mins) = time::to_hrs_minutes(target_minutes);
|
let pluralized_minutes = match minutes {
|
||||||
println!("Working for {}", time::show_time(hrs, mins));
|
1 => "1 minute".to_string(),
|
||||||
println!("Input times one per line. Send an EOF character to finish inputting...");
|
_ => format!("{minutes} minutes"),
|
||||||
let mut lines: Vec<String> = vec![];
|
};
|
||||||
for line in stdin.lock().lines() {
|
|
||||||
lines.push(line.expect("Issues when reading from stdin"));
|
if hours == 0 {
|
||||||
|
return pluralized_minutes.to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is immeidately going to be turned back into a DateTime, which the doc says is fine
|
if minutes == 0 {
|
||||||
#[allow(deprecated)]
|
return pluralized_hours.to_string();
|
||||||
let midnight = Local::now().date().and_hms_opt(0, 0, 0).ok_or(anyhow!("Expected midnight to exist"))?;
|
}
|
||||||
let times = time::from_stream(&midnight, lines.iter())?;
|
|
||||||
|
|
||||||
let mut total_minutes: i64 = 0;
|
return format!("{pluralized_hours} and {pluralized_minutes}");
|
||||||
let mut first: Option<DateTime<Local>> = None;
|
}
|
||||||
let mut last: Option<DateTime<Local>> = None;
|
|
||||||
for time in times {
|
fn get_charaterized_time_remaining(total_minutes: i64, target_minutes: i64) -> String {
|
||||||
match first {
|
if total_minutes == target_minutes {
|
||||||
None => {
|
return "Exactly done".to_string();
|
||||||
first = Some(time);
|
}
|
||||||
|
|
||||||
|
if total_minutes > target_minutes {
|
||||||
|
let diff = total_minutes - target_minutes;
|
||||||
|
let (hours, minutes) = to_hrs_minutes(diff);
|
||||||
|
return format!("You have overworked {}", show_time(hours, minutes))
|
||||||
|
} else {
|
||||||
|
let diff = target_minutes - total_minutes;
|
||||||
|
let (hours, minutes) = to_hrs_minutes(diff);
|
||||||
|
let end_at = (Local::now() + Duration::minutes(diff)).time();
|
||||||
|
let end_str = end_at.format("%-I:%M %p");
|
||||||
|
return format!("You have {} remaining (end at {} starting now)", show_time(hours, minutes), end_str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args = Args::parse();
|
||||||
|
let is_terminal = atty::is(Stream::Stdin);
|
||||||
|
|
||||||
|
if !is_terminal && !args.mode.supports_piped_input() {
|
||||||
|
panic!("Cannot run {0} mode on piped input", args.mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
let maybe_durations = match args.mode {
|
||||||
|
Modes::TimeTable => all_at_once(is_terminal),
|
||||||
|
Modes::Live => live_spans(),
|
||||||
|
};
|
||||||
|
|
||||||
|
match maybe_durations {
|
||||||
|
Ok(durations) => {
|
||||||
|
let total_minutes: i64 = durations.iter().map(|d| { d.num_minutes() }).sum();
|
||||||
|
let (hours, minutes) = to_hrs_minutes(total_minutes);
|
||||||
|
|
||||||
|
println!("-----------------");
|
||||||
|
println!("You have been working for {}", show_time(hours, minutes));
|
||||||
|
println!("{}", get_charaterized_time_remaining(total_minutes, args.hours * 60))
|
||||||
},
|
},
|
||||||
Some(prev) => {
|
Err(err) => eprintln!("{}\nExiting...", err),
|
||||||
total_minutes += (time - prev).num_minutes();
|
|
||||||
first = None;
|
|
||||||
last = Some(time);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(remaining) = first {
|
|
||||||
let now = Local::now();
|
|
||||||
let now_str = now.format("%-I:%M %p");
|
|
||||||
println!("Ended with unclosed span... assuming ending now: {}", now_str);
|
|
||||||
total_minutes += (now - remaining).num_minutes();
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("{}", time::get_charaterized_time_remaining(total_minutes, target_minutes, last.unwrap_or_else(|| Local::now())));
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
|
||||||
36
src/modes.rs
Normal file
36
src/modes.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
use clap::ValueEnum;
|
||||||
|
|
||||||
|
#[derive(ValueEnum, Debug, Copy, Clone, Eq, PartialEq)]
|
||||||
|
pub enum Modes {
|
||||||
|
/// Perform calculations by treating stdin as a stream of time pairs separated by newlines
|
||||||
|
TimeTable,
|
||||||
|
/// Perform live tracking of the user's time until an EOF character is received.
|
||||||
|
Live,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Modes {
|
||||||
|
/// Determine whether or not the variant supports piped input vs being used as a CLI tool
|
||||||
|
/// directly by a human
|
||||||
|
pub fn supports_piped_input(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Modes::TimeTable => true,
|
||||||
|
Modes::Live => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl Default for Modes {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::TimeTable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Modes {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.to_possible_value()
|
||||||
|
.expect("no values are skipped")
|
||||||
|
.get_name()
|
||||||
|
.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
89
src/time.rs
89
src/time.rs
|
|
@ -1,89 +0,0 @@
|
||||||
use chrono::{Duration, Local, DateTime};
|
|
||||||
use anyhow::{Result, anyhow};
|
|
||||||
use std::iter::Iterator;
|
|
||||||
use regex::Regex;
|
|
||||||
|
|
||||||
fn parse_datetime(reference_date: &DateTime<Local>, s: &str) -> Result<DateTime<Local>> {
|
|
||||||
let re = Regex::new(r"^\s*([12]?\d):([012345]\d)\s*$")?;
|
|
||||||
let (_, [hrs, mins]) = re.captures(s).ok_or(anyhow!(format!("Failed to parse \"{s}\" as a time")))?.extract();
|
|
||||||
let mins: u32 = mins.parse()?;
|
|
||||||
let mut hrs: u32 = hrs.parse()?;
|
|
||||||
let mut date = *reference_date;
|
|
||||||
|
|
||||||
if hrs > 23 {
|
|
||||||
let num_days = hrs / 24;
|
|
||||||
date = *reference_date + Duration::days(num_days.into());
|
|
||||||
hrs = hrs % 24;
|
|
||||||
}
|
|
||||||
#[allow(deprecated)]
|
|
||||||
Ok(date.date().and_hms_opt(hrs, mins, 0).ok_or(anyhow!(format!("{}:{:0>2} not a real time", hrs, mins)))?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_stream<'a>(reference_date: &DateTime<Local>, stream: impl Iterator<Item = &'a String>) -> Result<Vec<DateTime<Local>>> {
|
|
||||||
let mut durations: Vec<DateTime<Local>> = vec![];
|
|
||||||
for val in stream {
|
|
||||||
durations.push(parse_datetime(reference_date, val)?);
|
|
||||||
}
|
|
||||||
return Ok(durations);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn show_time(hours: i64, minutes: i64) -> String {
|
|
||||||
let pluralized_hours = match hours {
|
|
||||||
1 => "1 hour".to_string(),
|
|
||||||
_ => format!("{hours} hours"),
|
|
||||||
};
|
|
||||||
let pluralized_minutes = match minutes {
|
|
||||||
1 => "1 minute".to_string(),
|
|
||||||
_ => format!("{minutes} minutes"),
|
|
||||||
};
|
|
||||||
|
|
||||||
if hours == 0 {
|
|
||||||
return pluralized_minutes.to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
if minutes == 0 {
|
|
||||||
return pluralized_hours.to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
return format!("{pluralized_hours} and {pluralized_minutes}");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_hrs_minutes(total_minutes: i64) -> (i64, i64) {
|
|
||||||
let minutes = total_minutes % 60;
|
|
||||||
let hours = total_minutes / 60;
|
|
||||||
(hours, minutes)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_charaterized_time_remaining(
|
|
||||||
total_minutes: i64,
|
|
||||||
target_minutes: i64,
|
|
||||||
ended_at: DateTime<Local>,
|
|
||||||
) -> String {
|
|
||||||
if total_minutes == target_minutes {
|
|
||||||
return "Exactly done".to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
if total_minutes > target_minutes {
|
|
||||||
let diff = total_minutes - target_minutes;
|
|
||||||
let (hours, minutes) = to_hrs_minutes(diff);
|
|
||||||
return format!("You have overworked {}", show_time(hours, minutes))
|
|
||||||
} else {
|
|
||||||
let diff = target_minutes - total_minutes;
|
|
||||||
let (hours, minutes) = to_hrs_minutes(diff);
|
|
||||||
let now = Local::now();
|
|
||||||
return if ended_at > now {
|
|
||||||
let end_at = (ended_at + Duration::minutes(diff)).time();
|
|
||||||
let end_str = end_at.format("%-I:%M %p");
|
|
||||||
format!(
|
|
||||||
"You have {} remaining (end at {} starting from {})",
|
|
||||||
show_time(hours, minutes),
|
|
||||||
end_str,
|
|
||||||
ended_at.format("%-I:%M %p"),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
let end_at = (now + Duration::minutes(diff)).time();
|
|
||||||
let end_str = end_at.format("%-I:%M %p");
|
|
||||||
format!("You have {} remaining (end at {} starting now)", show_time(hours, minutes), end_str)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue