use std::fmt;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum DurationParseError {
Invalid {
value: String,
},
}
impl fmt::Display for DurationParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self::Invalid { value } = self;
write!(
f,
"invalid duration {value:?}; use positive seconds or a value ending in s, m, h, or d"
)
}
}
impl std::error::Error for DurationParseError {}
pub fn parse_duration_seconds(value: &str) -> Result<u64, DurationParseError> {
let (number, multiplier) = match value.as_bytes().last().copied() {
Some(b's') => (&value[..value.len() - 1], 1),
Some(b'm') => (&value[..value.len() - 1], 60),
Some(b'h') => (&value[..value.len() - 1], 60 * 60),
Some(b'd') => (&value[..value.len() - 1], 24 * 60 * 60),
Some(b'0'..=b'9') => (value, 1),
_ => {
return Err(DurationParseError::Invalid {
value: value.to_string(),
});
}
};
number
.parse::<u64>()
.ok()
.and_then(|amount| amount.checked_mul(multiplier))
.filter(|seconds| *seconds > 0)
.ok_or_else(|| DurationParseError::Invalid {
value: value.to_string(),
})
}
#[must_use]
pub fn display_duration_seconds(seconds: u64) -> String {
const MINUTE: u64 = 60;
const HOUR: u64 = 60 * MINUTE;
const DAY: u64 = 24 * HOUR;
if seconds == 0 {
"0s".to_string()
} else if seconds >= DAY {
scaled_duration_unit_text(seconds, DAY, "d")
} else if seconds >= HOUR {
scaled_duration_unit_text(seconds, HOUR, "h")
} else if seconds >= MINUTE {
scaled_duration_unit_text(seconds, MINUTE, "m")
} else {
format!("{seconds}s")
}
}
fn scaled_duration_unit_text(seconds: u64, unit_seconds: u64, suffix: &str) -> String {
if seconds.is_multiple_of(unit_seconds) {
return format!("{}{suffix}", seconds / unit_seconds);
}
let hundredths =
((u128::from(seconds) * 100) + (u128::from(unit_seconds) / 2)) / u128::from(unit_seconds);
let whole = hundredths / 100;
let fractional = hundredths % 100;
format!("{whole}.{fractional:02}{suffix}")
}
#[must_use]
pub fn compact_duration(seconds: u64) -> String {
const MINUTE: u64 = 60;
const HOUR: u64 = 60 * MINUTE;
const DAY: u64 = 24 * HOUR;
const WEEK: u64 = 7 * DAY;
match seconds {
0 => "0s".to_string(),
1..MINUTE => format!("{seconds}s"),
MINUTE..HOUR => compact_duration_pair(seconds, MINUTE, "m", 1, "s"),
HOUR..DAY => compact_duration_pair(seconds, HOUR, "h", MINUTE, "m"),
DAY..WEEK => compact_duration_pair(seconds, DAY, "d", HOUR, "h"),
_ => compact_duration_pair(seconds, WEEK, "w", DAY, "d"),
}
}
fn compact_duration_pair(
seconds: u64,
major_seconds: u64,
major_unit: &str,
minor_seconds: u64,
minor_unit: &str,
) -> String {
let major = seconds / major_seconds;
let minor = (seconds % major_seconds) / minor_seconds;
if minor == 0 {
format!("{major}{major_unit}")
} else {
format!("{major}{major_unit} {minor}{minor_unit}")
}
}
#[cfg(test)]
mod tests;