diff --git a/src/builtins/test.rs b/src/builtins/test.rs index 9fafb53..d3a473d 100644 --- a/src/builtins/test.rs +++ b/src/builtins/test.rs @@ -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], 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) } diff --git a/src/builtins/time.rs b/src/builtins/time.rs index b45471e..f7dee39 100644 --- a/src/builtins/time.rs +++ b/src/builtins/time.rs @@ -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], args: &[Value], strict: bool) -> Re safe_timestamp_nanos(span, strict, Utc::now().timestamp_nanos_opt()) } +fn parse_duration_ns( + span: &Span, + params: &[Ref], + args: &[Value], + strict: bool, +) -> Result { + 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], args: &[Value], strict: bool) -> Result { let name = "time.parse_ns"; ensure_args_count(span, name, params, args, 2)?; diff --git a/src/builtins/time/compat.rs b/src/builtins/time/compat.rs new file mode 100644 index 0000000..aa25b14 --- /dev/null +++ b/src/builtins/time/compat.rs @@ -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 { + // 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); + } + } +} diff --git a/tests/interpreter/cases/builtins/time/parse_duration_ns.yaml b/tests/interpreter/cases/builtins/time/parse_duration_ns.yaml new file mode 100644 index 0000000..c89f715 --- /dev/null +++ b/tests/interpreter/cases/builtins/time/parse_duration_ns.yaml @@ -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'