Skip to main content

ic_query/
duration.rs

1//! Module: duration
2//!
3//! Responsibility: parse duration values and render compact duration values.
4//!
5//! Does not own: command definitions, cache policy decisions, or report layouts.
6//!
7//! Boundary: keeps human duration syntax centralized so commands and reports do not
8//! each invent their own parsing or display conventions.
9
10use thiserror::Error as ThisError;
11
12///
13/// DurationParseError
14///
15/// Error returned when a duration value cannot be parsed into seconds.
16///
17
18#[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
24/// Parses a positive duration string into seconds.
25///
26/// Accepts bare seconds or integer values ending in `s`, `m`, `h`, or `d`.
27pub 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/// Renders seconds using the largest readable duration unit.
51#[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}