use chrono::{Duration as ChronoDuration, Local, NaiveTime, Timelike};
use crate::conditions::{self, WaitCondition};
use clap::{Parser, Subcommand};
use std::time::Duration;
fn parse_time_until(s: &str) -> Result<Duration, String> {
let now = Local::now();
let parsed_time = NaiveTime::parse_from_str(s, "%H:%M")
.or_else(|_| NaiveTime::parse_from_str(s, "%H:%M:%S"))
.map_err(|_| format!("Invalid time format. Expected HH:MM or HH:MM:SS: {s}"))?;
let mut target_datetime = now
.with_hour(parsed_time.hour())
.and_then(|dt| dt.with_minute(parsed_time.minute()))
.and_then(|dt| dt.with_second(parsed_time.second()))
.and_then(|dt| dt.with_nanosecond(parsed_time.nanosecond()))
.unwrap();
if target_datetime < now {
target_datetime += ChronoDuration::days(1);
}
let duration_until = target_datetime.signed_duration_since(now);
duration_until.to_std().map_err(|e| e.to_string())
}
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
#[arg(short, long, value_parser = humantime::parse_duration, global = true)]
pub jitter: Option<Duration>,
#[arg(short, long, value_name = "UPDATE_PERIOD", value_parser = humantime::parse_duration, num_args = 0..=1, default_missing_value = "1ns", global = true)]
pub verbose: Option<Duration>,
#[arg(short, long, global = true)]
pub probability: Option<f64>,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
#[command(alias = "d")]
Duration {
#[arg(value_parser = humantime::parse_duration)]
time: Duration,
},
#[command(alias = "n")]
Normal {
#[arg(value_parser = humantime::parse_duration)]
mean: Duration,
std_dev: f64,
},
#[command(alias = "e")]
Exponential {
lambda: f64,
},
#[command(alias = "ln")]
LogNormal {
#[arg(value_parser = humantime::parse_duration)]
mean: Duration,
std_dev: f64,
},
#[command(alias = "par")]
Pareto {
scale: f64,
shape: f64,
},
#[command(alias = "u")]
Uniform {
#[arg(value_parser = humantime::parse_duration)]
min: Duration,
#[arg(value_parser = humantime::parse_duration)]
max: Duration,
},
#[command(alias = "t")]
Triangular {
min: f64,
max: f64,
mode: f64,
},
#[command(alias = "g")]
Gamma {
shape: f64,
scale: f64,
},
#[command(aliases = &["a", "ali"])]
Align {
#[arg(value_parser = humantime::parse_duration)]
interval: Duration,
},
#[command()]
At {
#[arg(value_parser = parse_time_until)]
time: Duration,
},
}
impl Commands {
pub fn into_wait_condition(
self,
jitter: Option<Duration>,
verbose: Option<Duration>,
probability: Option<f64>,
) -> Box<dyn WaitCondition> {
match self {
Commands::Duration { time } => {
if let Some(probability) = probability {
Box::new(conditions::ProbabilisticWait {
duration: time,
probability,
verbose: verbose,
})
} else {
Box::new(conditions::DurationWait {
duration: time,
jitter: jitter,
verbose: verbose,
})
}
}
Commands::Normal { mean, std_dev } => Box::new(conditions::NormalWait {
mean,
std_dev,
verbose: verbose,
jitter: jitter,
}),
Commands::Exponential { lambda } => Box::new(conditions::ExponentialWait {
lambda,
verbose: verbose,
jitter: jitter,
}),
Commands::LogNormal { mean, std_dev } => Box::new(conditions::LogNormalWait {
mean,
std_dev,
verbose: verbose,
jitter: jitter,
}),
Commands::Pareto { scale, shape } => Box::new(conditions::ParetoWait {
scale,
shape,
verbose: verbose,
jitter: jitter,
}),
Commands::Triangular { min, max, mode } => Box::new(conditions::TriangularWait {
min,
max,
mode,
verbose: verbose,
jitter: jitter,
}),
Commands::Align { interval } => Box::new(conditions::TimeAlignWait {
align_interval: interval,
verbose: verbose,
}),
Commands::Uniform { min, max } => Box::new(conditions::UniformWait {
min,
max,
verbose: verbose,
jitter: jitter,
}),
Commands::At { time } => Box::new(conditions::UntilTimeWait {
sleep_duration: time,
verbose: verbose,
}),
Commands::Gamma { shape, scale } => Box::new(conditions::GammaWait {
shape,
scale,
verbose: verbose,
jitter: jitter,
}),
}
}
}
pub enum WaitType {
Duration(Duration),
Normal { mean: Duration, std_dev: f64 },
Exponential { lambda: f64 },
LogNormal { mean: Duration, std_dev: f64 },
Pareto { scale: f64, shape: f64 },
Uniform { min: Duration, max: Duration },
Triangular { min: f64, max: f64, mode: f64 },
Gamma { shape: f64, scale: f64 },
Align(Duration),
Until(Duration),
}
impl Cli {
pub fn get_wait_type(&self) -> WaitType {
match &self.command {
Commands::Duration { time } => WaitType::Duration(*time),
Commands::Normal { mean, std_dev } => WaitType::Normal {
mean: *mean,
std_dev: *std_dev,
},
Commands::Exponential { lambda } => WaitType::Exponential { lambda: *lambda },
Commands::LogNormal { mean, std_dev } => WaitType::LogNormal {
mean: *mean,
std_dev: *std_dev,
},
Commands::Pareto { scale, shape } => WaitType::Pareto {
scale: *scale,
shape: *shape,
},
Commands::Uniform { min, max } => WaitType::Uniform {
min: *min,
max: *max,
},
Commands::Triangular { min, max, mode } => WaitType::Triangular {
min: *min,
max: *max,
mode: *mode,
},
Commands::Gamma { shape, scale } => WaitType::Gamma {
shape: *shape,
scale: *scale,
},
Commands::Align { interval } => WaitType::Align(*interval),
Commands::At { time } => WaitType::Until(*time),
}
}
pub fn is_adaptive_verbose(&self) -> bool {
self.verbose == Some(Duration::from_nanos(1))
}
pub fn verbose_period(&self) -> Option<Duration> {
if self.is_adaptive_verbose() {
None
} else {
self.verbose
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Local;
#[test]
fn test_parse_time_until_in_future() {
let now = Local::now();
let future_time = now + ChronoDuration::minutes(1);
let time_str = future_time.format("%H:%M:%S").to_string();
let duration = parse_time_until(&time_str).unwrap();
assert!(duration > Duration::from_secs(50) && duration <= Duration::from_secs(60));
}
#[test]
fn test_parse_time_until_in_past_rolls_to_next_day() {
let now = Local::now();
let past_time = now - ChronoDuration::minutes(1);
let time_str = past_time.format("%H:%M:%S").to_string();
let duration = parse_time_until(&time_str).unwrap();
assert!(duration > Duration::from_secs(23 * 3600));
}
#[test]
fn test_parse_time_invalid_format() {
assert!(parse_time_until("invalid-time").is_err());
assert!(parse_time_until("25:00").is_err());
assert!(parse_time_until("10:65").is_err());
}
}