#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PrettyDuration(pub std::time::Duration);
impl PrettyDuration {
pub fn new(duration: std::time::Duration) -> Self {
Self(duration)
}
pub fn as_duration(&self) -> std::time::Duration {
self.0
}
pub fn from_secs(secs: u64) -> Self {
Self(std::time::Duration::from_secs(secs))
}
pub fn from_mins(mins: u64) -> Self {
Self(std::time::Duration::from_secs(mins * 60))
}
pub fn from_hours(hours: u64) -> Self {
Self(std::time::Duration::from_secs(hours * 60 * 60))
}
}
impl std::ops::Deref for PrettyDuration {
type Target = std::time::Duration;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<std::time::Duration> for PrettyDuration {
fn from(duration: std::time::Duration) -> Self {
Self::new(duration)
}
}
impl std::fmt::Display for PrettyDuration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
const DAY: u64 = 60 * 60 * 24;
const HOUR: u64 = 60 * 60;
let secs = self.0.as_secs();
let days = secs / DAY;
if days > 0 {
write!(f, "{}d", days)?;
}
let secs = secs % DAY;
let hours = secs / HOUR;
if hours > 0 {
write!(f, "{}h", hours)?;
}
let secs = secs % HOUR;
let mins = secs / 60;
if mins > 0 {
write!(f, "{}m", mins)?;
}
let secs = secs % 60;
if secs > 0 {
write!(f, "{}s", secs)?;
}
Ok(())
}
}
#[derive(Debug)]
pub struct PrettyDurationParseError {
value: String,
message: String,
}
impl std::fmt::Display for PrettyDurationParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Invalid time spec '{}': {}", self.value, self.message)
}
}
impl std::error::Error for PrettyDurationParseError {}
impl std::str::FromStr for PrettyDuration {
type Err = PrettyDurationParseError;
fn from_str(mut input: &str) -> Result<Self, Self::Err> {
let mut seconds = 0;
loop {
input = input.strip_prefix(' ').unwrap_or(input);
if input.is_empty() {
break;
}
let nums = input.chars().take_while(|x| x.is_ascii_digit()).count();
if nums < 1 {
return Err(PrettyDurationParseError {
message: "must start with a number".to_string(),
value: input.to_string(),
});
}
let number = &input[..nums]
.parse::<u64>()
.map_err(|e| PrettyDurationParseError {
message: format!("invalid number: {}", e),
value: input.to_string(),
})?;
input = &input[nums..];
let chars = input
.chars()
.take_while(|x| x.is_ascii_alphabetic())
.count();
let unit = &input[..chars];
input = &input[chars..];
let scale = match unit {
"s" | "sec" | "secs" | "seconds" => 1,
"m" | "min" | "mins" | "minutes" => 60,
"h" | "hour" | "hours" => 60 * 60,
"d" | "day" | "days" => 60 * 60 * 24,
_ => {
return Err(PrettyDurationParseError {
message: "unknown unit".to_string(),
value: input.to_string(),
});
}
};
seconds += number * scale;
}
let dur = std::time::Duration::from_secs(seconds);
Ok(Self(dur))
}
}
impl serde::Serialize for PrettyDuration {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.to_string().serialize(serializer)
}
}
impl<'de> serde::Deserialize<'de> for PrettyDuration {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
#[test]
fn test_pretty_duration_constructors() {
assert_eq!(
PrettyDuration::from_secs(1).as_duration(),
Duration::from_secs(1)
);
assert_eq!(
PrettyDuration::from_mins(1).as_duration(),
Duration::from_secs(60)
);
assert_eq!(
PrettyDuration::from_hours(1).as_duration(),
Duration::from_secs(60 * 60)
);
}
#[test]
fn test_pretty_duration_parse() {
let cases = &[
("1s", Duration::from_secs(1), "1s"),
("10s", Duration::from_secs(10), "10s"),
("59s", Duration::from_secs(59), "59s"),
("60s", Duration::from_secs(60), "1m"),
("1m", Duration::from_secs(60), "1m"),
("11m", Duration::from_secs(60) * 11, "11m"),
("60m", Duration::from_secs(60) * 60, "1h"),
("1h", Duration::from_secs(60) * 60, "1h"),
("11h", Duration::from_secs(60) * 60 * 11, "11h"),
("1h1m", Duration::from_secs(61) * 60, "1h1m"),
("1h1m1s", Duration::from_secs(61 * 60 + 1), "1h1m1s"),
];
for (index, (input, duration, output)) in cases.iter().enumerate() {
eprintln!("test case {index}: {input} => {duration:?} => {output}");
let p = input.parse::<PrettyDuration>().unwrap();
assert_eq!(p, PrettyDuration::new(*duration));
assert_eq!(p.to_string(), output.to_string());
}
}
#[test]
fn test_pretty_duration_serde() {
let dur = PrettyDuration::from_secs(1);
let json = serde_json::to_string(&dur).unwrap();
assert_eq!(json, "\"1s\"");
let dur2: PrettyDuration = serde_json::from_str(&json).unwrap();
assert_eq!(dur, dur2);
}
}