33// For the full copyright and license information, please view the LICENSE
44// file that was distributed with this source code.
55
6- // spell-checker:ignore (vars) NANOS numstr infinityh INFD nans nanh
6+ // spell-checker:ignore (vars) NANOS numstr infinityh INFD nans nanh bigdecimal extendedbigdecimal
77//! Parsing a duration from a string.
88//!
99//! Use the [`from_str`] function to parse a [`Duration`] from a string.
1010
11+ use crate :: {
12+ display:: Quotable ,
13+ extendedbigdecimal:: ExtendedBigDecimal ,
14+ parser:: num_parser:: { ExtendedParser , ExtendedParserError } ,
15+ } ;
16+ use bigdecimal:: BigDecimal ;
17+ use num_traits:: Signed ;
18+ use num_traits:: ToPrimitive ;
19+ use num_traits:: Zero ;
1120use std:: time:: Duration ;
1221
13- use crate :: display:: Quotable ;
14-
1522/// Parse a duration from a string.
1623///
1724/// The string may contain only a number, like "123" or "4.5", or it
@@ -26,9 +33,10 @@ use crate::display::Quotable;
2633/// * "h" for hours,
2734/// * "d" for days.
2835///
29- /// This function uses [`Duration::saturating_mul`] to compute the
30- /// number of seconds, so it does not overflow. If overflow would have
31- /// occurred, [`Duration::MAX`] is returned instead.
36+ /// This function does not overflow if large values are provided. If
37+ /// overflow would have occurred, [`Duration::MAX`] is returned instead.
38+ ///
39+ /// If the value is smaller than 1 nanosecond, we return 1 nanosecond.
3240///
3341/// # Errors
3442///
@@ -45,6 +53,10 @@ use crate::display::Quotable;
4553/// assert_eq!(from_str("2d"), Ok(Duration::from_secs(60 * 60 * 24 * 2)));
4654/// ```
4755pub fn from_str ( string : & str ) -> Result < Duration , String > {
56+ // TODO: Switch to Duration::NANOSECOND if that ever becomes stable
57+ // https://github.com/rust-lang/rust/issues/57391
58+ const NANOSECOND_DURATION : Duration = Duration :: from_nanos ( 1 ) ;
59+
4860 let len = string. len ( ) ;
4961 if len == 0 {
5062 return Err ( "empty string" . to_owned ( ) ) ;
@@ -63,23 +75,38 @@ pub fn from_str(string: &str) -> Result<Duration, String> {
6375 _ => return Err ( format ! ( "invalid time interval {}" , string. quote( ) ) ) ,
6476 } ,
6577 } ;
66- let num = numstr
67- . parse :: < f64 > ( )
68- . map_err ( |e| format ! ( "invalid time interval {}: {}" , string. quote( ) , e) ) ?;
78+ let num = match ExtendedBigDecimal :: extended_parse ( numstr) {
79+ Ok ( ebd) | Err ( ExtendedParserError :: Overflow ( ebd) ) => ebd,
80+ Err ( ExtendedParserError :: Underflow ( _) ) => return Ok ( NANOSECOND_DURATION ) ,
81+ _ => return Err ( format ! ( "invalid time interval {}" , string. quote( ) ) ) ,
82+ } ;
6983
70- if num < 0. || num. is_nan ( ) {
71- return Err ( format ! ( "invalid time interval {}" , string. quote( ) ) ) ;
72- }
84+ // Allow non-negative durations (-0 is fine), and infinity.
85+ let num = match num {
86+ ExtendedBigDecimal :: BigDecimal ( bd) if !bd. is_negative ( ) => bd,
87+ ExtendedBigDecimal :: MinusZero => 0 . into ( ) ,
88+ ExtendedBigDecimal :: Infinity => return Ok ( Duration :: MAX ) ,
89+ _ => return Err ( format ! ( "invalid time interval {}" , string. quote( ) ) ) ,
90+ } ;
91+
92+ // Pre-multiply times to avoid precision loss
93+ let num: BigDecimal = num * times;
7394
74- if num. is_infinite ( ) {
75- return Ok ( Duration :: MAX ) ;
95+ // Transform to nanoseconds (9 digits after decimal point)
96+ let ( nanos_bi, _) = num. with_scale ( 9 ) . into_bigint_and_scale ( ) ;
97+
98+ // If the value is smaller than a nanosecond, just return that.
99+ if nanos_bi. is_zero ( ) && !num. is_zero ( ) {
100+ return Ok ( NANOSECOND_DURATION ) ;
76101 }
77102
78103 const NANOS_PER_SEC : u32 = 1_000_000_000 ;
79- let whole_secs = num. trunc ( ) ;
80- let nanos = ( num. fract ( ) * ( NANOS_PER_SEC as f64 ) ) . trunc ( ) ;
81- let duration = Duration :: new ( whole_secs as u64 , nanos as u32 ) ;
82- Ok ( duration. saturating_mul ( times) )
104+ let whole_secs: u64 = match ( & nanos_bi / NANOS_PER_SEC ) . try_into ( ) {
105+ Ok ( whole_secs) => whole_secs,
106+ Err ( _) => return Ok ( Duration :: MAX ) ,
107+ } ;
108+ let nanos: u32 = ( & nanos_bi % NANOS_PER_SEC ) . to_u32 ( ) . unwrap ( ) ;
109+ Ok ( Duration :: new ( whole_secs, nanos) )
83110}
84111
85112#[ cfg( test) ]
@@ -99,8 +126,49 @@ mod tests {
99126 }
100127
101128 #[ test]
102- fn test_saturating_mul ( ) {
129+ fn test_overflow ( ) {
130+ // u64 seconds overflow (in Duration)
103131 assert_eq ! ( from_str( "9223372036854775808d" ) , Ok ( Duration :: MAX ) ) ;
132+ // ExtendedBigDecimal overflow
133+ assert_eq ! ( from_str( "1e92233720368547758080" ) , Ok ( Duration :: MAX ) ) ;
134+ }
135+
136+ #[ test]
137+ fn test_underflow ( ) {
138+ // TODO: Switch to Duration::NANOSECOND if that ever becomes stable
139+ // https://github.com/rust-lang/rust/issues/57391
140+ const NANOSECOND_DURATION : Duration = Duration :: from_nanos ( 1 ) ;
141+
142+ // ExtendedBigDecimal underflow
143+ assert_eq ! ( from_str( "1e-92233720368547758080" ) , Ok ( NANOSECOND_DURATION ) ) ;
144+ // nanoseconds underflow (in Duration)
145+ assert_eq ! ( from_str( "0.0000000001" ) , Ok ( NANOSECOND_DURATION ) ) ;
146+ assert_eq ! ( from_str( "1e-10" ) , Ok ( NANOSECOND_DURATION ) ) ;
147+ assert_eq ! ( from_str( "9e-10" ) , Ok ( NANOSECOND_DURATION ) ) ;
148+ assert_eq ! ( from_str( "1e-9" ) , Ok ( NANOSECOND_DURATION ) ) ;
149+ assert_eq ! ( from_str( "1.9e-9" ) , Ok ( NANOSECOND_DURATION ) ) ;
150+ assert_eq ! ( from_str( "2e-9" ) , Ok ( Duration :: from_nanos( 2 ) ) ) ;
151+ }
152+
153+ #[ test]
154+ fn test_zero ( ) {
155+ assert_eq ! ( from_str( "0e-9" ) , Ok ( Duration :: ZERO ) ) ;
156+ assert_eq ! ( from_str( "0e-100" ) , Ok ( Duration :: ZERO ) ) ;
157+ assert_eq ! ( from_str( "0e-92233720368547758080" ) , Ok ( Duration :: ZERO ) ) ;
158+ assert_eq ! ( from_str( "0.000000000000000000000" ) , Ok ( Duration :: ZERO ) ) ;
159+ }
160+
161+ #[ test]
162+ fn test_hex_float ( ) {
163+ assert_eq ! (
164+ from_str( "0x1.1p-1" ) ,
165+ Ok ( Duration :: from_secs_f64( 0.53125f64 ) )
166+ ) ;
167+ assert_eq ! (
168+ from_str( "0x1.1p-1d" ) ,
169+ Ok ( Duration :: from_secs_f64( 0.53125f64 * 3600.0 * 24.0 ) )
170+ ) ;
171+ assert_eq ! ( from_str( "0xfh" ) , Ok ( Duration :: from_secs( 15 * 3600 ) ) ) ;
104172 }
105173
106174 #[ test]
0 commit comments