uucore/features/parser/
parse_time.rs

1// This file is part of the uutils coreutils package.
2//
3// For the full copyright and license information, please view the LICENSE
4// file that was distributed with this source code.
5
6// spell-checker:ignore (vars) NANOS numstr infinityh INFD nans nanh bigdecimal extendedbigdecimal
7//! Parsing a duration from a string.
8//!
9//! Use the [`from_str`] function to parse a [`Duration`] from a string.
10
11use crate::{
12    display::Quotable,
13    extendedbigdecimal::ExtendedBigDecimal,
14    parser::num_parser::{self, ExtendedParserError, ParseTarget},
15};
16use num_traits::ToPrimitive;
17use num_traits::Zero;
18use num_traits::{FromPrimitive, Signed};
19use std::time::Duration;
20
21/// Parse a duration from a string.
22///
23/// The string may contain only a number, like "123" or "4.5", or it
24/// may contain a number with a unit specifier, like "123s" meaning
25/// one hundred twenty three seconds or "4.5d" meaning four and a half
26/// days. If no unit is specified, the unit is assumed to be seconds.
27///
28/// If `allow_suffixes` is true, the allowed suffixes are
29///
30/// * "s" for seconds,
31/// * "m" for minutes,
32/// * "h" for hours,
33/// * "d" for days.
34///
35/// This function does not overflow if large values are provided. If
36/// overflow would have occurred, [`Duration::MAX`] is returned instead.
37///
38/// If the value is smaller than 1 nanosecond, we return 1 nanosecond.
39///
40/// # Errors
41///
42/// This function returns an error if the input string is empty, the
43/// input is not a valid number, or the unit specifier is invalid or
44/// unknown.
45///
46/// # Examples
47///
48/// ```rust
49/// use std::time::Duration;
50/// use uucore::parser::parse_time::from_str;
51/// assert_eq!(from_str("123", true), Ok(Duration::from_secs(123)));
52/// assert_eq!(from_str("123", false), Ok(Duration::from_secs(123)));
53/// assert_eq!(from_str("2d", true), Ok(Duration::from_secs(60 * 60 * 24 * 2)));
54/// assert!(from_str("2d", false).is_err());
55/// ```
56pub fn from_str(string: &str, allow_suffixes: bool) -> Result<Duration, String> {
57    // TODO: Switch to Duration::NANOSECOND if that ever becomes stable
58    // https://github.com/rust-lang/rust/issues/57391
59    const NANOSECOND_DURATION: Duration = Duration::from_nanos(1);
60
61    let len = string.len();
62    if len == 0 {
63        return Err(format!("invalid time interval {}", string.quote()));
64    }
65    let num = match num_parser::parse(
66        string,
67        ParseTarget::Duration,
68        if allow_suffixes {
69            &[('s', 1), ('m', 60), ('h', 60 * 60), ('d', 60 * 60 * 24)]
70        } else {
71            &[]
72        },
73    ) {
74        Ok(ebd) | Err(ExtendedParserError::Overflow(ebd)) => ebd,
75        Err(ExtendedParserError::Underflow(_)) => return Ok(NANOSECOND_DURATION),
76        _ => {
77            return Err(format!("invalid time interval {}", string.quote()));
78        }
79    };
80
81    // Allow non-negative durations (-0 is fine), and infinity.
82    let num = match num {
83        ExtendedBigDecimal::BigDecimal(bd) if !bd.is_negative() => {
84            if bd.fractional_digit_count() <= -20 {
85                // bd >= 10^20 > u64::MAX -- early return to avoid
86                // potentially expensive to-nanoseconds conversion
87                return Ok(Duration::MAX);
88            }
89            // early return if number is too small (< 1 ns)
90            if !bd.is_zero() && bd < bigdecimal::BigDecimal::from_f64(0.0000000001).unwrap() {
91                return Ok(NANOSECOND_DURATION);
92            }
93            bd
94        }
95        ExtendedBigDecimal::MinusZero => 0.into(),
96        ExtendedBigDecimal::Infinity => return Ok(Duration::MAX),
97        _ => return Err(format!("invalid time interval {}", string.quote())),
98    };
99
100    // Transform to nanoseconds (9 digits after decimal point)
101    let (nanos_bi, _) = num.with_scale(9).into_bigint_and_scale();
102
103    // If the value is smaller than a nanosecond, just return that.
104    if nanos_bi.is_zero() && !num.is_zero() {
105        return Ok(NANOSECOND_DURATION);
106    }
107
108    const NANOS_PER_SEC: u32 = 1_000_000_000;
109    let whole_secs: u64 = match (&nanos_bi / NANOS_PER_SEC).try_into() {
110        Ok(whole_secs) => whole_secs,
111        Err(_) => return Ok(Duration::MAX),
112    };
113    let nanos: u32 = (&nanos_bi % NANOS_PER_SEC).to_u32().unwrap();
114    Ok(Duration::new(whole_secs, nanos))
115}
116
117#[cfg(test)]
118mod tests {
119
120    use crate::parser::parse_time::from_str;
121    use std::time::Duration;
122
123    #[test]
124    fn test_no_units() {
125        assert_eq!(from_str("123", true), Ok(Duration::from_secs(123)));
126        assert_eq!(from_str("123", false), Ok(Duration::from_secs(123)));
127    }
128
129    #[test]
130    fn test_units() {
131        assert_eq!(
132            from_str("2d", true),
133            Ok(Duration::from_secs(60 * 60 * 24 * 2))
134        );
135        assert!(from_str("2d", false).is_err());
136    }
137
138    #[test]
139    fn test_overflow() {
140        // u64 seconds overflow (in Duration)
141        assert_eq!(from_str("9223372036854775808d", true), Ok(Duration::MAX));
142        assert_eq!(from_str("1e6666666666668320", true), Ok(Duration::MAX));
143        // ExtendedBigDecimal overflow
144        assert_eq!(from_str("1e92233720368547758080", false), Ok(Duration::MAX));
145        assert_eq!(from_str("1e92233720368547758080", false), Ok(Duration::MAX));
146    }
147
148    #[test]
149    fn test_underflow() {
150        // TODO: Switch to Duration::NANOSECOND if that ever becomes stable
151        // https://github.com/rust-lang/rust/issues/57391
152        const NANOSECOND_DURATION: Duration = Duration::from_nanos(1);
153
154        // ExtendedBigDecimal underflow
155        assert_eq!(
156            from_str("1e-92233720368547758080", true),
157            Ok(NANOSECOND_DURATION)
158        );
159        // nanoseconds underflow (in Duration, true)
160        assert_eq!(from_str("0.0000000001", true), Ok(NANOSECOND_DURATION));
161        assert_eq!(from_str("1e-10", true), Ok(NANOSECOND_DURATION));
162        assert_eq!(from_str("9e-10", true), Ok(NANOSECOND_DURATION));
163        assert_eq!(from_str("1e-9", true), Ok(NANOSECOND_DURATION));
164        assert_eq!(from_str("1.9e-9", true), Ok(NANOSECOND_DURATION));
165        assert_eq!(from_str("2e-9", true), Ok(Duration::from_nanos(2)));
166
167        // ExtendedBigDecimal underflow
168        assert_eq!(
169            from_str("1e-92233720368547758080", false),
170            Ok(NANOSECOND_DURATION)
171        );
172        assert_eq!(
173            from_str("0x6p-4376646810043701", false),
174            Ok(NANOSECOND_DURATION)
175        );
176        // nanoseconds underflow (in Duration, false)
177        assert_eq!(from_str("0.0000000001", false), Ok(NANOSECOND_DURATION));
178        assert_eq!(from_str("1e-10", false), Ok(NANOSECOND_DURATION));
179        assert_eq!(from_str("9e-10", false), Ok(NANOSECOND_DURATION));
180        assert_eq!(from_str("1e-9", false), Ok(NANOSECOND_DURATION));
181        assert_eq!(from_str("1.9e-9", false), Ok(NANOSECOND_DURATION));
182        assert_eq!(from_str("2e-9", false), Ok(Duration::from_nanos(2)));
183    }
184
185    #[test]
186    fn test_zero() {
187        assert_eq!(from_str("0e-9", true), Ok(Duration::ZERO));
188        assert_eq!(from_str("0e-100", true), Ok(Duration::ZERO));
189        assert_eq!(
190            from_str("0e-92233720368547758080", true),
191            Ok(Duration::ZERO)
192        );
193        assert_eq!(
194            from_str("0.000000000000000000000", true),
195            Ok(Duration::ZERO)
196        );
197
198        assert_eq!(from_str("0e-9", false), Ok(Duration::ZERO));
199        assert_eq!(from_str("0e-100", false), Ok(Duration::ZERO));
200        assert_eq!(
201            from_str("0e-92233720368547758080", false),
202            Ok(Duration::ZERO)
203        );
204        assert_eq!(
205            from_str("0.000000000000000000000", false),
206            Ok(Duration::ZERO)
207        );
208    }
209
210    #[test]
211    fn test_hex_float() {
212        assert_eq!(
213            from_str("0x1.1p-1", true),
214            Ok(Duration::from_secs_f64(0.53125f64))
215        );
216        assert_eq!(
217            from_str("0x1.1p-1", false),
218            Ok(Duration::from_secs_f64(0.53125f64))
219        );
220        assert_eq!(
221            from_str("0x1.1p-1d", true),
222            Ok(Duration::from_secs_f64(0.53125f64 * 3600.0 * 24.0))
223        );
224        assert_eq!(from_str("0xfh", true), Ok(Duration::from_secs(15 * 3600)));
225    }
226
227    #[test]
228    fn test_error_empty() {
229        assert!(from_str("", true).is_err());
230        assert!(from_str("", false).is_err());
231    }
232
233    #[test]
234    fn test_error_invalid_unit() {
235        assert!(from_str("123X", true).is_err());
236        assert!(from_str("123X", false).is_err());
237    }
238
239    #[test]
240    fn test_error_multi_bytes_characters() {
241        assert!(from_str("10€", true).is_err());
242        assert!(from_str("10€", false).is_err());
243    }
244
245    #[test]
246    fn test_error_invalid_magnitude() {
247        assert!(from_str("12abc3s", true).is_err());
248        assert!(from_str("12abc3s", false).is_err());
249    }
250
251    #[test]
252    fn test_error_only_point() {
253        assert!(from_str(".", true).is_err());
254        assert!(from_str(".", false).is_err());
255    }
256
257    #[test]
258    fn test_negative() {
259        assert!(from_str("-1", true).is_err());
260        assert!(from_str("-1", false).is_err());
261    }
262
263    #[test]
264    fn test_infinity() {
265        assert_eq!(from_str("inf", true), Ok(Duration::MAX));
266        assert_eq!(from_str("infinity", true), Ok(Duration::MAX));
267        assert_eq!(from_str("infinityh", true), Ok(Duration::MAX));
268        assert_eq!(from_str("INF", true), Ok(Duration::MAX));
269        assert_eq!(from_str("INFs", true), Ok(Duration::MAX));
270
271        assert_eq!(from_str("inf", false), Ok(Duration::MAX));
272        assert_eq!(from_str("infinity", false), Ok(Duration::MAX));
273        assert_eq!(from_str("INF", false), Ok(Duration::MAX));
274    }
275
276    #[test]
277    fn test_nan() {
278        assert!(from_str("nan", true).is_err());
279        assert!(from_str("nans", true).is_err());
280        assert!(from_str("-nanh", true).is_err());
281        assert!(from_str("NAN", true).is_err());
282        assert!(from_str("-NAN", true).is_err());
283
284        assert!(from_str("nan", false).is_err());
285        assert!(from_str("NAN", false).is_err());
286        assert!(from_str("-NAN", false).is_err());
287    }
288
289    /// Test that capital letters are not allowed in suffixes.
290    #[test]
291    fn test_no_capital_letters() {
292        assert!(from_str("1S", true).is_err());
293        assert!(from_str("1M", true).is_err());
294        assert!(from_str("1H", true).is_err());
295        assert!(from_str("1D", true).is_err());
296        assert!(from_str("INFD", true).is_err());
297    }
298}