mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
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:
+4
-14
@@ -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, ¶ms[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)
|
||||
}
|
||||
|
||||
@@ -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, ¶ms[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)?;
|
||||
|
||||
@@ -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'
|
||||
Reference in New Issue
Block a user