use std::time::Duration;
const MAX_VALUE: u128 = 99_999_999;
const UNITS: &[(u128, char)] = &[
(1, 'n'),
(1_000, 'u'),
(1_000_000, 'm'),
(1_000_000_000, 'S'),
(60 * 1_000_000_000, 'M'),
(3600 * 1_000_000_000, 'H'),
];
pub fn format_grpc_timeout(d: Duration) -> String {
let nanos = d.as_nanos();
for &(unit_nanos, suffix) in UNITS {
let value = nanos.div_ceil(unit_nanos);
if value <= MAX_VALUE {
return format!("{value}{suffix}");
}
}
format!("{MAX_VALUE}H")
}
pub fn parse_grpc_timeout(s: &str) -> Option<Duration> {
let bytes = s.as_bytes();
let (&suffix, digits) = bytes.split_last()?;
if digits.is_empty() || !digits.iter().all(u8::is_ascii_digit) {
return None;
}
let value: u64 = std::str::from_utf8(digits).ok()?.parse().ok()?;
let unit_nanos: u64 = match suffix {
b'n' => 1,
b'u' => 1_000,
b'm' => 1_000_000,
b'S' => 1_000_000_000,
b'M' => 60 * 1_000_000_000,
b'H' => 3600 * 1_000_000_000,
_ => return None,
};
let total_nanos = (value as u128).checked_mul(unit_nanos as u128)?;
let secs = u64::try_from(total_nanos / 1_000_000_000).ok()?;
let nanos = (total_nanos % 1_000_000_000) as u32;
Some(Duration::new(secs, nanos))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn format_picks_smallest_fitting_unit() {
assert_eq!(format_grpc_timeout(Duration::from_nanos(500)), "500n");
assert_eq!(format_grpc_timeout(Duration::from_micros(500)), "500000n");
assert_eq!(format_grpc_timeout(Duration::from_millis(500)), "500000u");
assert_eq!(format_grpc_timeout(Duration::from_secs(5)), "5000000u");
assert_eq!(format_grpc_timeout(Duration::from_secs(300)), "300000m");
assert_eq!(format_grpc_timeout(Duration::from_secs(3600)), "3600000m");
assert_eq!(
format_grpc_timeout(Duration::from_secs(86_400)),
"86400000m"
);
}
#[test]
fn format_rounds_up() {
assert_eq!(format_grpc_timeout(Duration::from_nanos(1500)), "1500n");
assert_eq!(
format_grpc_timeout(Duration::from_nanos(99_999_999_500)),
"100000m"
);
}
#[test]
fn format_zero() {
assert_eq!(format_grpc_timeout(Duration::ZERO), "0n");
}
#[test]
fn format_saturates_at_max_hours() {
let huge = Duration::from_secs(u64::MAX); assert_eq!(format_grpc_timeout(huge), "99999999H");
}
#[test]
fn parse_each_unit() {
assert_eq!(parse_grpc_timeout("500n"), Some(Duration::from_nanos(500)));
assert_eq!(parse_grpc_timeout("500u"), Some(Duration::from_micros(500)));
assert_eq!(parse_grpc_timeout("500m"), Some(Duration::from_millis(500)));
assert_eq!(parse_grpc_timeout("5S"), Some(Duration::from_secs(5)));
assert_eq!(parse_grpc_timeout("2M"), Some(Duration::from_secs(120)));
assert_eq!(parse_grpc_timeout("1H"), Some(Duration::from_secs(3600)));
}
#[test]
fn parse_extreme_value() {
let d = parse_grpc_timeout("99999999H").unwrap();
assert_eq!(d.as_secs(), 99_999_999u64 * 3600);
}
#[test]
fn parse_rejects_malformed() {
assert_eq!(parse_grpc_timeout(""), None);
assert_eq!(parse_grpc_timeout("S"), None); assert_eq!(parse_grpc_timeout("5"), None); assert_eq!(parse_grpc_timeout("5s"), None); assert_eq!(parse_grpc_timeout("5x"), None); assert_eq!(parse_grpc_timeout("-1S"), None); assert_eq!(parse_grpc_timeout("1.5S"), None); assert_eq!(parse_grpc_timeout(" 5S"), None); }
#[test]
fn parse_zero_is_valid() {
assert_eq!(parse_grpc_timeout("0n"), Some(Duration::ZERO));
assert_eq!(parse_grpc_timeout("0S"), Some(Duration::ZERO));
}
#[test]
fn roundtrip_typical_durations() {
for d in [
Duration::from_millis(1),
Duration::from_millis(100),
Duration::from_secs(1),
Duration::from_secs(30),
Duration::from_secs(300),
] {
let formatted = format_grpc_timeout(d);
let parsed = parse_grpc_timeout(&formatted).unwrap();
assert_eq!(parsed, d, "roundtrip failed for {d:?} → {formatted:?}");
}
}
}