1use thiserror::Error as ThisError;
11
12#[derive(Debug, ThisError)]
19pub enum DurationParseError {
20 #[error("invalid duration {value:?}; use positive seconds or a value ending in s, m, h, or d")]
21 Invalid { value: String },
22}
23
24pub fn parse_duration_seconds(value: &str) -> Result<u64, DurationParseError> {
28 let (number, multiplier) = match value.as_bytes().last().copied() {
29 Some(b's') => (&value[..value.len() - 1], 1),
30 Some(b'm') => (&value[..value.len() - 1], 60),
31 Some(b'h') => (&value[..value.len() - 1], 60 * 60),
32 Some(b'd') => (&value[..value.len() - 1], 24 * 60 * 60),
33 Some(b'0'..=b'9') => (value, 1),
34 _ => {
35 return Err(DurationParseError::Invalid {
36 value: value.to_string(),
37 });
38 }
39 };
40 number
41 .parse::<u64>()
42 .ok()
43 .and_then(|amount| amount.checked_mul(multiplier))
44 .filter(|seconds| *seconds > 0)
45 .ok_or_else(|| DurationParseError::Invalid {
46 value: value.to_string(),
47 })
48}
49
50#[must_use]
52pub fn display_duration_seconds(seconds: u64) -> String {
53 const MINUTE: u64 = 60;
54 const HOUR: u64 = 60 * MINUTE;
55 const DAY: u64 = 24 * HOUR;
56
57 if seconds == 0 {
58 "0s".to_string()
59 } else if seconds >= DAY {
60 scaled_duration_unit_text(seconds, DAY, "d")
61 } else if seconds >= HOUR {
62 scaled_duration_unit_text(seconds, HOUR, "h")
63 } else if seconds >= MINUTE {
64 scaled_duration_unit_text(seconds, MINUTE, "m")
65 } else {
66 format!("{seconds}s")
67 }
68}
69
70fn scaled_duration_unit_text(seconds: u64, unit_seconds: u64, suffix: &str) -> String {
71 if seconds.is_multiple_of(unit_seconds) {
72 return format!("{}{suffix}", seconds / unit_seconds);
73 }
74 let hundredths =
75 ((u128::from(seconds) * 100) + (u128::from(unit_seconds) / 2)) / u128::from(unit_seconds);
76 let whole = hundredths / 100;
77 let fractional = hundredths % 100;
78 format!("{whole}.{fractional:02}{suffix}")
79}