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
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 ( ) ) ;
@@ -58,27 +70,43 @@ pub fn from_str(string: &str) -> Result<Duration, String> {
5870 'h' => ( slice, 60 * 60 ) ,
5971 'd' => ( slice, 60 * 60 * 24 ) ,
6072 val if !val. is_alphabetic ( ) => ( string, 1 ) ,
61- _ => {
62- if string == "inf" || string == "infinity" {
63- ( "inf" , 1 )
64- } else {
65- return Err ( format ! ( "invalid time interval {}" , string. quote( ) ) ) ;
66- }
67- }
73+ _ => match string. to_ascii_lowercase ( ) . as_str ( ) {
74+ "inf" | "infinity" => ( "inf" , 1 ) ,
75+ _ => return Err ( format ! ( "invalid time interval {}" , string. quote( ) ) ) ,
76+ } ,
77+ } ;
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( ) ) ) ,
6882 } ;
69- let num = numstr
70- . parse :: < f64 > ( )
71- . map_err ( |e| format ! ( "invalid time interval {}: {}" , string. quote( ) , e) ) ?;
7283
73- if num < 0. {
74- return Err ( format ! ( "invalid time interval {}" , string. quote( ) ) ) ;
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;
94+
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 ) ;
75101 }
76102
77103 const NANOS_PER_SEC : u32 = 1_000_000_000 ;
78- let whole_secs = num. trunc ( ) ;
79- let nanos = ( num. fract ( ) * ( NANOS_PER_SEC as f64 ) ) . trunc ( ) ;
80- let duration = Duration :: new ( whole_secs as u64 , nanos as u32 ) ;
81- 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) )
82110}
83111
84112#[ cfg( test) ]
@@ -98,8 +126,49 @@ mod tests {
98126 }
99127
100128 #[ test]
101- fn test_saturating_mul ( ) {
129+ fn test_overflow ( ) {
130+ // u64 seconds overflow (in Duration)
102131 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 ) ) ) ;
103172 }
104173
105174 #[ test]
@@ -127,12 +196,31 @@ mod tests {
127196 assert ! ( from_str( "-1" ) . is_err( ) ) ;
128197 }
129198
199+ #[ test]
200+ fn test_infinity ( ) {
201+ assert_eq ! ( from_str( "inf" ) , Ok ( Duration :: MAX ) ) ;
202+ assert_eq ! ( from_str( "infinity" ) , Ok ( Duration :: MAX ) ) ;
203+ assert_eq ! ( from_str( "infinityh" ) , Ok ( Duration :: MAX ) ) ;
204+ assert_eq ! ( from_str( "INF" ) , Ok ( Duration :: MAX ) ) ;
205+ assert_eq ! ( from_str( "INFs" ) , Ok ( Duration :: MAX ) ) ;
206+ }
207+
208+ #[ test]
209+ fn test_nan ( ) {
210+ assert ! ( from_str( "nan" ) . is_err( ) ) ;
211+ assert ! ( from_str( "nans" ) . is_err( ) ) ;
212+ assert ! ( from_str( "-nanh" ) . is_err( ) ) ;
213+ assert ! ( from_str( "NAN" ) . is_err( ) ) ;
214+ assert ! ( from_str( "-NAN" ) . is_err( ) ) ;
215+ }
216+
130217 /// Test that capital letters are not allowed in suffixes.
131218 #[ test]
132219 fn test_no_capital_letters ( ) {
133220 assert ! ( from_str( "1S" ) . is_err( ) ) ;
134221 assert ! ( from_str( "1M" ) . is_err( ) ) ;
135222 assert ! ( from_str( "1H" ) . is_err( ) ) ;
136223 assert ! ( from_str( "1D" ) . is_err( ) ) ;
224+ assert ! ( from_str( "INFD" ) . is_err( ) ) ;
137225 }
138226}
0 commit comments