Implement builtin time.parse_duration_ns method (#100)

* Implement builtin `time.parse_duration_ns` method

* Parse durations properly in `test.sleep` method
This commit is contained in:
Burak
2024-01-08 10:21:35 +00:00
committed by GitHub
parent c0fa1c1a42
commit 49bd657a41
4 changed files with 423 additions and 14 deletions
+4 -14
View File
@@ -3,15 +3,15 @@
use crate::ast::{Expr, Ref};
use crate::builtins;
use crate::builtins::time;
use crate::builtins::utils::{ensure_args_count, ensure_string};
use crate::lexer::Span;
use crate::value::Value;
use std::collections::HashMap;
use std::thread;
use std::time::Duration;
use anyhow::{bail, Ok, Result};
use anyhow::{Ok, Result};
pub fn register(m: &mut HashMap<&'static str, builtins::BuiltinFcn>) {
m.insert("test.sleep", (sleep, 1));
@@ -22,19 +22,9 @@ fn sleep(span: &Span, params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Re
ensure_args_count(span, name, params, args, 1)?;
let val = ensure_string(name, &params[0], &args[0])?;
let dur = time::compat::parse_duration(val.as_ref())?;
let duration = if let Some(millis) = val.strip_suffix("ms").and_then(|v| v.parse().ok()) {
Duration::from_millis(millis)
} else if let Some(secs) = val.strip_suffix("s").and_then(|v| v.parse().ok()) {
Duration::from_secs(secs)
} else {
bail!(params[0].span().error(
format!("`{name}` expects a simple duration ends with `ms` or `s`. Got {val} instead")
.as_str()
))
};
thread::sleep(duration);
thread::sleep(dur.to_std()?);
Ok(Value::Null)
}
+16
View File
@@ -17,6 +17,7 @@ use chrono::{
};
use chrono_tz::Tz;
pub(in crate::builtins) mod compat;
mod diff;
pub fn register(m: &mut HashMap<&'static str, builtins::BuiltinFcn>) {
@@ -26,6 +27,7 @@ pub fn register(m: &mut HashMap<&'static str, builtins::BuiltinFcn>) {
m.insert("time.diff", (diff, 2));
m.insert("time.format", (format, 1));
m.insert("time.now_ns", (now_ns, 0));
m.insert("time.parse_duration_ns", (parse_duration_ns, 1));
m.insert("time.parse_ns", (parse_ns, 2));
m.insert("time.parse_rfc3339_ns", (parse_rfc3339_ns, 1));
m.insert("time.weekday", (weekday, 1));
@@ -136,6 +138,20 @@ fn now_ns(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) -> Re
safe_timestamp_nanos(span, strict, Utc::now().timestamp_nanos_opt())
}
fn parse_duration_ns(
span: &Span,
params: &[Ref<Expr>],
args: &[Value],
strict: bool,
) -> Result<Value> {
let name = "time.parse_duration_ns";
ensure_args_count(span, name, params, args, 1)?;
let value = ensure_string(name, &params[0], &args[0])?;
let dur = compat::parse_duration(value.as_ref())?;
safe_timestamp_nanos(span, strict, dur.num_nanoseconds())
}
fn parse_ns(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) -> Result<Value> {
let name = "time.parse_ns";
ensure_args_count(span, name, params, args, 2)?;
+356
View File
@@ -0,0 +1,356 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//
// This module contains methods for compatibility with Go's `time` package.
//
// Copyright (c) 2009 The Go Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
use std::error::Error;
use std::fmt;
use chrono::Duration;
const NANOSECOND: u64 = 1;
const MICROSECOND: u64 = 1000 * NANOSECOND;
const MILLISECOND: u64 = 1000 * MICROSECOND;
const SECOND: u64 = 1000 * MILLISECOND;
const MINUTE: u64 = 60 * SECOND;
const HOUR: u64 = 60 * MINUTE;
#[derive(Debug)]
pub enum ParseDurationError {
InvalidDuration(String),
UnknownUnit(String),
Overflow,
}
impl fmt::Display for ParseDurationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParseDurationError::InvalidDuration(dur) => {
write!(f, "invalid duration: {dur}")
}
ParseDurationError::UnknownUnit(unit) => {
write!(f, "unknown unit: {unit}")
}
ParseDurationError::Overflow => {
write!(f, "overflow")
}
}
}
}
impl Error for ParseDurationError {}
// Parses a duration string in the form of `10h12m45s`.
//
// Adapted from Go's `time.ParseDuration`:
// https://github.com/golang/go/blob/8db131082d08e497fd8e9383d0ff7715e1bef478/src/time/format.go#L1584-L1686
pub fn parse_duration(mut s: &str) -> Result<Duration, ParseDurationError> {
// Input is in the format of `[-+]?([0-9]*(\.[0-9]*)?[a-z]+)+`
let orig = s;
// Consume [-+]?
let neg = if s.starts_with('-') {
s = &s[1..];
true
} else if s.starts_with('+') {
s = &s[1..];
false
} else {
false
};
// Special case: if all that is left is "0", this is zero.
if s == "0" {
return Ok(Duration::zero());
}
if s.is_empty() {
return Err(ParseDurationError::InvalidDuration(orig.to_string()));
}
let mut dur = 0u64;
while !s.is_empty() {
// The next character must be [0-9.]
if !(s.starts_with('.') || s.starts_with(|c: char| c.is_ascii_digit())) {
return Err(ParseDurationError::InvalidDuration(orig.to_string()));
}
let previous_len = s.len();
// v is the integers before the decimal point
// Consume [0-9]*
let (mut v, rem) = leading_int(s)?;
s = rem;
// whether we consumed anything before a period
let pre = previous_len != s.len();
// Consume (\.[0-9]*)?
let mut post = false;
let mut f = 0;
let mut scale = 0.0;
if !s.is_empty() && s.starts_with('.') {
s = &s[1..];
let previous_len = s.len();
(f, scale, s) = leading_fraction(s);
post = previous_len != s.len();
}
if !pre && !post {
// no digits (e.g. ".s" or "-.s")
return Err(ParseDurationError::InvalidDuration(orig.to_string()));
}
// Consume unit.
let mut idx = 0;
for (i, c) in s.char_indices() {
if c == '.' || c.is_ascii_digit() {
break;
}
idx = i;
}
let unit = match &s[..idx + 1] {
"ns" => NANOSECOND,
"us" => MICROSECOND,
"µs" => MICROSECOND, // U+00B5 = micro symbol
"μs" => MICROSECOND, // U+03BC = Greek letter mu
"ms" => MILLISECOND,
"s" => SECOND,
"m" => MINUTE,
"h" => HOUR,
unkonwn => return Err(ParseDurationError::UnknownUnit(unkonwn.to_string())),
};
s = &s[idx + 1..];
if v > ((1 << 63) / unit) {
// overflow
return Err(ParseDurationError::InvalidDuration(orig.to_string()));
}
v *= unit;
if f > 0 {
// f64 is needed to be nanosecond accurate for fractions of hours.
// v >= 0 && (f*unit/scale) <= 3.6e+12 (ns/h, h is the largest unit)
v += (f as f64 * (unit as f64 / scale)) as u64;
if v > 1 << 63 {
// overflow
return Err(ParseDurationError::InvalidDuration(orig.to_string()));
}
}
dur += v;
if dur > 1 << 63 {
return Err(ParseDurationError::InvalidDuration(orig.to_string()));
}
}
if neg {
let dur = dur as i64;
if dur < 0 {
return Ok(Duration::nanoseconds(dur));
}
return Ok(-Duration::nanoseconds(dur));
}
if dur > i64::MAX as u64 {
return Err(ParseDurationError::InvalidDuration(orig.to_string()));
}
Ok(Duration::nanoseconds(dur as i64))
}
fn leading_int(s: &str) -> Result<(u64, &str), ParseDurationError> {
let mut last_idx = 0;
let mut num: u64 = 0;
for (i, c) in s.char_indices() {
last_idx = i;
let n = match c.to_digit(10) {
Some(n) => n as u64,
None => break,
};
if num > ((1 << 63) / 10) {
// overflow
return Err(ParseDurationError::Overflow);
}
num = num * 10 + n;
if num > 1 << 63 {
// overflow
return Err(ParseDurationError::Overflow);
}
}
Ok((num, &s[last_idx..]))
}
fn leading_fraction(s: &str) -> (u64, f64, &str) {
let mut num: u64 = 0;
let mut scale = 1.0;
let mut overflow = false;
let mut last_idx = 0;
for (i, c) in s.char_indices() {
last_idx = i;
let n = match c.to_digit(10) {
Some(n) => n as u64,
None => break,
};
if overflow {
continue;
}
if num > (i64::MAX as u64 / 10) {
// It's possible for overflow to give a positive number, so take care.
overflow = true;
continue;
}
let y = num * 10 + n;
if y > 1 << 63 {
overflow = true;
continue;
}
num = y;
scale *= 10.0;
}
(num, scale, &s[last_idx..])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_durations() {
// Test cases are copied from Go's `time.ParseDuration` tests:
// https://github.com/golang/go/blob/8db131082d08e497fd8e9383d0ff7715e1bef478/src/time/time_test.go#L891-L951
for (input, expected_dur) in [
// simple
("0", Duration::zero()),
("5s", Duration::seconds(5)),
("30s", Duration::seconds(30)),
("1478s", Duration::seconds(1478)),
// sign
("-5s", -Duration::seconds(5)),
("+5s", Duration::seconds(5)),
("-0", Duration::zero()),
("+0", Duration::zero()),
// decimal
("5.0s", Duration::seconds(5)),
("5.6s", Duration::seconds(5) + Duration::milliseconds(600)),
("5.s", Duration::seconds(5)),
(".5s", Duration::milliseconds(500)),
("1.0s", Duration::seconds(1)),
("1.00s", Duration::seconds(1)),
("1.004s", Duration::seconds(1) + Duration::milliseconds(4)),
("1.0040s", Duration::seconds(1) + Duration::milliseconds(4)),
(
"100.00100s",
Duration::seconds(100) + Duration::milliseconds(1),
),
// different units
("10ns", Duration::nanoseconds(10)),
("11us", Duration::microseconds(11)),
("12µs", Duration::microseconds(12)), // U+00B5
("12μs", Duration::microseconds(12)), // U+03BC
("13ms", Duration::milliseconds(13)),
("14s", Duration::seconds(14)),
("15m", Duration::minutes(15)),
("16h", Duration::hours(16)),
// composite durations
("3h30m", Duration::hours(3) + Duration::minutes(30)),
(
"10.5s4m",
Duration::minutes(4) + Duration::seconds(10) + Duration::milliseconds(500),
),
(
"-2m3.4s",
-(Duration::minutes(2) + Duration::seconds(3) + Duration::milliseconds(400)),
),
(
"1h2m3s4ms5us6ns",
Duration::hours(1)
+ Duration::minutes(2)
+ Duration::seconds(3)
+ Duration::milliseconds(4)
+ Duration::microseconds(5)
+ Duration::nanoseconds(6),
),
(
"39h9m14.425s",
Duration::hours(39)
+ Duration::minutes(9)
+ Duration::seconds(14)
+ Duration::milliseconds(425),
),
// large value
("52763797000ns", Duration::nanoseconds(52763797000)),
// more than 9 digits after decimal point, see https://golang.org/issue/6617
("0.3333333333333333333h", Duration::minutes(20)),
// 9007199254740993 = 1<<53+1 cannot be stored precisely in a float64
("9007199254740993ns", Duration::nanoseconds((1 << 53) + 1)),
// largest duration that can be represented by int64 in nanoseconds
("9223372036854775807ns", Duration::nanoseconds(i64::MAX)),
("9223372036854775.807us", Duration::nanoseconds(i64::MAX)),
(
"9223372036s854ms775us807ns",
Duration::nanoseconds(i64::MAX),
),
("-9223372036854775808ns", Duration::nanoseconds(i64::MIN)),
("-9223372036854775.808us", Duration::nanoseconds(i64::MIN)),
(
"-9223372036s854ms775us808ns",
Duration::nanoseconds(i64::MIN),
),
// largest negative value
("-9223372036854775808ns", Duration::nanoseconds(i64::MIN)),
// largest negative round trip value, see https://golang.org/issue/48629
("-2562047h47m16.854775808s", Duration::nanoseconds(i64::MIN)),
// huge string; issue 15011.
("0.100000000000000000000h", Duration::minutes(6)),
// This value tests the first overflow check in leadingFraction.
(
"0.830103483285477580700h",
Duration::minutes(49) + Duration::seconds(48) + Duration::nanoseconds(372539827),
),
] {
let dur = parse_duration(input).unwrap();
assert_eq!(dur, expected_dur);
}
}
}
@@ -0,0 +1,47 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
cases:
- note: simple
data: {}
modules:
- |
package test
a := time.parse_duration_ns("1h")
b := time.parse_duration_ns("5m")
c := time.parse_duration_ns("45s")
d := time.parse_duration_ns("15ms")
query: data.test
want_result:
a: 3600000000000
b: 300000000000
c: 45000000000
d: 15000000
- note: complex
data: {}
modules:
- |
package test
a := time.parse_duration_ns("1h6m13.3s20ms15µs1ns")
b := time.parse_duration_ns("1.2h20.2ms15µs56ns")
c := time.parse_duration_ns("45.67s123ms")
d := time.parse_duration_ns("15.000001ms")
query: data.test
want_result:
a: 3973320015001
b: 4320020215056
c: 45793000000
d: 15000001
- note: invalid-type
data: {}
modules:
- |
package test
a := time.parse_duration_ns(100)
query: data.test
error: '`time.parse_duration_ns` expects string argument. Got `100` instead'