ic_host_text/duration/
mod.rs1use std::fmt;
5
6#[derive(Clone, Debug, Eq, PartialEq)]
9pub enum DurationParseError {
10 Invalid {
12 value: String,
14 },
15}
16
17impl fmt::Display for DurationParseError {
18 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19 let Self::Invalid { value } = self;
20 write!(
21 f,
22 "invalid duration {value:?}; use positive seconds or a value ending in s, m, h, or d"
23 )
24 }
25}
26
27impl std::error::Error for DurationParseError {}
28
29pub fn parse_duration_seconds(value: &str) -> Result<u64, DurationParseError> {
43 let (number, multiplier) = match value.as_bytes().last().copied() {
44 Some(b's') => (&value[..value.len() - 1], 1),
45 Some(b'm') => (&value[..value.len() - 1], 60),
46 Some(b'h') => (&value[..value.len() - 1], 60 * 60),
47 Some(b'd') => (&value[..value.len() - 1], 24 * 60 * 60),
48 Some(b'0'..=b'9') => (value, 1),
49 _ => {
50 return Err(DurationParseError::Invalid {
51 value: value.to_string(),
52 });
53 }
54 };
55 number
56 .parse::<u64>()
57 .ok()
58 .and_then(|amount| amount.checked_mul(multiplier))
59 .filter(|seconds| *seconds > 0)
60 .ok_or_else(|| DurationParseError::Invalid {
61 value: value.to_string(),
62 })
63}
64
65#[must_use]
76pub fn display_duration_seconds(seconds: u64) -> String {
77 const MINUTE: u64 = 60;
78 const HOUR: u64 = 60 * MINUTE;
79 const DAY: u64 = 24 * HOUR;
80
81 if seconds == 0 {
82 "0s".to_string()
83 } else if seconds >= DAY {
84 scaled_duration_unit_text(seconds, DAY, "d")
85 } else if seconds >= HOUR {
86 scaled_duration_unit_text(seconds, HOUR, "h")
87 } else if seconds >= MINUTE {
88 scaled_duration_unit_text(seconds, MINUTE, "m")
89 } else {
90 format!("{seconds}s")
91 }
92}
93
94fn scaled_duration_unit_text(seconds: u64, unit_seconds: u64, suffix: &str) -> String {
95 if seconds.is_multiple_of(unit_seconds) {
96 return format!("{}{suffix}", seconds / unit_seconds);
97 }
98 let hundredths =
99 ((u128::from(seconds) * 100) + (u128::from(unit_seconds) / 2)) / u128::from(unit_seconds);
100 let whole = hundredths / 100;
101 let fractional = hundredths % 100;
102 format!("{whole}.{fractional:02}{suffix}")
103}
104
105#[must_use]
107pub fn compact_duration(seconds: u64) -> String {
108 const MINUTE: u64 = 60;
109 const HOUR: u64 = 60 * MINUTE;
110 const DAY: u64 = 24 * HOUR;
111 const WEEK: u64 = 7 * DAY;
112
113 match seconds {
114 0 => "0s".to_string(),
115 1..MINUTE => format!("{seconds}s"),
116 MINUTE..HOUR => compact_duration_pair(seconds, MINUTE, "m", 1, "s"),
117 HOUR..DAY => compact_duration_pair(seconds, HOUR, "h", MINUTE, "m"),
118 DAY..WEEK => compact_duration_pair(seconds, DAY, "d", HOUR, "h"),
119 _ => compact_duration_pair(seconds, WEEK, "w", DAY, "d"),
120 }
121}
122
123fn compact_duration_pair(
124 seconds: u64,
125 major_seconds: u64,
126 major_unit: &str,
127 minor_seconds: u64,
128 minor_unit: &str,
129) -> String {
130 let major = seconds / major_seconds;
131 let minor = (seconds % major_seconds) / minor_seconds;
132 if minor == 0 {
133 format!("{major}{major_unit}")
134 } else {
135 format!("{major}{major_unit} {minor}{minor_unit}")
136 }
137}
138
139#[cfg(test)]
140mod tests;