pub use std::time::Duration;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DurationParseError {
pub input: String,
}
impl std::fmt::Display for DurationParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "cannot parse {:?} as a duration", self.input)
}
}
impl std::error::Error for DurationParseError {}
#[allow(clippy::module_inception)] pub mod duration {
use super::{Duration, DurationParseError};
pub fn nanos(n: u64) -> Duration {
Duration::from_nanos(n)
}
pub fn micros(n: u64) -> Duration {
Duration::from_micros(n)
}
pub fn millis(n: u64) -> Duration {
Duration::from_millis(n)
}
pub fn seconds(n: u64) -> Duration {
Duration::from_secs(n)
}
pub fn seconds_f64(n: f64) -> Duration {
Duration::from_secs_f64(n)
}
pub fn minutes(n: u64) -> Duration {
Duration::from_secs(n * 60)
}
pub fn hours(n: u64) -> Duration {
Duration::from_secs(n * 3_600)
}
pub fn days(n: u64) -> Duration {
Duration::from_secs(n * 86_400)
}
pub fn weeks(n: u64) -> Duration {
Duration::from_secs(n * 604_800)
}
pub const INFINITY: Duration = Duration::MAX;
pub const ZERO: Duration = Duration::ZERO;
pub fn decode(input: &str) -> Result<Duration, DurationParseError> {
let err = || DurationParseError {
input: input.to_string(),
};
let s = input.trim();
if s.is_empty() {
return Err(err());
}
if let Ok(n) = s.parse::<f64>() {
if n < 0.0 {
return Err(err());
}
return Ok(Duration::from_secs_f64(n / 1_000.0));
}
let split_pos = s.find(|c: char| c.is_alphabetic()).ok_or_else(err)?;
if split_pos == 0 {
return Err(err());
}
let num_str = s[..split_pos].trim();
let unit_str = s[split_pos..].trim().to_lowercase();
let n: f64 = num_str.parse().map_err(|_| err())?;
if n < 0.0 {
return Err(err());
}
let d = match unit_str.as_str() {
"ns" | "nanos" | "nanosecond" | "nanoseconds" => Duration::from_secs_f64(n / 1_000_000_000.0),
"us" | "\u{b5}s" | "micros" | "microsecond" | "microseconds" => {
Duration::from_secs_f64(n / 1_000_000.0)
}
"ms" | "millis" | "millisecond" | "milliseconds" => Duration::from_secs_f64(n / 1_000.0),
"s" | "sec" | "secs" | "second" | "seconds" => Duration::from_secs_f64(n),
"m" | "min" | "mins" | "minute" | "minutes" => Duration::from_secs_f64(n * 60.0),
"h" | "hr" | "hrs" | "hour" | "hours" => Duration::from_secs_f64(n * 3_600.0),
"d" | "day" | "days" => Duration::from_secs_f64(n * 86_400.0),
"w" | "week" | "weeks" => Duration::from_secs_f64(n * 604_800.0),
_ => return Err(err()),
};
Ok(d)
}
pub fn sum(a: Duration, b: Duration) -> Duration {
a + b
}
pub fn subtract(a: Duration, b: Duration) -> Duration {
a.saturating_sub(b)
}
pub fn times(a: Duration, n: u32) -> Duration {
a * n
}
pub fn min(a: Duration, b: Duration) -> Duration {
a.min(b)
}
pub fn max(a: Duration, b: Duration) -> Duration {
a.max(b)
}
pub fn clamp(d: Duration, minimum: Duration, maximum: Duration) -> Duration {
d.max(minimum).min(maximum)
}
pub fn between(d: Duration, minimum: Duration, maximum: Duration) -> bool {
d >= minimum && d <= maximum
}
pub fn to_millis(d: Duration) -> f64 {
d.as_millis() as f64
}
pub fn to_nanos(d: Duration) -> u128 {
d.as_nanos()
}
pub fn to_seconds(d: Duration) -> f64 {
d.as_secs_f64()
}
pub fn to_hours(d: Duration) -> f64 {
d.as_secs_f64() / 3_600.0
}
pub fn format(d: Duration) -> String {
let total_secs = d.as_secs();
let subsec_nanos = d.subsec_nanos();
let weeks = total_secs / 604_800;
let rem = total_secs % 604_800;
let days = rem / 86_400;
let rem = rem % 86_400;
let hours = rem / 3_600;
let rem = rem % 3_600;
let minutes = rem / 60;
let secs = rem % 60;
let millis = subsec_nanos / 1_000_000;
let mut parts = Vec::new();
if weeks > 0 {
parts.push(format!("{weeks}w"));
}
if days > 0 {
parts.push(format!("{days}d"));
}
if hours > 0 {
parts.push(format!("{hours}h"));
}
if minutes > 0 {
parts.push(format!("{minutes}m"));
}
match (secs, millis) {
(0, 0) => {}
(s, 0) => parts.push(format!("{s}s")),
(0, ms) => parts.push(format!("0.{ms:03}s")),
(s, ms) => parts.push(format!("{s}.{ms:03}s")),
}
if parts.is_empty() {
"0s".to_string()
} else {
parts.join(" ")
}
}
pub fn is_zero(d: Duration) -> bool {
d.is_zero()
}
pub fn is_finite(d: Duration) -> bool {
d != Duration::MAX
}
}
#[cfg(test)]
mod tests {
use super::Duration;
use super::duration;
use rstest::rstest;
mod constructors {
use super::*;
#[test]
fn nanos_round_trips_to_nanos() {
assert_eq!(duration::nanos(500).as_nanos(), 500);
}
#[test]
fn micros_round_trips() {
assert_eq!(duration::micros(200).as_micros(), 200);
}
#[test]
fn millis_round_trips() {
assert_eq!(duration::millis(1_000).as_millis(), 1_000);
}
#[test]
fn seconds_round_trips() {
assert_eq!(duration::seconds(60).as_secs(), 60);
}
#[test]
fn minutes_is_60_seconds() {
assert_eq!(duration::minutes(1), duration::seconds(60));
}
#[test]
fn hours_is_3600_seconds() {
assert_eq!(duration::hours(1), duration::seconds(3_600));
}
#[test]
fn days_is_86400_seconds() {
assert_eq!(duration::days(1), duration::seconds(86_400));
}
#[test]
fn weeks_is_7_days() {
assert_eq!(duration::weeks(1), duration::days(7));
}
#[test]
fn zero_constant_is_zero_duration() {
assert!(duration::ZERO.is_zero());
}
#[test]
fn infinity_constant_is_max_duration() {
assert_eq!(duration::INFINITY, Duration::MAX);
}
#[test]
fn seconds_f64_half_second() {
let d = duration::seconds_f64(0.5);
assert_eq!(d.as_millis(), 500);
}
}
mod decode {
use super::*;
#[rstest]
#[case::millis_abbrev("100ms", 100)]
#[case::millis_word("100 millis", 100)]
#[case::millis_full("100 milliseconds", 100)]
fn millis_forms(#[case] input: &str, #[case] expected_ms: u64) {
let d = duration::decode(input).expect("should parse");
assert_eq!(d.as_millis(), expected_ms as u128);
}
#[rstest]
#[case::s_abbrev("2s", 2)]
#[case::sec("2 sec", 2)]
#[case::secs("2 secs", 2)]
#[case::second("2 second", 2)]
#[case::seconds("2 seconds", 2)]
fn seconds_forms(#[case] input: &str, #[case] expected_s: u64) {
let d = duration::decode(input).expect("should parse");
assert_eq!(d.as_secs(), expected_s);
}
#[rstest]
#[case::m("5m", 5)]
#[case::min("5 min", 5)]
#[case::mins("5 mins", 5)]
#[case::minute("5 minute", 5)]
#[case::minutes("5 minutes", 5)]
fn minutes_forms(#[case] input: &str, #[case] expected_m: u64) {
let d = duration::decode(input).expect("should parse");
assert_eq!(d.as_secs(), expected_m * 60);
}
#[rstest]
#[case::h("1h")]
#[case::hr("1 hr")]
#[case::hrs("1 hrs")]
#[case::hour("1 hour")]
#[case::hours("1 hours")]
fn hours_forms(#[case] input: &str) {
let d = duration::decode(input).expect("should parse");
assert_eq!(d.as_secs(), 3_600);
}
#[test]
fn days_form() {
assert_eq!(duration::decode("1d").unwrap().as_secs(), 86_400);
assert_eq!(duration::decode("1 day").unwrap().as_secs(), 86_400);
assert_eq!(duration::decode("1 days").unwrap().as_secs(), 86_400);
}
#[test]
fn weeks_form() {
assert_eq!(duration::decode("1w").unwrap().as_secs(), 604_800);
assert_eq!(duration::decode("1 week").unwrap().as_secs(), 604_800);
assert_eq!(duration::decode("1 weeks").unwrap().as_secs(), 604_800);
}
#[test]
fn nanos_form() {
let d = duration::decode("500ns").unwrap();
assert_eq!(d.as_nanos(), 500);
}
#[test]
fn micros_form() {
let d = duration::decode("10us").unwrap();
assert_eq!(d.as_micros(), 10);
}
#[test]
fn bare_number_is_millis() {
let d = duration::decode("500").unwrap();
assert_eq!(d.as_millis(), 500);
}
#[test]
fn fractional_seconds() {
let d = duration::decode("1.5s").unwrap();
assert_eq!(d.as_millis(), 1_500);
}
#[test]
fn empty_string_returns_error() {
assert!(duration::decode("").is_err());
}
#[test]
fn whitespace_only_returns_error() {
assert!(duration::decode(" ").is_err());
}
#[test]
fn unknown_unit_returns_error() {
assert!(duration::decode("5 fortnights").is_err());
}
#[test]
fn unit_only_no_number_returns_error() {
assert!(duration::decode("ms").is_err());
}
#[test]
fn error_carries_original_input() {
let err = duration::decode("bad input").unwrap_err();
assert_eq!(err.input, "bad input");
}
}
mod math {
use super::*;
#[test]
fn sum_adds_durations() {
let a = duration::seconds(1);
let b = duration::millis(500);
assert_eq!(duration::sum(a, b).as_millis(), 1_500);
}
#[test]
fn subtract_normal_case() {
let a = duration::seconds(2);
let b = duration::seconds(1);
assert_eq!(duration::subtract(a, b), duration::seconds(1));
}
#[test]
fn subtract_saturates_at_zero() {
assert_eq!(
duration::subtract(duration::seconds(1), duration::seconds(5)),
duration::ZERO
);
}
#[test]
fn times_multiplies() {
assert_eq!(
duration::times(duration::seconds(3), 4),
duration::seconds(12)
);
}
#[test]
fn min_returns_shorter() {
assert_eq!(
duration::min(duration::seconds(1), duration::seconds(5)),
duration::seconds(1)
);
}
#[test]
fn max_returns_longer() {
assert_eq!(
duration::max(duration::seconds(1), duration::seconds(5)),
duration::seconds(5)
);
}
#[rstest]
#[case::below_min(
duration::ZERO,
duration::seconds(1),
duration::seconds(10),
duration::seconds(1)
)]
#[case::in_range(
duration::seconds(5),
duration::seconds(1),
duration::seconds(10),
duration::seconds(5)
)]
#[case::above_max(
duration::seconds(20),
duration::seconds(1),
duration::seconds(10),
duration::seconds(10)
)]
#[case::at_min(
duration::seconds(1),
duration::seconds(1),
duration::seconds(10),
duration::seconds(1)
)]
#[case::at_max(
duration::seconds(10),
duration::seconds(1),
duration::seconds(10),
duration::seconds(10)
)]
fn clamp_cases(
#[case] d: Duration,
#[case] min: Duration,
#[case] max: Duration,
#[case] expected: Duration,
) {
assert_eq!(duration::clamp(d, min, max), expected);
}
#[rstest]
#[case::in_range(
duration::seconds(5),
duration::seconds(1),
duration::seconds(10),
true
)]
#[case::below(duration::ZERO, duration::seconds(1), duration::seconds(10), false)]
#[case::above(
duration::seconds(20),
duration::seconds(1),
duration::seconds(10),
false
)]
#[case::at_min(
duration::seconds(1),
duration::seconds(1),
duration::seconds(10),
true
)]
#[case::at_max(
duration::seconds(10),
duration::seconds(1),
duration::seconds(10),
true
)]
fn between_cases(
#[case] d: Duration,
#[case] min: Duration,
#[case] max: Duration,
#[case] expected: bool,
) {
assert_eq!(duration::between(d, min, max), expected);
}
}
mod extraction {
use super::*;
#[test]
fn to_millis_converts_correctly() {
assert_eq!(duration::to_millis(duration::seconds(2)), 2_000.0);
}
#[test]
fn to_nanos_converts_correctly() {
assert_eq!(duration::to_nanos(duration::millis(1)), 1_000_000);
}
#[test]
fn to_seconds_converts_correctly() {
assert!((duration::to_seconds(duration::millis(500)) - 0.5).abs() < 1e-10);
}
#[test]
fn to_hours_converts_correctly() {
assert!((duration::to_hours(duration::hours(2)) - 2.0).abs() < 1e-10);
}
}
mod format {
use super::*;
#[test]
fn zero_formats_as_0s() {
assert_eq!(duration::format(duration::ZERO), "0s");
}
#[test]
fn whole_seconds_format() {
assert_eq!(duration::format(duration::seconds(5)), "5s");
}
#[test]
fn millis_format() {
assert_eq!(duration::format(duration::millis(500)), "0.500s");
}
#[test]
fn minutes_format() {
assert_eq!(duration::format(duration::minutes(3)), "3m");
}
#[test]
fn hours_format() {
assert_eq!(duration::format(duration::hours(2)), "2h");
}
#[test]
fn combined_format() {
let d = duration::hours(1) + duration::minutes(2) + duration::seconds(3);
assert_eq!(duration::format(d), "1h 2m 3s");
}
#[test]
fn days_format() {
assert_eq!(duration::format(duration::days(1)), "1d");
}
#[test]
fn weeks_format() {
assert_eq!(duration::format(duration::weeks(1)), "1w");
}
#[test]
fn seconds_with_millis_format() {
let d = duration::seconds(3) + duration::millis(4);
assert_eq!(duration::format(d), "3.004s");
}
}
mod checks {
use super::*;
#[test]
fn is_zero_true_for_zero() {
assert!(duration::is_zero(duration::ZERO));
}
#[test]
fn is_zero_false_for_nonzero() {
assert!(!duration::is_zero(duration::millis(1)));
}
#[test]
fn is_finite_true_for_ordinary_duration() {
assert!(duration::is_finite(duration::seconds(100)));
}
#[test]
fn is_finite_false_for_max_duration() {
assert!(!duration::is_finite(duration::INFINITY));
}
}
}