use std::collections::HashMap;
use std::ops::Add;
use std::time::Duration;
use lazy_static::lazy_static;
lazy_static! {
static ref UNIT_MAP: HashMap<&'static str, u64> = {
let mut m = HashMap::new();
m.insert("ns", 1);
m.insert("us", 1_000);
m.insert("µs", 1_000);
m.insert("μs", 1_000);
m.insert("ms", 1_000_000);
m.insert("s", 1_000_000_000);
m.insert("m", 60_000_000_000);
m.insert("h", 3_600_000_000_000);
m
};
}
pub fn parse_duration_from_golang_string(s: &str) -> Result<Duration, String> {
let orig = s;
let mut d: u64 = 0;
let mut s = s;
if !s.is_empty() {
let c = s.chars().next().unwrap();
if c == '-' || c == '+' {
if c == '-' {
return Err(format!("invalid negative duration {}", quote(orig)));
}
s = &s[1..];
}
}
if s == "0" {
return Ok(Duration::from_nanos(0));
}
if s.is_empty() {
return Err(format!("invalid duration {}", quote(orig)));
}
while !s.is_empty() {
let mut v: u64 = 0;
let mut f: u64 = 0;
let mut scale: f64 = 1.0;
if !s.starts_with(|c: char| c.is_ascii_digit() || c == '.') {
return Err(format!("invalid duration {}", quote(orig)));
}
let pl = s.len();
let (v_temp, s_temp) = leading_int(s)?;
v = v_temp;
s = s_temp;
let pre = pl != s.len();
let mut post = false;
if s.starts_with('.') {
s = &s[1..];
let pl = s.len();
let (f_temp, scale_temp, s_temp) = leading_fraction(s);
f = f_temp;
scale = scale_temp;
s = s_temp;
post = pl != s.len();
}
if !pre && !post {
return Err(format!("invalid duration {}", quote(orig)));
}
let i = s
.find(|c: char| c == '.' || c.is_ascii_digit())
.unwrap_or(s.len());
if i == 0 {
return Err(format!("missing unit in duration {}", quote(orig)));
}
let u = &s[..i];
s = &s[i..];
let unit = UNIT_MAP
.get(u)
.ok_or_else(|| format!("unknown unit {} in duration {}", quote(u), quote(orig)))?;
if v > u64::MAX / unit {
return Err(format!("invalid duration {}", quote(orig)));
}
v *= unit;
if f > 0 {
let vf = f as f64 * (*unit as f64 / scale);
if vf > u64::MAX as f64 {
return Err(format!("invalid duration {}", quote(orig)));
}
v += vf as u64
}
d = d
.checked_add(v)
.ok_or_else(|| format!("invalid duration {}", quote(orig)))?;
}
Ok(Duration::from_nanos(d))
}
fn leading_int(s: &str) -> Result<(u64, &str), String> {
let mut x: u64 = 0;
let mut i = 0;
for c in s.chars() {
if !c.is_ascii_digit() {
break;
}
if x > u64::MAX / 10 {
return Err("bad [0-9]*".to_string());
}
x = x * 10 + (c as u64 - '0' as u64);
i += 1;
}
Ok((x, &s[i..]))
}
fn leading_fraction(s: &str) -> (u64, f64, &str) {
let mut x: u64 = 0;
let mut scale: f64 = 1.0;
let mut overflow = false;
let mut i = 0;
for c in s.chars() {
if !c.is_ascii_digit() {
break;
}
if overflow {
i += 1;
continue;
}
if x > (u64::MAX - 1) / 10 {
overflow = true;
i += 1;
continue;
}
let y = x * 10 + (c as u64 - '0' as u64);
x = y;
scale *= 10.0;
i += 1;
}
(x, scale, &s[i..])
}
fn quote(s: &str) -> String {
format!("\"{s}\"")
}
pub fn duration_to_golang_string(duration: &Duration) -> String {
let mut buf = [0u8; 32];
let mut w = buf.len();
let mut u = duration.as_nanos() as u64;
if u < 1_000_000_000 {
w -= 1;
buf[w] = b's';
w -= 1;
let prec;
if u == 0 {
buf[w] = b'0';
return String::from_utf8_lossy(&buf[w..]).to_string();
} else if u < 1_000 {
prec = 0;
buf[w] = b'n';
} else if u < 1_000_000 {
prec = 3;
buf[w] = 0xB5; w -= 1;
buf[w] = 0xC2; } else {
prec = 6;
buf[w] = b'm';
}
(w, u) = fmt_frac(&mut buf[..w], u, prec);
w = fmt_int(&mut buf[..w], u);
} else {
w -= 1;
buf[w] = b's';
(w, u) = fmt_frac(&mut buf[..w], u, 9);
w = fmt_int(&mut buf[..w], u % 60);
u /= 60;
if u > 0 {
w -= 1;
buf[w] = b'm';
w = fmt_int(&mut buf[..w], u % 60);
u /= 60;
if u > 0 {
w -= 1;
buf[w] = b'h';
w = fmt_int(&mut buf[..w], u);
}
}
}
String::from_utf8_lossy(&buf[w..]).to_string()
}
fn fmt_frac(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 fmt_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 std::ops::Sub;
use std::time::Duration;
use crate::helpers::durations::{duration_to_golang_string, parse_duration_from_golang_string};
#[test]
fn test_parse_duration() {
let parse_duration_tests = vec![
("0", Duration::from_secs(0)),
("5s", Duration::from_secs(5)),
("30s", Duration::from_secs(30)),
("1478s", Duration::from_secs(1478)),
("+5s", Duration::from_secs(5)),
("+0", Duration::from_secs(0)),
("5.0s", Duration::from_secs(5)),
("5.6s", Duration::from_millis(5600)),
("5.s", Duration::from_secs(5)),
(".5s", Duration::from_millis(500)),
("1.0s", Duration::from_secs(1)),
("1.00s", Duration::from_secs(1)),
("1.004s", Duration::from_millis(1004)),
("1.0040s", Duration::from_millis(1004)),
("100.00100s", Duration::from_millis(100001)),
("10ns", Duration::from_nanos(10)),
("11us", Duration::from_micros(11)),
("12µs", Duration::from_micros(12)),
("12μs", Duration::from_micros(12)),
("13ms", Duration::from_millis(13)),
("14s", Duration::from_secs(14)),
("15m", Duration::from_secs(15 * 60)),
("16h", Duration::from_secs(16 * 3600)),
("3h30m", Duration::from_secs(3 * 3600 + 30 * 60)),
("10.5s4m", Duration::from_millis(4 * 60 * 1000 + 10500)),
(
"1h2m3s4ms5us6ns",
Duration::from_secs(60 * 60)
+ Duration::from_secs(2 * 60)
+ Duration::from_secs(3)
+ Duration::from_millis(4)
+ Duration::from_micros(5)
+ Duration::from_nanos(6),
),
(
"39h9m14.425s",
Duration::from_secs(39 * 60 * 60)
+ Duration::from_secs(9 * 60)
+ Duration::from_millis(14425),
),
("52763797000ns", Duration::from_nanos(52_763_797_000)),
("0.3333333333333333333h", Duration::from_secs(20 * 60)),
(
"9007199254740993ns",
Duration::from_nanos(9_007_199_254_740_993),
),
(
"9223372036854775807ns",
Duration::from_nanos(9_223_372_036_854_775_807),
),
(
"9223372036854775.807us",
Duration::from_nanos(9_223_372_036_854_775_807),
),
(
"9223372036s854ms775us807ns",
Duration::from_nanos(9_223_372_036_854_775_807),
),
("0.100000000000000000000h", Duration::from_secs(6 * 60)),
("0.830103483285477580700h", Duration::new(2988, 372_539_827)),
];
for (input, expected) in parse_duration_tests {
match parse_duration_from_golang_string(input) {
Ok(duration) => {
assert_eq!(
duration, expected,
"ParseDuration({input}) = {duration:?}, want {expected:?}"
);
}
Err(e) => {
panic!("ParseDuration({input}) returned error: {e}");
}
}
}
}
#[test]
fn test_duration_to_golang_string() {
let tests = vec![
(Duration::from_secs(0), "0s"),
(Duration::from_nanos(1), "1ns"),
(Duration::from_nanos(1100), "1.1µs"),
(Duration::from_micros(2200), "2.2ms"),
(Duration::from_millis(3300), "3.3s"),
(Duration::from_secs(245), "4m5s"),
(Duration::from_millis(245001), "4m5.001s"),
(Duration::from_millis(18367001), "5h6m7.001s"),
(Duration::from_nanos(480000000001), "8m0.000000001s"),
(
Duration::from_nanos(1 << 63).sub(Duration::from_nanos(1)),
"2562047h47m16.854775807s",
),
];
for (input, expected) in tests {
let actual = duration_to_golang_string(&input);
assert_eq!(
actual, expected,
"DurationToString({input:?}) = {actual:?}, want {expected:?}"
);
}
}
}