mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-28 19:47:45 +00:00
Merge pull request #7648 from drinkcat/parse_time-ebd
timeout: Use common parser to parse time duration
This commit is contained in:
commit
88cf66174f
3 changed files with 155 additions and 25 deletions
|
@ -294,8 +294,14 @@ fn construct_extended_big_decimal<'a>(
|
||||||
scale: u64,
|
scale: u64,
|
||||||
exponent: BigInt,
|
exponent: BigInt,
|
||||||
) -> Result<ExtendedBigDecimal, ExtendedParserError<'a, ExtendedBigDecimal>> {
|
) -> Result<ExtendedBigDecimal, ExtendedParserError<'a, ExtendedBigDecimal>> {
|
||||||
if digits == BigUint::zero() && negative {
|
if digits == BigUint::zero() {
|
||||||
return Ok(ExtendedBigDecimal::MinusZero);
|
// Return return 0 if the digits are zero. In particular, we do not ever
|
||||||
|
// return Overflow/Underflow errors in that case.
|
||||||
|
return Ok(if negative {
|
||||||
|
ExtendedBigDecimal::MinusZero
|
||||||
|
} else {
|
||||||
|
ExtendedBigDecimal::zero()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let sign = if negative { Sign::Minus } else { Sign::Plus };
|
let sign = if negative { Sign::Minus } else { Sign::Plus };
|
||||||
|
@ -712,6 +718,24 @@ mod tests {
|
||||||
ExtendedBigDecimal::MinusZero
|
ExtendedBigDecimal::MinusZero
|
||||||
))
|
))
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// But no Overflow/Underflow if the digits are 0.
|
||||||
|
assert_eq!(
|
||||||
|
ExtendedBigDecimal::extended_parse(&format!("0e{}", i64::MAX as u64 + 2)),
|
||||||
|
Ok(ExtendedBigDecimal::zero()),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ExtendedBigDecimal::extended_parse(&format!("-0.0e{}", i64::MAX as u64 + 3)),
|
||||||
|
Ok(ExtendedBigDecimal::MinusZero)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ExtendedBigDecimal::extended_parse(&format!("0.0000e{}", i64::MIN)),
|
||||||
|
Ok(ExtendedBigDecimal::zero()),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ExtendedBigDecimal::extended_parse(&format!("-0e{}", i64::MIN + 2)),
|
||||||
|
Ok(ExtendedBigDecimal::MinusZero)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -3,15 +3,22 @@
|
||||||
// For the full copyright and license information, please view the LICENSE
|
// For the full copyright and license information, please view the LICENSE
|
||||||
// file that was distributed with this source code.
|
// file that was distributed with this source code.
|
||||||
|
|
||||||
// spell-checker:ignore (vars) NANOS numstr
|
// spell-checker:ignore (vars) NANOS numstr infinityh INFD nans nanh bigdecimal extendedbigdecimal
|
||||||
//! Parsing a duration from a string.
|
//! Parsing a duration from a string.
|
||||||
//!
|
//!
|
||||||
//! Use the [`from_str`] function to parse a [`Duration`] from a string.
|
//! Use the [`from_str`] function to parse a [`Duration`] from a string.
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
display::Quotable,
|
||||||
|
extendedbigdecimal::ExtendedBigDecimal,
|
||||||
|
parser::num_parser::{ExtendedParser, ExtendedParserError},
|
||||||
|
};
|
||||||
|
use bigdecimal::BigDecimal;
|
||||||
|
use num_traits::Signed;
|
||||||
|
use num_traits::ToPrimitive;
|
||||||
|
use num_traits::Zero;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::display::Quotable;
|
|
||||||
|
|
||||||
/// Parse a duration from a string.
|
/// Parse a duration from a string.
|
||||||
///
|
///
|
||||||
/// The string may contain only a number, like "123" or "4.5", or it
|
/// The string may contain only a number, like "123" or "4.5", or it
|
||||||
|
@ -26,9 +33,10 @@ use crate::display::Quotable;
|
||||||
/// * "h" for hours,
|
/// * "h" for hours,
|
||||||
/// * "d" for days.
|
/// * "d" for days.
|
||||||
///
|
///
|
||||||
/// This function uses [`Duration::saturating_mul`] to compute the
|
/// This function does not overflow if large values are provided. If
|
||||||
/// number of seconds, so it does not overflow. If overflow would have
|
/// overflow would have occurred, [`Duration::MAX`] is returned instead.
|
||||||
/// occurred, [`Duration::MAX`] is returned instead.
|
///
|
||||||
|
/// If the value is smaller than 1 nanosecond, we return 1 nanosecond.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
|
@ -45,6 +53,10 @@ use crate::display::Quotable;
|
||||||
/// assert_eq!(from_str("2d"), Ok(Duration::from_secs(60 * 60 * 24 * 2)));
|
/// assert_eq!(from_str("2d"), Ok(Duration::from_secs(60 * 60 * 24 * 2)));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn from_str(string: &str) -> Result<Duration, String> {
|
pub fn from_str(string: &str) -> Result<Duration, String> {
|
||||||
|
// TODO: Switch to Duration::NANOSECOND if that ever becomes stable
|
||||||
|
// https://github.com/rust-lang/rust/issues/57391
|
||||||
|
const NANOSECOND_DURATION: Duration = Duration::from_nanos(1);
|
||||||
|
|
||||||
let len = string.len();
|
let len = string.len();
|
||||||
if len == 0 {
|
if len == 0 {
|
||||||
return Err("empty string".to_owned());
|
return Err("empty string".to_owned());
|
||||||
|
@ -58,27 +70,43 @@ pub fn from_str(string: &str) -> Result<Duration, String> {
|
||||||
'h' => (slice, 60 * 60),
|
'h' => (slice, 60 * 60),
|
||||||
'd' => (slice, 60 * 60 * 24),
|
'd' => (slice, 60 * 60 * 24),
|
||||||
val if !val.is_alphabetic() => (string, 1),
|
val if !val.is_alphabetic() => (string, 1),
|
||||||
_ => {
|
_ => match string.to_ascii_lowercase().as_str() {
|
||||||
if string == "inf" || string == "infinity" {
|
"inf" | "infinity" => ("inf", 1),
|
||||||
("inf", 1)
|
_ => return Err(format!("invalid time interval {}", string.quote())),
|
||||||
} else {
|
},
|
||||||
return Err(format!("invalid time interval {}", string.quote()));
|
};
|
||||||
}
|
let num = match ExtendedBigDecimal::extended_parse(numstr) {
|
||||||
}
|
Ok(ebd) | Err(ExtendedParserError::Overflow(ebd)) => ebd,
|
||||||
|
Err(ExtendedParserError::Underflow(_)) => return Ok(NANOSECOND_DURATION),
|
||||||
|
_ => return Err(format!("invalid time interval {}", string.quote())),
|
||||||
};
|
};
|
||||||
let num = numstr
|
|
||||||
.parse::<f64>()
|
|
||||||
.map_err(|e| format!("invalid time interval {}: {}", string.quote(), e))?;
|
|
||||||
|
|
||||||
if num < 0. {
|
// Allow non-negative durations (-0 is fine), and infinity.
|
||||||
return Err(format!("invalid time interval {}", string.quote()));
|
let num = match num {
|
||||||
|
ExtendedBigDecimal::BigDecimal(bd) if !bd.is_negative() => bd,
|
||||||
|
ExtendedBigDecimal::MinusZero => 0.into(),
|
||||||
|
ExtendedBigDecimal::Infinity => return Ok(Duration::MAX),
|
||||||
|
_ => return Err(format!("invalid time interval {}", string.quote())),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pre-multiply times to avoid precision loss
|
||||||
|
let num: BigDecimal = num * times;
|
||||||
|
|
||||||
|
// Transform to nanoseconds (9 digits after decimal point)
|
||||||
|
let (nanos_bi, _) = num.with_scale(9).into_bigint_and_scale();
|
||||||
|
|
||||||
|
// If the value is smaller than a nanosecond, just return that.
|
||||||
|
if nanos_bi.is_zero() && !num.is_zero() {
|
||||||
|
return Ok(NANOSECOND_DURATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
const NANOS_PER_SEC: u32 = 1_000_000_000;
|
const NANOS_PER_SEC: u32 = 1_000_000_000;
|
||||||
let whole_secs = num.trunc();
|
let whole_secs: u64 = match (&nanos_bi / NANOS_PER_SEC).try_into() {
|
||||||
let nanos = (num.fract() * (NANOS_PER_SEC as f64)).trunc();
|
Ok(whole_secs) => whole_secs,
|
||||||
let duration = Duration::new(whole_secs as u64, nanos as u32);
|
Err(_) => return Ok(Duration::MAX),
|
||||||
Ok(duration.saturating_mul(times))
|
};
|
||||||
|
let nanos: u32 = (&nanos_bi % NANOS_PER_SEC).to_u32().unwrap();
|
||||||
|
Ok(Duration::new(whole_secs, nanos))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -98,8 +126,49 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_saturating_mul() {
|
fn test_overflow() {
|
||||||
|
// u64 seconds overflow (in Duration)
|
||||||
assert_eq!(from_str("9223372036854775808d"), Ok(Duration::MAX));
|
assert_eq!(from_str("9223372036854775808d"), Ok(Duration::MAX));
|
||||||
|
// ExtendedBigDecimal overflow
|
||||||
|
assert_eq!(from_str("1e92233720368547758080"), Ok(Duration::MAX));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_underflow() {
|
||||||
|
// TODO: Switch to Duration::NANOSECOND if that ever becomes stable
|
||||||
|
// https://github.com/rust-lang/rust/issues/57391
|
||||||
|
const NANOSECOND_DURATION: Duration = Duration::from_nanos(1);
|
||||||
|
|
||||||
|
// ExtendedBigDecimal underflow
|
||||||
|
assert_eq!(from_str("1e-92233720368547758080"), Ok(NANOSECOND_DURATION));
|
||||||
|
// nanoseconds underflow (in Duration)
|
||||||
|
assert_eq!(from_str("0.0000000001"), Ok(NANOSECOND_DURATION));
|
||||||
|
assert_eq!(from_str("1e-10"), Ok(NANOSECOND_DURATION));
|
||||||
|
assert_eq!(from_str("9e-10"), Ok(NANOSECOND_DURATION));
|
||||||
|
assert_eq!(from_str("1e-9"), Ok(NANOSECOND_DURATION));
|
||||||
|
assert_eq!(from_str("1.9e-9"), Ok(NANOSECOND_DURATION));
|
||||||
|
assert_eq!(from_str("2e-9"), Ok(Duration::from_nanos(2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_zero() {
|
||||||
|
assert_eq!(from_str("0e-9"), Ok(Duration::ZERO));
|
||||||
|
assert_eq!(from_str("0e-100"), Ok(Duration::ZERO));
|
||||||
|
assert_eq!(from_str("0e-92233720368547758080"), Ok(Duration::ZERO));
|
||||||
|
assert_eq!(from_str("0.000000000000000000000"), Ok(Duration::ZERO));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hex_float() {
|
||||||
|
assert_eq!(
|
||||||
|
from_str("0x1.1p-1"),
|
||||||
|
Ok(Duration::from_secs_f64(0.53125f64))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
from_str("0x1.1p-1d"),
|
||||||
|
Ok(Duration::from_secs_f64(0.53125f64 * 3600.0 * 24.0))
|
||||||
|
);
|
||||||
|
assert_eq!(from_str("0xfh"), Ok(Duration::from_secs(15 * 3600)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -127,6 +196,24 @@ mod tests {
|
||||||
assert!(from_str("-1").is_err());
|
assert!(from_str("-1").is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_infinity() {
|
||||||
|
assert_eq!(from_str("inf"), Ok(Duration::MAX));
|
||||||
|
assert_eq!(from_str("infinity"), Ok(Duration::MAX));
|
||||||
|
assert_eq!(from_str("infinityh"), Ok(Duration::MAX));
|
||||||
|
assert_eq!(from_str("INF"), Ok(Duration::MAX));
|
||||||
|
assert_eq!(from_str("INFs"), Ok(Duration::MAX));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_nan() {
|
||||||
|
assert!(from_str("nan").is_err());
|
||||||
|
assert!(from_str("nans").is_err());
|
||||||
|
assert!(from_str("-nanh").is_err());
|
||||||
|
assert!(from_str("NAN").is_err());
|
||||||
|
assert!(from_str("-NAN").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
/// Test that capital letters are not allowed in suffixes.
|
/// Test that capital letters are not allowed in suffixes.
|
||||||
#[test]
|
#[test]
|
||||||
fn test_no_capital_letters() {
|
fn test_no_capital_letters() {
|
||||||
|
@ -134,5 +221,6 @@ mod tests {
|
||||||
assert!(from_str("1M").is_err());
|
assert!(from_str("1M").is_err());
|
||||||
assert!(from_str("1H").is_err());
|
assert!(from_str("1H").is_err());
|
||||||
assert!(from_str("1D").is_err());
|
assert!(from_str("1D").is_err());
|
||||||
|
assert!(from_str("INFD").is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -125,6 +125,24 @@ fn test_dont_overflow() {
|
||||||
.no_output();
|
.no_output();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dont_underflow() {
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&[".0000000001", "sleep", "1"])
|
||||||
|
.fails_with_code(124)
|
||||||
|
.no_output();
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&["1e-100", "sleep", "1"])
|
||||||
|
.fails_with_code(124)
|
||||||
|
.no_output();
|
||||||
|
// Unlike GNU coreutils, we underflow to 1ns for very short timeouts.
|
||||||
|
// https://debbugs.gnu.org/cgi/bugreport.cgi?bug=77535
|
||||||
|
new_ucmd!()
|
||||||
|
.args(&["1e-18172487393827593258", "sleep", "1"])
|
||||||
|
.fails_with_code(124)
|
||||||
|
.no_output();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_negative_interval() {
|
fn test_negative_interval() {
|
||||||
new_ucmd!()
|
new_ucmd!()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue