use std::{error::Error as StdError, num::IntErrorKind, sync::LazyLock, time::Duration};
use adbc_core::error::{Error, Result, Status};
use regex::Regex;
static RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?P<int>[0-9]*)\.?(?P<frac>[0-9]*)(?P<unit>ns|us|µs|μs|ms|s|m|h)")
.expect("valid regex")
});
fn invalid_arg(err: impl Into<String>) -> Error {
Error::with_message_and_status(err.into(), Status::InvalidArguments)
}
fn arg_err(err: impl StdError) -> Error {
invalid_arg(err.to_string())
}
fn overflow() -> Error {
invalid_arg("duration overflow")
}
fn bad_input<T>() -> Result<T> {
Err(invalid_arg("invalid duration (valid durations are a sequence of decimal numbers, each with optional fraction and a unit suffix, such as 300ms, 1.5h, 2h45m, valid time units are ns, us, ms, s, m, h)"))
}
pub(crate) fn parse_duration(input: &str) -> Result<Duration> {
let input = input.replace(['+', '-'], "");
if input == "0" {
return Ok(Duration::ZERO);
}
if RE.find(&input).is_none() {
return bad_input();
}
RE.captures_iter(&input)
.map(|caps| caps.extract())
.try_fold(Duration::ZERO, |mut duration, (_, [int, frac, unit])| {
if int.is_empty() && frac.is_empty() {
return bad_input();
}
let int = int
.parse::<u64>()
.or_else(|err| match err.kind() {
&IntErrorKind::Empty => Ok(0),
_ => Err(err),
})
.map_err(arg_err)?;
let frac = format!("0.{frac}").parse::<f64>().map_err(arg_err)?;
duration += match unit {
"h" => {
Duration::from_secs(3600u64.checked_mul(int).ok_or(overflow())?)
+ Duration::try_from_secs_f64(3600f64 * frac).map_err(arg_err)?
}
"m" => {
Duration::from_secs(60u64.checked_mul(int).ok_or(overflow())?)
+ Duration::try_from_secs_f64(60f64 * frac).map_err(arg_err)?
}
"s" => {
Duration::from_secs(int) + Duration::try_from_secs_f64(frac).map_err(arg_err)?
}
"ms" => {
Duration::from_millis(int)
+ Duration::try_from_secs_f64(frac / 1e3).map_err(arg_err)?
}
"us" | "µs" | "μs" => {
Duration::from_micros(int)
+ Duration::try_from_secs_f64(frac / 1e6).map_err(arg_err)?
}
"ns" => {
if frac != 0f64 {
return Err(invalid_arg(
"unexpected fractional part for duration with ns unit",
));
}
Duration::from_nanos(int)
}
_ => unreachable!("<unit> matching group prevents this branch"),
};
Ok(duration)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse() {
assert_eq!(parse_duration("0"), Ok(Duration::from_secs(0)));
assert_eq!(parse_duration("0000.0000s"), Ok(Duration::from_secs(0)));
assert_eq!(parse_duration("5s"), Ok(Duration::from_secs(5)));
assert_eq!(parse_duration("30s"), Ok(Duration::from_secs(30)));
assert_eq!(parse_duration("1478s"), Ok(Duration::from_secs(1478)));
assert_eq!(parse_duration("-5s"), Ok(Duration::from_secs(5)));
assert_eq!(parse_duration("+5s"), Ok(Duration::from_secs(5)));
assert_eq!(parse_duration("-0"), Ok(Duration::from_secs(0)));
assert_eq!(parse_duration("+0"), Ok(Duration::from_secs(0)));
assert_eq!(parse_duration("5.0s"), Ok(Duration::from_secs(5)));
assert_eq!(
parse_duration("5.6s"),
Ok(Duration::from_secs(5) + Duration::from_millis(600))
);
assert_eq!(parse_duration("5.s"), Ok(Duration::from_secs(5)));
assert_eq!(parse_duration(".5s"), Ok(Duration::from_millis(500)));
assert_eq!(parse_duration("1.0s"), Ok(Duration::from_secs(1)));
assert_eq!(parse_duration("1.00s"), Ok(Duration::from_secs(1)));
assert_eq!(
parse_duration("1.004s"),
Ok(Duration::from_secs(1) + Duration::from_millis(4))
);
assert_eq!(
parse_duration("1.0040s"),
Ok(Duration::from_secs(1) + Duration::from_millis(4))
);
assert_eq!(
parse_duration("100.00100s"),
Ok(Duration::from_secs(100) + Duration::from_millis(1))
);
assert_eq!(parse_duration("10ns"), Ok(Duration::from_nanos(10)));
assert_eq!(parse_duration("11us"), Ok(Duration::from_micros(11)));
assert_eq!(parse_duration("12µs"), Ok(Duration::from_micros(12)));
assert_eq!(parse_duration("12μs"), Ok(Duration::from_micros(12)));
assert_eq!(parse_duration("13ms"), Ok(Duration::from_millis(13)));
assert_eq!(parse_duration("14s"), Ok(Duration::from_secs(14)));
assert_eq!(parse_duration("15m"), Ok(Duration::from_secs(60 * 15)));
assert_eq!(parse_duration("16h"), Ok(Duration::from_secs(60 * 60 * 16)));
assert_eq!(
parse_duration("3h30m"),
Ok(Duration::from_secs(60 * 60 * 3) + Duration::from_secs(60 * 30))
);
assert_eq!(
parse_duration("10.5s4m"),
Ok(Duration::from_secs(10) + Duration::from_millis(500) + Duration::from_secs(60 * 4))
);
assert_eq!(
parse_duration("-2m3.4s"),
Ok(Duration::from_secs(60 * 2) + Duration::from_secs(3) + Duration::from_millis(400))
);
assert_eq!(
parse_duration("1h2m3s4ms5us6ns"),
Ok(Duration::from_secs(60 * 60)
+ Duration::from_secs(60 * 2)
+ Duration::from_secs(3)
+ Duration::from_millis(4)
+ Duration::from_micros(5)
+ Duration::from_nanos(6))
);
assert_eq!(
parse_duration("39h9m14.425s"),
Ok(Duration::from_secs(60 * 60 * 39)
+ Duration::from_secs(60 * 9)
+ Duration::from_secs(14)
+ Duration::from_millis(425))
);
assert_eq!(
parse_duration("52763797000ns"),
Ok(Duration::from_nanos(52763797000))
);
assert_eq!(
parse_duration("0.3333333333333333333h"),
Ok(Duration::from_secs(60 * 20))
);
assert_eq!(
parse_duration("9007199254740993ns"),
Ok(Duration::from_nanos(9007199254740993))
);
assert_eq!(
parse_duration("9223372036854775807ns"),
Ok(Duration::from_nanos(9223372036854775807))
);
assert_eq!(
parse_duration("9223372036854775.807us"),
Ok(Duration::from_micros(9223372036854775) + Duration::from_nanos(807))
);
assert_eq!(
parse_duration("9223372036s854ms775us807ns"),
Ok(Duration::from_secs(9223372036)
+ Duration::from_millis(854)
+ Duration::from_micros(775)
+ Duration::from_nanos(807))
);
assert_eq!(
parse_duration("-9223372036854775808ns"),
Ok(Duration::from_nanos(9223372036854775808))
);
assert_eq!(
parse_duration("-9223372036854775.808us"),
Ok(Duration::from_micros(9223372036854775) + Duration::from_nanos(808))
);
assert_eq!(
parse_duration("-9223372036s854ms775us808ns"),
Ok(Duration::from_secs(9223372036)
+ Duration::from_millis(854)
+ Duration::from_micros(775)
+ Duration::from_nanos(808))
);
assert_eq!(
parse_duration("-9223372036854775808ns"),
Ok(Duration::from_nanos(9223372036854775808))
);
assert_eq!(
parse_duration("-2562047h47m16.854775808s"),
Ok(Duration::from_secs(60 * 60 * 2562047)
+ Duration::from_secs(60 * 47)
+ Duration::from_secs(16)
+ Duration::from_nanos(854775808))
);
assert_eq!(
parse_duration("0.100000000000000000000h"),
Ok(Duration::from_secs(60 * 6))
);
assert_eq!(
parse_duration("0.830103483285477580700h"),
Ok(Duration::from_secs(60 * 49)
+ Duration::from_secs(48)
+ Duration::from_nanos(372539828))
);
let bad_input = Err(invalid_arg("invalid duration (valid durations are a sequence of decimal numbers, each with optional fraction and a unit suffix, such as 300ms, 1.5h, 2h45m, valid time units are ns, us, ms, s, m, h)"));
assert_eq!(parse_duration(""), bad_input);
assert_eq!(parse_duration("3"), bad_input);
assert_eq!(parse_duration("-"), bad_input);
assert_eq!(parse_duration("s"), bad_input);
assert_eq!(parse_duration("."), bad_input);
assert_eq!(parse_duration("-."), bad_input);
assert_eq!(parse_duration(".s"), bad_input);
assert_eq!(parse_duration("+.s"), bad_input);
assert_eq!(parse_duration("1d"), bad_input);
assert_eq!(
parse_duration("1.1ns"),
Err(invalid_arg(
"unexpected fractional part for duration with ns unit"
))
);
assert_eq!(parse_duration("9999999999999999h"), Err(overflow()));
}
}