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;
const SECOND: u64 = 1_000_000_000;
const MILLISECOND: u64 = 1_000_000;
const MICROSECOND: u64 = 1_000;
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, duration) = many1(parse_number_unit)(i)
.map(|(i, d)| (i, d.iter().fold(Duration::zero(), |acc, next| acc + *next)))?;
Ok((i, duration * if neg.is_some() { -1 } else { 1 }))
}
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)?;
let duration = to_duration(num, unit);
Ok((i, duration))
}
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) -> Duration {
Duration::nanoseconds((num * unit.nanos() as f64).trunc() as i64)
}
pub fn format_duration(d: &Duration) -> String {
let buf = &mut [0u8; 32];
let mut w = buf.len();
let mut neg = false;
let mut u = d
.num_nanoseconds()
.map(|n| {
if n < 0 {
neg = true;
}
n as u64
})
.unwrap_or_else(|| {
let s = d.num_seconds();
if s < 0 {
neg = true;
}
s as u64 * SECOND
});
if u < SECOND {
let mut _prec = 0;
w -= 1;
buf[w] = b's';
w -= 1;
if u == 0 {
return "0s".to_string();
} else if u < MICROSECOND {
_prec = 0;
buf[w] = b'n';
} else if u < MILLISECOND {
_prec = 3;
buf[w] = 0xB5;
w -= 1;
buf[w] = 0xC2;
} else {
_prec = 6;
buf[w] = b'm';
}
(w, u) = format_float(&mut buf[..w], u, _prec);
w = format_int(&mut buf[..w], u);
} else {
w -= 1;
buf[w] = b's';
(w, u) = format_float(&mut buf[..w], u, 9);
w = format_int(&mut buf[..w], u % 60);
u /= 60;
if u > 0 {
w -= 1;
buf[w] = b'm';
w = format_int(&mut buf[..w], u % 60);
u /= 60;
if u > 0 {
w -= 1;
buf[w] = b'h';
w = format_int(&mut buf[..w], u);
}
}
}
if neg {
w -= 1;
buf[w] = b'-';
}
String::from_utf8_lossy(&buf[w..]).into_owned()
}
fn format_float(buf: &mut [u8], mut v: u64, prec: usize) -> (usize, u64) {
let mut w = buf.len();
let mut print = false;
for _ in 0..prec {
let digit = v % 10;
print = print || digit != 0;
if print {
w -= 1;
buf[w] = digit as u8 + b'0';
}
v /= 10;
}
if print {
w -= 1;
buf[w] = b'.';
}
(w, v)
}
fn format_int(buf: &mut [u8], mut v: u64) -> usize {
let mut w = buf.len();
if v == 0 {
w -= 1;
buf[w] = b'0';
} else {
while v > 0 {
w -= 1;
buf[w] = (v % 10) as u8 + b'0';
v /= 10;
}
}
w
}
#[cfg(test)]
mod tests {
use crate::duration::{format_duration, 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(input: Duration, expected: &str) {
let actual = format_duration(&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 {
($($duration:expr => $str:expr),*$(,)?) => {
#[test]
fn test_format_durations() {
$(
assert_print_duration($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(),
}
assert_duration_format! {
Duration::zero() => "0s",
Duration::nanoseconds(1) => "1ns",
Duration::nanoseconds(1100) => "1.1µs",
Duration::microseconds(2200) => "2.2ms",
Duration::milliseconds(3300) => "3.3s",
Duration::minutes(4) + Duration::seconds(5) => "4m5s",
Duration::minutes(4) + Duration::milliseconds(5001) => "4m5.001s",
Duration::hours(5) + Duration::minutes(6) + Duration::milliseconds(7001) => "5h6m7.001s",
Duration::minutes(8) + Duration::nanoseconds(1) => "8m0.000000001s",
Duration::nanoseconds(i64::MAX) => "2562047h47m16.854775807s",
Duration::nanoseconds(i64::MIN) => "-2562047h47m16.854775808s",
}
}