use crate::{PrimitiveError, PrimitiveResult};
use alloc::string::String;
use core::{fmt, str::FromStr, time::Duration};
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct HumanDuration(Duration);
impl HumanDuration {
pub fn parse(s: &str) -> PrimitiveResult<Self> {
if s.is_empty() {
return Err(PrimitiveError::Empty);
}
const RANK_H: u8 = 4;
const RANK_M: u8 = 3;
const RANK_S: u8 = 2;
const RANK_MS: u8 = 1;
let mut total_nanos: u128 = 0;
let mut last_rank: u8 = u8::MAX;
let mut pos = 0;
let bytes = s.as_bytes();
while pos < bytes.len() {
let num_start = pos;
while pos < bytes.len() && bytes[pos].is_ascii_digit() {
pos += 1;
}
if pos == num_start {
return Err(PrimitiveError::Invalid {
message: "expected a number before unit",
});
}
let num_str = &s[num_start..pos];
let num = parse_u64(num_str).ok_or(PrimitiveError::Invalid {
message: "duration number is too large",
})?;
let unit_start = pos;
while pos < bytes.len() && bytes[pos].is_ascii_alphabetic() {
pos += 1;
}
let unit = &s[unit_start..pos];
let (nanos_per_unit, rank): (u128, u8) = match unit {
"h" => (3_600 * 1_000_000_000, RANK_H),
"m" => (60 * 1_000_000_000, RANK_M),
"s" => (1_000_000_000, RANK_S),
"ms" => (1_000_000, RANK_MS),
_ => {
return Err(PrimitiveError::Invalid {
message: "unknown time unit; use h, m, s, or ms",
})
}
};
if rank >= last_rank {
return Err(PrimitiveError::Invalid {
message: "units must be in descending order (h, m, s, ms) with no duplicates",
});
}
last_rank = rank;
let component =
(num as u128)
.checked_mul(nanos_per_unit)
.ok_or(PrimitiveError::Invalid {
message: "duration overflow",
})?;
total_nanos = total_nanos
.checked_add(component)
.ok_or(PrimitiveError::Invalid {
message: "duration overflow",
})?;
}
let secs =
u64::try_from(total_nanos / 1_000_000_000).map_err(|_| PrimitiveError::Invalid {
message: "duration overflow: total duration exceeds maximum representable value",
})?;
let nanos = (total_nanos % 1_000_000_000) as u32;
Ok(Self(Duration::new(secs, nanos)))
}
pub fn as_duration(self) -> Duration {
self.0
}
pub fn as_secs(self) -> u64 {
self.0.as_secs()
}
pub fn as_millis(self) -> u128 {
self.0.as_millis()
}
}
fn parse_u64(s: &str) -> Option<u64> {
if s.is_empty() {
return None;
}
let mut result: u64 = 0;
for c in s.chars() {
let digit = c.to_digit(10)? as u64;
result = result.checked_mul(10)?.checked_add(digit)?;
}
Some(result)
}
impl fmt::Display for HumanDuration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let total_secs = self.0.as_secs();
let millis = self.0.subsec_millis();
let h = total_secs / 3600;
let m = (total_secs % 3600) / 60;
let s = total_secs % 60;
let mut wrote = false;
if h > 0 {
write!(f, "{h}h")?;
wrote = true;
}
if m > 0 {
write!(f, "{m}m")?;
wrote = true;
}
if s > 0 || millis > 0 {
if s > 0 {
write!(f, "{s}s")?;
}
if millis > 0 {
write!(f, "{millis}ms")?;
}
wrote = true;
}
if !wrote {
write!(f, "0s")?;
}
Ok(())
}
}
impl FromStr for HumanDuration {
type Err = PrimitiveError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl PartialEq<str> for HumanDuration {
fn eq(&self, other: &str) -> bool {
Self::parse(other).is_ok_and(|other| self == &other)
}
}
impl PartialEq<&str> for HumanDuration {
fn eq(&self, other: &&str) -> bool {
self.eq(*other)
}
}
impl PartialEq<String> for HumanDuration {
fn eq(&self, other: &String) -> bool {
self.eq(other.as_str())
}
}
impl PartialEq<&String> for HumanDuration {
fn eq(&self, other: &&String) -> bool {
self.eq(other.as_str())
}
}
#[cfg(test)]
mod tests {
use super::HumanDuration;
use crate::PrimitiveError;
use alloc::string::ToString;
#[test]
fn parses_seconds() {
assert_eq!(HumanDuration::parse("45s").unwrap().as_secs(), 45);
}
#[test]
fn parses_minutes() {
assert_eq!(HumanDuration::parse("2m").unwrap().as_secs(), 120);
}
#[test]
fn parses_hours() {
assert_eq!(HumanDuration::parse("1h").unwrap().as_secs(), 3600);
}
#[test]
fn parses_milliseconds() {
assert_eq!(HumanDuration::parse("500ms").unwrap().as_millis(), 500);
}
#[test]
fn parses_combination() {
let d = HumanDuration::parse("1h30m45s").unwrap();
assert_eq!(d.as_secs(), 3600 + 1800 + 45);
}
#[test]
fn parses_minutes_and_seconds() {
assert_eq!(HumanDuration::parse("2m30s").unwrap().as_secs(), 150);
}
#[test]
fn rejects_empty() {
assert_eq!(HumanDuration::parse("").unwrap_err(), PrimitiveError::Empty);
}
#[test]
fn rejects_unknown_unit() {
assert!(HumanDuration::parse("5d").is_err());
}
#[test]
fn rejects_no_number() {
assert!(HumanDuration::parse("s").is_err());
}
#[test]
fn rejects_out_of_order_units() {
assert!(HumanDuration::parse("1s1h").is_err());
}
#[test]
fn rejects_duplicate_units() {
assert!(HumanDuration::parse("1h1h").is_err());
}
#[test]
fn rejects_ms_before_s() {
assert!(HumanDuration::parse("500ms30s").is_err());
}
#[test]
fn as_duration() {
let d = HumanDuration::parse("1s").unwrap();
assert_eq!(d.as_duration().as_secs(), 1);
}
#[test]
fn display_seconds() {
assert_eq!(HumanDuration::parse("45s").unwrap().to_string(), "45s");
}
#[test]
fn display_combined() {
assert_eq!(HumanDuration::parse("1h30m").unwrap().to_string(), "1h30m");
}
#[test]
fn display_zero() {
assert_eq!(HumanDuration::parse("0s").unwrap().to_string(), "0s");
}
#[test]
fn display_mixed_seconds_and_millis() {
assert_eq!(
HumanDuration::parse("1s500ms").unwrap().to_string(),
"1s500ms"
);
}
#[test]
fn display_millis_only() {
assert_eq!(HumanDuration::parse("500ms").unwrap().to_string(), "500ms");
}
#[test]
fn rejects_duration_that_overflows_u64_seconds() {
assert!(HumanDuration::parse("18446744073709551615h").is_err());
}
#[test]
fn from_str_and_string_comparisons() {
let duration = "1m30s".parse::<HumanDuration>().unwrap();
let owned = "90s".to_string();
assert_eq!(duration, "1m30s");
assert_eq!(duration, owned);
assert!("1s1m".parse::<HumanDuration>().is_err());
}
}