use chrono::Duration;
pub fn parse_duration(s: &str) -> Result<Duration, String> {
let s = s.trim();
if s.is_empty() {
return Err("duration is empty".into());
}
let split = s
.find(|c: char| !c.is_ascii_digit())
.ok_or_else(|| format!("duration '{s}' has no unit suffix (try 30s, 5m, 2h, 7d)"))?;
let (num, unit) = s.split_at(split);
if num.is_empty() {
return Err(format!("duration '{s}': missing number before unit"));
}
let n: i64 = num
.parse()
.map_err(|_| format!("duration '{s}': invalid number '{num}'"))?;
let unit = unit.trim();
let dur = match unit {
"ms" => Duration::milliseconds(n),
"s" | "sec" | "secs" => Duration::seconds(n),
"m" | "min" | "mins" => Duration::minutes(n),
"h" | "hr" | "hrs" => Duration::hours(n),
"d" | "day" | "days" => Duration::days(n),
other => return Err(format!("duration '{s}': unknown unit '{other}'")),
};
Ok(dur)
}
pub fn parse_size(s: &str) -> Result<u64, String> {
let s = s.trim();
if s.is_empty() {
return Err("size is empty".into());
}
let split = match s.find(|c: char| !c.is_ascii_digit()) {
None => {
return s.parse().map_err(|_| format!("size '{s}': invalid number"));
}
Some(i) => i,
};
let (num, unit) = s.split_at(split);
if num.is_empty() {
return Err(format!("size '{s}': missing number before unit"));
}
let n: u64 = num
.parse()
.map_err(|_| format!("size '{s}': invalid number '{num}'"))?;
let unit = unit.trim();
let mul: u64 = match unit {
"B" => 1,
"KB" => 1_000,
"MB" => 1_000_000,
"GB" => 1_000_000_000,
"KiB" => 1_024,
"MiB" => 1_024 * 1_024,
"GiB" => 1_024 * 1_024 * 1_024,
other => return Err(format!("size '{s}': unknown unit '{other}'")),
};
n.checked_mul(mul)
.ok_or_else(|| format!("size '{s}': value overflows u64 bytes"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_every_unit_and_alias() {
assert_eq!(
parse_duration("250ms").unwrap(),
Duration::milliseconds(250)
);
assert_eq!(parse_duration("30s").unwrap(), Duration::seconds(30));
assert_eq!(parse_duration("30sec").unwrap(), Duration::seconds(30));
assert_eq!(parse_duration("30secs").unwrap(), Duration::seconds(30));
assert_eq!(parse_duration("5m").unwrap(), Duration::minutes(5));
assert_eq!(parse_duration("5min").unwrap(), Duration::minutes(5));
assert_eq!(parse_duration("90mins").unwrap(), Duration::minutes(90));
assert_eq!(parse_duration("2h").unwrap(), Duration::hours(2));
assert_eq!(parse_duration("2hr").unwrap(), Duration::hours(2));
assert_eq!(parse_duration("2hrs").unwrap(), Duration::hours(2));
assert_eq!(parse_duration("7d").unwrap(), Duration::days(7));
assert_eq!(parse_duration("7day").unwrap(), Duration::days(7));
assert_eq!(parse_duration("7days").unwrap(), Duration::days(7));
}
#[test]
fn trims_surrounding_whitespace() {
assert_eq!(parse_duration(" 5m ").unwrap(), Duration::minutes(5));
}
#[test]
fn rejects_bare_integer() {
assert!(parse_duration("10").is_err());
}
#[test]
fn rejects_empty_unit_and_unknown_inputs() {
assert!(parse_duration("").is_err());
assert!(parse_duration(" ").is_err());
assert!(parse_duration("hour").is_err()); assert!(parse_duration("ms").is_err()); assert!(parse_duration("10x").is_err()); assert!(parse_duration("-5m").is_err()); }
#[test]
fn unit_whitespace_is_tolerated() {
assert_eq!(parse_duration("5 m").unwrap(), Duration::minutes(5));
}
#[test]
fn error_messages_name_the_input() {
let err = parse_duration("10x").unwrap_err();
assert!(err.contains("10x"), "error should echo the input: {err}");
assert!(
err.contains("unknown unit"),
"error should name the fault: {err}"
);
}
#[test]
fn parse_size_bare_integer_is_bytes() {
assert_eq!(parse_size("0").unwrap(), 0);
assert_eq!(parse_size("1024").unwrap(), 1024);
}
#[test]
fn parse_size_decimal_si_units() {
assert_eq!(parse_size("1B").unwrap(), 1);
assert_eq!(parse_size("1KB").unwrap(), 1_000);
assert_eq!(parse_size("1MB").unwrap(), 1_000_000);
assert_eq!(parse_size("2GB").unwrap(), 2_000_000_000);
}
#[test]
fn parse_size_binary_iec_units() {
assert_eq!(parse_size("1KiB").unwrap(), 1_024);
assert_eq!(parse_size("1MiB").unwrap(), 1_048_576);
assert_eq!(parse_size("1GiB").unwrap(), 1_073_741_824);
}
#[test]
fn parse_size_trims_surrounding_whitespace() {
assert_eq!(parse_size(" 5MB ").unwrap(), 5_000_000);
}
#[test]
fn parse_size_rejects_bad_inputs() {
assert!(parse_size("").is_err());
assert!(parse_size(" ").is_err());
assert!(parse_size("abc").is_err()); assert!(parse_size("5x").is_err()); assert!(parse_size("-5MB").is_err()); }
#[test]
fn parse_size_oversized_value_errors_without_panic() {
let err = parse_size("99999999999GB").unwrap_err();
assert!(
err.contains("99999999999GB"),
"error should echo input: {err}"
);
assert!(
err.contains("overflow"),
"error should name overflow: {err}"
);
}
#[test]
fn parse_size_error_messages_name_the_input() {
let err = parse_size("5x").unwrap_err();
assert!(err.contains("5x"), "error should echo the input: {err}");
assert!(
err.contains("unknown unit"),
"error should name the fault: {err}"
);
}
}