use chrono::Duration;
use nom::branch::alt;
use nom::bytes::complete::tag;
use nom::character::complete::char;
use nom::combinator::{map, opt};
use nom::multi::many1;
use nom::number::complete::double;
use nom::IResult;
pub fn parse_duration(i: &str) -> IResult<&str, Duration> {
let (i, neg) = opt(parse_negative)(i)?;
if i == "0" {
return Ok((i, Duration::zero()));
}
let (i, components) = many1(parse_number_unit)(i)?;
let mut duration = Duration::zero();
for component in components {
duration = match duration.checked_add(&component) {
Some(duration) => duration,
None => return Err(too_large(i)),
};
}
Ok((i, duration * if neg.is_some() { -1 } else { 1 }))
}
fn too_large(i: &str) -> nom::Err<nom::error::Error<&str>> {
nom::Err::Failure(nom::error::Error::new(i, nom::error::ErrorKind::TooLarge))
}
enum Unit {
Nanosecond,
Microsecond,
Millisecond,
Second,
Minute,
Hour,
}
impl Unit {
fn nanos(&self) -> i64 {
match self {
Unit::Nanosecond => 1,
Unit::Microsecond => 1_000,
Unit::Millisecond => 1_000_000,
Unit::Second => 1_000_000_000,
Unit::Minute => 60 * 1_000_000_000,
Unit::Hour => 60 * 60 * 1_000_000_000,
}
}
}
fn parse_number_unit(i: &str) -> IResult<&str, Duration> {
let (i, num) = double(i)?;
let (i, unit) = parse_unit(i)?;
match to_duration(num, unit) {
Some(duration) => Ok((i, duration)),
None => Err(too_large(i)),
}
}
fn parse_negative(i: &str) -> IResult<&str, ()> {
let (i, _): (&str, char) = char('-')(i)?;
Ok((i, ()))
}
fn parse_unit(i: &str) -> IResult<&str, Unit> {
alt((
map(tag("ms"), |_| Unit::Millisecond),
map(tag("us"), |_| Unit::Microsecond),
map(tag("ns"), |_| Unit::Nanosecond),
map(char('h'), |_| Unit::Hour),
map(char('m'), |_| Unit::Minute),
map(char('s'), |_| Unit::Second),
))(i)
}
fn to_duration(num: f64, unit: Unit) -> Option<Duration> {
let nanos = num * unit.nanos() as f64;
if nanos.is_nan() {
return None;
}
if nanos.abs() > i64::MAX as f64 {
return Some(if nanos > 0.0 {
Duration::MAX
} else {
Duration::MIN
});
}
Some(Duration::nanoseconds(nanos as i64))
}
pub fn format_duration_seconds(d: &Duration) -> String {
let secs = d.num_seconds();
let nanos = d.subsec_nanos();
let sign = if secs < 0 || nanos < 0 { "-" } else { "" };
let abs_secs = secs.unsigned_abs();
let abs_nanos = nanos.unsigned_abs();
if abs_nanos == 0 {
format!("{sign}{abs_secs}s")
} else {
let frac = format!("{abs_nanos:09}");
format!("{sign}{abs_secs}.{}s", frac.trim_end_matches('0'))
}
}
#[cfg(test)]
mod tests {
use crate::duration::{format_duration_seconds, parse_duration};
use chrono::Duration;
fn assert_duration(input: &str, expected: Duration) {
let (_, duration) = parse_duration(input).unwrap();
assert_eq!(duration, expected, "{input}");
}
fn assert_print_duration_seconds(input: Duration, expected: &str) {
let actual = format_duration_seconds(&input);
assert_eq!(actual, expected, "{input}");
}
macro_rules! assert_durations {
($($str:expr => $duration:expr),*$(,)?) => {
#[test]
fn test_durations() {
$(
assert_duration($str, $duration);
)*
}
};
}
macro_rules! assert_duration_format_seconds {
($($duration:expr => $str:expr),*$(,)?) => {
#[test]
fn test_format_duration_seconds() {
$(
assert_print_duration_seconds($duration, $str);
)*
}
};
}
assert_durations! {
"1s" => Duration::seconds(1),
"-1s" => Duration::seconds(-1),
"1.1s" => Duration::seconds(1) + Duration::milliseconds(100),
"1.5m" => Duration::minutes(1) + Duration::seconds(30),
"1m1s" => Duration::minutes(1) + Duration::seconds(1),
"1h1m1s" => Duration::hours(1) + Duration::minutes(1) + Duration::seconds(1),
"1ms" => Duration::milliseconds(1),
"1us" => Duration::microseconds(1),
"1ns" => Duration::nanoseconds(1),
"1.1ns" => Duration::nanoseconds(1),
"1.123us" => Duration::microseconds(1) + Duration::nanoseconds(123),
"0s" => Duration::zero(),
"0h0m0s" => Duration::zero(),
"0h0m1s" => Duration::seconds(1),
"0" => Duration::zero(),
"-0" => Duration::zero(),
"9223372036854775807ns" => Duration::nanoseconds(i64::MAX),
"-9223372036854775807ns" => Duration::nanoseconds(-i64::MAX),
}
assert_duration_format_seconds! {
Duration::zero() => "0s",
Duration::seconds(1_000_000) => "1000000s",
Duration::seconds(1) + Duration::milliseconds(500) => "1.5s",
Duration::seconds(-1) => "-1s",
Duration::nanoseconds(1) => "0.000000001s",
Duration::seconds(-1) - Duration::milliseconds(500) => "-1.5s",
Duration::milliseconds(-500) => "-0.5s",
Duration::nanoseconds(i64::MAX) => "9223372036.854775807s",
Duration::nanoseconds(i64::MIN) => "-9223372036.854775808s",
Duration::seconds(10_000_000_000_000) + Duration::milliseconds(500)
=> "10000000000000.5s",
}
}