fundu_systemd/
lib.rs

1// spell-checker: ignore econd inute onths nute nths econds inutes
2
3//! A simple to use, fast and accurate systemd time span parser fully compatible with the
4//! [systemd time span format](https://www.freedesktop.org/software/systemd/man/systemd.time.html)
5//!
6//! `fundu-systemd` can parse rust strings like
7//!
8//! | `&str` | Duration |
9//! | -- | -- |
10//! | `"2 h"` | `Duration::positive(2 * 60 * 60, 0)` |
11//! | `"2hours"` |`Duration::positive(2 * 60 * 60, 0)` |
12//! | `"second"` |`Duration::positive(1, 0)` |
13//! | `"48hr"` |`Duration::positive(48 * 60 * 60, 0)` |
14//! | `"12.3 seconds"` |`Duration::positive(12, 300_000_000)` |
15//! | `"1y 12month"` | `Duration::positive(63_115_200, 0)` |
16//! | `"999us +1d"` |`Duration::positive(86_400, 999_000)` |
17//! | `"55s500ms"` | `Duration::positive(55, 500_000_000)` |
18//! | `"300ms20s 5day"` |`Duration::positive(20 + 5 * 60 * 60 * 24, 300_000_000)` |
19//! | `"123456789"` |`Duration::positive(123_456_789, 0)` (Default: Second) |
20//! | `"100"` |`Duration::positive(0, 100_000)` (when default is set to `MicroSecond`) |
21//! | `"infinity"` | variable: the maximum duration which is currently in use (see below) |
22//!
23//! Note that `fundu` parses into its own [`Duration`] which is a superset of other `Durations` like
24//! [`std::time::Duration`], [`chrono::Duration`] and [`time::Duration`]. See the
25//! [documentation](https://docs.rs/fundu/latest/fundu/index.html#fundus-duration) how to easily
26//! handle the conversion between these durations.
27//!
28//! # The Format
29//!
30//! Supported time units:
31//!
32//! - `nsec`, `ns` (can be switched on, per default these are not included)
33//! - `usec`, `us`, `µs`
34//! - `msec,` `ms`
35//! - `seconds`, `second`, `sec`, `s`
36//! - `minutes`, `minute`, `min`, `m`
37//! - `hours`, `hour`, `hr`, `h`
38//! - `days`, `day`, `d`
39//! - `weeks`, `week`, `w`
40//! - `months`, `month`, `M` (defined as `30.44` days or a `1/12` year)
41//! - `years`, `year`, `y` (defined as `365.25` days)
42//!
43//! Summary of the rest of the format:
44//!
45//! - Only numbers like `"123 days"` or with fraction `"1.2 days"` but without exponent (like `"3e9
46//!   days"`) are allowed
47//! - For numbers without a time unit (like `"1234"`) the default time unit is usually `second` but
48//!   can be changed since in some case systemd uses a different granularity.
49//! - Time units without a number (like in `"second"`) are allowed and a value of `1` is assumed.
50//! - The parsed duration represents the value exactly (without rounding errors as would occur in
51//!   floating point calculations) as it is specified in the source string (just like systemd).
52//! - The maximum supported duration (`Duration::MAX`) has `u64::MAX` seconds
53//!   (`18_446_744_073_709_551_615`) and `999_999_999` nano seconds. However, systemd uses
54//!   `u64::MAX` micro seconds as maximum duration when parsing without nanos and `u64::MAX` nano
55//!   seconds when parsing with nanos. `fundu-systemd` provides the `parse` and `parse_nanos`
56//!   functions to reflect that. If you don't like the maximum duration of `systemd` it's still
57//!   possible via `parse_with_max` and `parse_nanos_with_max` to adjust this limit to a duration
58//!   ranging from `Duration::ZERO` to `Duration::MAX`.
59//! - The special value `"infinity"` evaluates to the maximum duration. Note the maximum duration
60//!   depends on whether parsing with nano seconds or without. If the maximum duration is manually
61//!   set to a different value then it evaluates to that maximum duration.
62//! - parsed durations larger than the maximum duration (like `"100000000000000years"`) saturate at
63//!   the maximum duration
64//! - Negative durations are not allowed, also no intermediate negative durations like in `"5day
65//!   -1ms"` although the final duration would not be negative.
66//! - Any leading, trailing whitespace or whitespace between the number and the time unit (like in
67//!   `"1 \n sec"`) and multiple durations (like in `"1week \n 2minutes"`) is ignored and follows
68//!   the posix definition of whitespace which is:
69//!     - Space (`' '`)
70//!     - Horizontal Tab (`'\x09'`)
71//!     - Line Feed (`'\x0A'`)
72//!     - Vertical Tab (`'\x0B'`)
73//!     - Form Feed (`'\x0C'`)
74//!     - Carriage Return (`'\x0D'`)
75//!
76//! Please see also the systemd
77//! [documentation](https://www.freedesktop.org/software/systemd/man/systemd.time.html) for a
78//! description of their format.
79//!
80//! # Examples
81//!
82//! A re-usable parser providing different parse methods
83//!
84//! ```rust
85//! use fundu::Duration;
86//! use fundu_systemd::{TimeSpanParser, SYSTEMD_MAX_MICRO_DURATION, SYSTEMD_MAX_NANOS_DURATION};
87//!
88//! const PARSER: TimeSpanParser = TimeSpanParser::new();
89//!
90//! let parser = &PARSER;
91//! assert_eq!(parser.parse("2h"), Ok(Duration::positive(2 * 60 * 60, 0)));
92//! assert_eq!(parser.parse("second"), Ok(Duration::positive(1, 0)));
93//! assert_eq!(
94//!     parser.parse("48hr"),
95//!     Ok(Duration::positive(48 * 60 * 60, 0))
96//! );
97//! assert_eq!(
98//!     parser.parse("12.3 seconds"),
99//!     Ok(Duration::positive(12, 300_000_000))
100//! );
101//! assert_eq!(
102//!     parser.parse("300ms20s 5day"),
103//!     Ok(Duration::positive(20 + 5 * 60 * 60 * 24, 300_000_000))
104//! );
105//! assert_eq!(
106//!     parser.parse("123456789"),
107//!     Ok(Duration::positive(123_456_789, 0))
108//! );
109//! assert_eq!(parser.parse("infinity"), Ok(SYSTEMD_MAX_MICRO_DURATION));
110//!
111//! // Or parsing nano seconds
112//! assert_eq!(
113//!     parser.parse_nanos("7809 nsec"),
114//!     Ok(Duration::positive(0, 7809))
115//! );
116//! assert_eq!(
117//!     parser.parse_nanos("infinity"),
118//!     Ok(SYSTEMD_MAX_NANOS_DURATION)
119//! );
120//! ```
121//!
122//! Change the default unit to something different than `Second`
123//! ```rust
124//! use fundu::{Duration, TimeUnit};
125//! use fundu_systemd::TimeSpanParser;
126//!
127//! let parser = TimeSpanParser::with_default_unit(TimeUnit::MicroSecond);
128//! assert_eq!(parser.parse("100"), Ok(Duration::positive(0, 100_000)));
129//!
130//! let mut parser = TimeSpanParser::new();
131//! parser.set_default_unit(TimeUnit::MicroSecond);
132//!
133//! assert_eq!(parser.parse("100"), Ok(Duration::positive(0, 100_000)));
134//! ```
135//!
136//! Or use one of the global methods [`parse`], [`parse_nanos`].
137//!
138//! ```rust
139//! use fundu::{Duration, ParseError};
140//! use fundu_systemd::{
141//!     parse, parse_nanos, SYSTEMD_MAX_MICRO_DURATION, SYSTEMD_MAX_NANOS_DURATION,
142//! };
143//!
144//! assert_eq!(parse("123 sec", None, None), Ok(Duration::positive(123, 0)));
145//!
146//! // This is an error with `parse` because the nano seconds are excluded
147//! assert_eq!(
148//!     parse("123 nsec", None, None),
149//!     Err(ParseError::InvalidInput("nsec".to_string()))
150//! );
151//!
152//! // Use `parse_nanos` if the nano second time units should be included
153//! assert_eq!(
154//!     parse_nanos("123 nsec", None, None),
155//!     Ok(Duration::positive(0, 123))
156//! );
157//!
158//! // The maximum duration differs depending on the method in use
159//! assert_eq!(
160//!     parse("infinity", None, None),
161//!     Ok(SYSTEMD_MAX_MICRO_DURATION)
162//! );
163//! assert_eq!(
164//!     parse_nanos("infinity", None, None),
165//!     Ok(SYSTEMD_MAX_NANOS_DURATION)
166//! );
167//!
168//! // But can be easily changed
169//! assert_eq!(
170//!     parse_nanos("infinity", None, Some(Duration::MAX)),
171//!     Ok(Duration::MAX)
172//! );
173//! ```
174//!
175//! For further details see [`parse`], [`parse_nanos`] or the documentation of [`TimeSpanParser`]
176//!
177//! [`chrono::Duration`]: https://docs.rs/chrono/latest/chrono/struct.Duration.html
178//! [`time::Duration`]: https://docs.rs/time/latest/time/struct.Duration.html
179
180#![cfg_attr(docsrs, feature(doc_auto_cfg))]
181#![doc(test(attr(warn(unused))))]
182#![doc(test(attr(allow(unused_extern_crates))))]
183#![warn(missing_docs)]
184#![warn(clippy::pedantic)]
185#![warn(clippy::default_numeric_fallback)]
186#![warn(clippy::else_if_without_else)]
187#![warn(clippy::fn_to_numeric_cast_any)]
188#![warn(clippy::get_unwrap)]
189#![warn(clippy::if_then_some_else_none)]
190#![warn(clippy::mixed_read_write_in_expression)]
191#![warn(clippy::partial_pub_fields)]
192#![warn(clippy::rest_pat_in_fully_bound_structs)]
193#![warn(clippy::str_to_string)]
194#![warn(clippy::string_to_string)]
195#![warn(clippy::todo)]
196#![warn(clippy::try_err)]
197#![warn(clippy::undocumented_unsafe_blocks)]
198#![warn(clippy::unneeded_field_pattern)]
199#![allow(clippy::must_use_candidate)]
200#![allow(clippy::return_self_not_must_use)]
201#![allow(clippy::enum_glob_use)]
202#![allow(clippy::module_name_repetitions)]
203
204use fundu::TimeUnit::*;
205use fundu::{
206    Config, ConfigBuilder, Delimiter, Duration, Multiplier, ParseError, Parser, TimeUnit,
207    TimeUnitsLike,
208};
209
210// whitespace definition of: b' ', b'\x09', b'\x0A', b'\x0B', b'\x0C', b'\x0D'
211const DELIMITER: Delimiter = |byte| byte == b' ' || byte.wrapping_sub(9) < 5;
212
213const CONFIG: Config = ConfigBuilder::new()
214    .allow_time_unit_delimiter()
215    .disable_exponent()
216    .disable_infinity()
217    .number_is_optional()
218    .parse_multiple(None)
219    .inner_delimiter(DELIMITER)
220    .outer_delimiter(DELIMITER)
221    .build();
222
223const TIME_UNITS_WITH_NANOS: TimeUnitsWithNanos = TimeUnitsWithNanos {};
224const TIME_UNITS: TimeUnits = TimeUnits {};
225
226const NANO_SECOND: (TimeUnit, Multiplier) = (NanoSecond, Multiplier(1, 0));
227const MICRO_SECOND: (TimeUnit, Multiplier) = (MicroSecond, Multiplier(1, 0));
228const MILLI_SECOND: (TimeUnit, Multiplier) = (MilliSecond, Multiplier(1, 0));
229const SECOND: (TimeUnit, Multiplier) = (Second, Multiplier(1, 0));
230const MINUTE: (TimeUnit, Multiplier) = (Minute, Multiplier(1, 0));
231const HOUR: (TimeUnit, Multiplier) = (Hour, Multiplier(1, 0));
232const DAY: (TimeUnit, Multiplier) = (Day, Multiplier(1, 0));
233const WEEK: (TimeUnit, Multiplier) = (Week, Multiplier(1, 0));
234const MONTH: (TimeUnit, Multiplier) = (Month, Multiplier(1, 0));
235const YEAR: (TimeUnit, Multiplier) = (Year, Multiplier(1, 0));
236
237const PARSER: TimeSpanParser<'static> = TimeSpanParser::new();
238
239/// The maximum duration used when parsing with micro seconds precision
240pub const SYSTEMD_MAX_MICRO_DURATION: Duration =
241    Duration::positive(u64::MAX / 1_000_000, (u64::MAX % 1_000_000) as u32 * 1000);
242
243/// The maximum duration used when parsing with nano seconds precision
244pub const SYSTEMD_MAX_NANOS_DURATION: Duration =
245    Duration::positive(u64::MAX / 1_000_000_000, (u64::MAX % 1_000_000_000) as u32);
246
247/// The main systemd time span parser
248///
249/// Note this parser can be created as const at compile time.
250///
251/// # Examples
252///
253/// ```rust
254/// use fundu::Duration;
255/// use fundu_systemd::{TimeSpanParser, SYSTEMD_MAX_MICRO_DURATION};
256///
257/// const PARSER: TimeSpanParser = TimeSpanParser::new();
258///
259/// let parser = &PARSER;
260/// assert_eq!(parser.parse("2h"), Ok(Duration::positive(2 * 60 * 60, 0)));
261/// assert_eq!(
262///     parser.parse("2hours"),
263///     Ok(Duration::positive(2 * 60 * 60, 0))
264/// );
265/// assert_eq!(parser.parse("second"), Ok(Duration::positive(1, 0)));
266/// assert_eq!(
267///     parser.parse("48hr"),
268///     Ok(Duration::positive(48 * 60 * 60, 0))
269/// );
270/// assert_eq!(
271///     parser.parse("12.3 seconds"),
272///     Ok(Duration::positive(12, 300_000_000))
273/// );
274/// assert_eq!(
275///     parser.parse("1y 12month"),
276///     Ok(Duration::positive(63_115_200, 0))
277/// );
278/// assert_eq!(
279///     parser.parse("999us +1d"),
280///     Ok(Duration::positive(86_400, 999_000))
281/// );
282/// assert_eq!(
283///     parser.parse("55s500ms"),
284///     Ok(Duration::positive(55, 500_000_000))
285/// );
286/// assert_eq!(
287///     parser.parse("300ms20s 5day"),
288///     Ok(Duration::positive(20 + 5 * 60 * 60 * 24, 300_000_000))
289/// );
290/// assert_eq!(
291///     parser.parse("123456789"),
292///     Ok(Duration::positive(123_456_789, 0))
293/// );
294/// assert_eq!(parser.parse("infinity"), Ok(SYSTEMD_MAX_MICRO_DURATION));
295/// ```
296///
297/// It's possible to change the default unit to something different than `Second` either during the
298/// initialization with [`TimeSpanParser::with_default_unit`] or at runtime with
299/// [`TimeSpanParser::set_default_unit`]
300///
301/// ```rust
302/// use fundu::{Duration, TimeUnit};
303/// use fundu_systemd::TimeSpanParser;
304///
305/// let parser = TimeSpanParser::with_default_unit(TimeUnit::MicroSecond);
306/// assert_eq!(parser.parse("100"), Ok(Duration::positive(0, 100_000)));
307///
308/// let mut parser = TimeSpanParser::new();
309/// parser.set_default_unit(TimeUnit::MicroSecond);
310///
311/// assert_eq!(parser.parse("100"), Ok(Duration::positive(0, 100_000)));
312/// ```
313#[derive(Debug, Eq, PartialEq)]
314pub struct TimeSpanParser<'a> {
315    raw: Parser<'a>,
316}
317
318impl<'a> TimeSpanParser<'a> {
319    /// Create a new `TimeSpanParser` with [`TimeUnit::Second`] as default unit
320    ///
321    /// # Examples
322    ///
323    /// ```rust
324    /// use fundu::Duration;
325    /// use fundu_systemd::TimeSpanParser;
326    ///
327    /// let parser = TimeSpanParser::new();
328    /// assert_eq!(parser.parse("2h"), Ok(Duration::positive(2 * 60 * 60, 0)));
329    /// assert_eq!(parser.parse("123"), Ok(Duration::positive(123, 0)));
330    /// assert_eq!(
331    ///     parser.parse("3us +10sec"),
332    ///     Ok(Duration::positive(10, 3_000))
333    /// );
334    /// ```
335    pub const fn new() -> Self {
336        Self {
337            raw: Parser::with_config(CONFIG),
338        }
339    }
340
341    /// Create a new `TimeSpanParser` with the specified [`TimeUnit`] as default
342    ///
343    /// # Examples
344    ///
345    /// ```rust
346    /// use fundu::{Duration, TimeUnit};
347    /// use fundu_systemd::TimeSpanParser;
348    ///
349    /// let parser = TimeSpanParser::with_default_unit(TimeUnit::MicroSecond);
350    /// assert_eq!(parser.parse("123"), Ok(Duration::positive(0, 123_000)));
351    /// assert_eq!(
352    ///     parser.parse("3us +10sec"),
353    ///     Ok(Duration::positive(10, 3_000))
354    /// );
355    /// ```
356    pub const fn with_default_unit(time_unit: TimeUnit) -> Self {
357        let mut config = CONFIG;
358        config.default_unit = time_unit;
359        Self {
360            raw: Parser::with_config(config),
361        }
362    }
363
364    fn parse_infinity(source: &str, max: Duration) -> Option<Duration> {
365        (source == "infinity").then_some(max)
366    }
367
368    /// Parse the `source` string into a [`Duration`]
369    ///
370    /// This method does not include the time units for nano seconds unlike the
371    /// [`TimeSpanParser::parse_nanos`] method. The parser saturates at the maximum [`Duration`] of
372    /// `u64::MAX` micro seconds. If you need a different maximum use the
373    /// [`TimeSpanParser::parse_with_max`] method.
374    ///
375    /// # Errors
376    ///
377    /// Returns a [`ParseError`] if an error during the parsing process occurred
378    ///
379    /// # Examples
380    ///
381    /// ```rust
382    /// use fundu::Duration;
383    /// use fundu_systemd::{TimeSpanParser, SYSTEMD_MAX_MICRO_DURATION};
384    ///
385    /// let parser = TimeSpanParser::new();
386    /// assert_eq!(
387    ///     parser.parse("2hours"),
388    ///     Ok(Duration::positive(2 * 60 * 60, 0))
389    /// );
390    /// assert_eq!(
391    ///     parser.parse("12.3 seconds"),
392    ///     Ok(Duration::positive(12, 300_000_000))
393    /// );
394    /// assert_eq!(
395    ///     parser.parse("100000000000000000000000000000years"),
396    ///     Ok(SYSTEMD_MAX_MICRO_DURATION)
397    /// );
398    /// assert_eq!(
399    ///     parser.parse("1y 12month"),
400    ///     Ok(Duration::positive(63_115_200, 0))
401    /// );
402    /// assert_eq!(
403    ///     parser.parse("123456789"),
404    ///     Ok(Duration::positive(123_456_789, 0))
405    /// );
406    /// assert_eq!(parser.parse("infinity"), Ok(SYSTEMD_MAX_MICRO_DURATION));
407    /// ```
408    pub fn parse(&self, source: &str) -> Result<Duration, ParseError> {
409        self.parse_with_max(source, SYSTEMD_MAX_MICRO_DURATION)
410    }
411
412    /// Parse the `source` string into a [`Duration`] saturating at the given `max` [`Duration`]
413    ///
414    /// This method does not include the time units for nano seconds unlike the
415    /// [`TimeSpanParser::parse_nanos_with_max`] method
416    ///
417    /// # Panics
418    ///
419    /// This method panics if `max` is a a negative [`Duration`].
420    ///
421    /// # Errors
422    ///
423    /// Returns a [`ParseError`] if an error during the parsing process occurred
424    ///
425    /// # Examples
426    ///
427    /// ```rust
428    /// use fundu::Duration;
429    /// use fundu_systemd::TimeSpanParser;
430    ///
431    /// let parser = TimeSpanParser::new();
432    /// assert_eq!(
433    ///     parser.parse_with_max("100000000000000000000000000000years", Duration::MAX),
434    ///     Ok(Duration::MAX)
435    /// );
436    /// assert_eq!(
437    ///     parser.parse_with_max("123 sec", Duration::positive(1, 0)),
438    ///     Ok(Duration::positive(1, 0))
439    /// );
440    /// assert_eq!(
441    ///     parser.parse_with_max("infinity", Duration::positive(i64::MAX as u64, 123)),
442    ///     Ok(Duration::positive(i64::MAX as u64, 123))
443    /// );
444    /// ```
445    pub fn parse_with_max(&self, source: &str, max: Duration) -> Result<Duration, ParseError> {
446        assert!(max.is_positive());
447        let trimmed = trim_whitespace(source);
448        match Self::parse_infinity(trimmed, max) {
449            Some(duration) => Ok(duration),
450            None => self
451                .raw
452                .parse(trimmed, &TIME_UNITS, None, None)
453                .map(|duration| duration.min(max)),
454        }
455    }
456
457    /// Parse the `source` string into a [`Duration`]
458    ///
459    /// This method does include the time units for nano seconds unlike the
460    /// [`TimeSpanParser::parse`] method. The parser saturates at the maximum [`Duration`] of
461    /// `u64::MAX` nano seconds. If you need a different maximum use the
462    /// [`TimeSpanParser::parse_nanos_with_max`] method.
463    ///
464    /// # Errors
465    ///
466    /// Returns a [`ParseError`] if an error during the parsing process occurred
467    ///
468    /// # Examples
469    ///
470    /// ```rust
471    /// use fundu::Duration;
472    /// use fundu_systemd::{TimeSpanParser, SYSTEMD_MAX_NANOS_DURATION};
473    ///
474    /// let parser = TimeSpanParser::new();
475    /// assert_eq!(
476    ///     parser.parse_nanos("2hours"),
477    ///     Ok(Duration::positive(2 * 60 * 60, 0))
478    /// );
479    /// assert_eq!(
480    ///     parser.parse_nanos("12.3 seconds"),
481    ///     Ok(Duration::positive(12, 300_000_000))
482    /// );
483    /// assert_eq!(
484    ///     parser.parse_nanos("100000000000000000000000000000years"),
485    ///     Ok(SYSTEMD_MAX_NANOS_DURATION)
486    /// );
487    /// assert_eq!(
488    ///     parser.parse_nanos("1y 12month"),
489    ///     Ok(Duration::positive(63_115_200, 0))
490    /// );
491    /// assert_eq!(
492    ///     parser.parse_nanos("123456789"),
493    ///     Ok(Duration::positive(123_456_789, 0))
494    /// );
495    /// assert_eq!(
496    ///     parser.parse_nanos("infinity"),
497    ///     Ok(SYSTEMD_MAX_NANOS_DURATION)
498    /// );
499    /// ```
500    pub fn parse_nanos(&self, source: &str) -> Result<Duration, ParseError> {
501        self.parse_nanos_with_max(source, SYSTEMD_MAX_NANOS_DURATION)
502    }
503
504    /// Parse the `source` string into a [`Duration`] saturating at the given `max` [`Duration`]
505    ///
506    /// This method does include the time units for nano seconds unlike the
507    /// [`TimeSpanParser::parse_with_max`] method
508    ///
509    /// # Panics
510    ///
511    /// This method panics if `max` is a a negative [`Duration`].
512    ///
513    /// # Errors
514    ///
515    /// Returns a [`ParseError`] if an error during the parsing process occurred
516    ///
517    /// # Examples
518    ///
519    /// ```rust
520    /// use fundu::Duration;
521    /// use fundu_systemd::TimeSpanParser;
522    ///
523    /// let parser = TimeSpanParser::new();
524    /// assert_eq!(
525    ///     parser.parse_nanos_with_max("100000000000000000000000000000years", Duration::MAX),
526    ///     Ok(Duration::MAX)
527    /// );
528    /// assert_eq!(
529    ///     parser.parse_nanos_with_max("1234567890 nsec", Duration::positive(1, 0)),
530    ///     Ok(Duration::positive(1, 0))
531    /// );
532    /// assert_eq!(
533    ///     parser.parse_nanos_with_max("infinity", Duration::positive(i64::MAX as u64, 123)),
534    ///     Ok(Duration::positive(i64::MAX as u64, 123))
535    /// );
536    /// ```
537    pub fn parse_nanos_with_max(
538        &self,
539        source: &str,
540        max: Duration,
541    ) -> Result<Duration, ParseError> {
542        assert!(max.is_positive());
543        let trimmed = trim_whitespace(source);
544        match Self::parse_infinity(trimmed, max) {
545            Some(duration) => Ok(duration),
546            None => self
547                .raw
548                .parse(trimmed, &TIME_UNITS_WITH_NANOS, None, None)
549                .map(|duration| duration.min(max)),
550        }
551    }
552
553    /// Set the default [`TimeUnit`] during runtime
554    ///
555    /// The default unit is applied to numbers without time units
556    ///
557    /// # Examples
558    ///
559    /// ```rust
560    /// use fundu::{Duration, TimeUnit};
561    /// use fundu_systemd::TimeSpanParser;
562    ///
563    /// let mut parser = TimeSpanParser::with_default_unit(TimeUnit::MicroSecond);
564    /// assert_eq!(parser.parse("100"), Ok(Duration::positive(0, 100_000)));
565    ///
566    /// parser.set_default_unit(TimeUnit::Second);
567    /// assert_eq!(parser.parse("100"), Ok(Duration::positive(100, 0)));
568    ///
569    /// let mut parser = TimeSpanParser::new();
570    /// assert_eq!(parser.parse("123456"), Ok(Duration::positive(123456, 0)));
571    ///
572    /// parser.set_default_unit(TimeUnit::MicroSecond);
573    /// assert_eq!(
574    ///     parser.parse("123456"),
575    ///     Ok(Duration::positive(0, 123_456_000))
576    /// );
577    /// ```
578    pub fn set_default_unit(&mut self, time_unit: TimeUnit) {
579        self.raw.config.default_unit = time_unit;
580    }
581}
582
583impl<'a> Default for TimeSpanParser<'a> {
584    fn default() -> Self {
585        Self::new()
586    }
587}
588
589/// This struct is used internally to hold the time units without nano second time units
590pub struct TimeUnits {}
591
592impl TimeUnitsLike for TimeUnits {
593    #[inline]
594    fn is_empty(&self) -> bool {
595        false
596    }
597
598    #[inline]
599    fn get(&self, identifier: &str) -> Option<(TimeUnit, Multiplier)> {
600        match identifier {
601            // These are two different letters: the greek small letter mu U+03BC and the micro sign
602            // U+00B5
603            "us" | "\u{03bc}s" | "\u{00b5}s" | "usec" => Some(MICRO_SECOND),
604            "ms" | "msec" => Some(MILLI_SECOND),
605            "s" | "sec" | "second" | "seconds" => Some(SECOND),
606            "m" | "min" | "minute" | "minutes" => Some(MINUTE),
607            "h" | "hr" | "hour" | "hours" => Some(HOUR),
608            "d" | "day" | "days" => Some(DAY),
609            "w" | "week" | "weeks" => Some(WEEK),
610            "M" | "month" | "months" => Some(MONTH),
611            "y" | "year" | "years" => Some(YEAR),
612            _ => None,
613        }
614    }
615}
616
617/// This struct is used internally to hold the time units with nano second time units
618pub struct TimeUnitsWithNanos {}
619
620impl TimeUnitsLike for TimeUnitsWithNanos {
621    #[inline]
622    fn is_empty(&self) -> bool {
623        false
624    }
625
626    #[inline]
627    fn get(&self, identifier: &str) -> Option<(TimeUnit, Multiplier)> {
628        match identifier {
629            "ns" | "nsec" => Some(NANO_SECOND),
630            // These are two different letters: the greek small letter mu U+03BC and the micro sign
631            // U+00B5
632            "us" | "\u{03bc}s" | "\u{00b5}s" | "usec" => Some(MICRO_SECOND),
633            "ms" | "msec" => Some(MILLI_SECOND),
634            "s" | "sec" | "second" | "seconds" => Some(SECOND),
635            "m" | "min" | "minute" | "minutes" => Some(MINUTE),
636            "h" | "hr" | "hour" | "hours" => Some(HOUR),
637            "d" | "day" | "days" => Some(DAY),
638            "w" | "week" | "weeks" => Some(WEEK),
639            "M" | "month" | "months" => Some(MONTH),
640            "y" | "year" | "years" => Some(YEAR),
641            _ => None,
642        }
643    }
644}
645
646/// Parse the `source` string into a [`Duration`]
647///
648/// This method does not include the time units for nano seconds unlike the
649/// [`TimeSpanParser::parse_nanos`] method. The parser saturates at the maximum [`Duration`] of
650/// `u64::MAX` micro seconds if not specified otherwise. Optionally, it's possible to specify a
651/// different default time unit than [`TimeUnit::Second`]
652///
653/// # Panics
654///
655/// This method panics if `max` is a a negative [`Duration`].
656///
657/// # Errors
658///
659/// Returns a [`ParseError`] if an error during the parsing process occurred
660///
661/// # Examples
662///
663/// ```rust
664/// use fundu::{Duration, TimeUnit};
665/// use fundu_systemd::{parse, SYSTEMD_MAX_MICRO_DURATION};
666///
667/// assert_eq!(
668///     parse("2hours", None, None),
669///     Ok(Duration::positive(2 * 60 * 60, 0))
670/// );
671/// assert_eq!(
672///     parse("1y 12month", None, None),
673///     Ok(Duration::positive(63_115_200, 0))
674/// );
675/// assert_eq!(
676///     parse("12.3", Some(TimeUnit::MilliSecond), None),
677///     Ok(Duration::positive(0, 12_300_000))
678/// );
679/// assert_eq!(
680///     parse("100000000000000000000000000000years", None, None),
681///     Ok(SYSTEMD_MAX_MICRO_DURATION)
682/// );
683/// assert_eq!(
684///     parse(
685///         "100000000000000000000000000000years",
686///         None,
687///         Some(Duration::MAX)
688///     ),
689///     Ok(Duration::MAX)
690/// );
691/// assert_eq!(
692///     parse("infinity", None, None),
693///     Ok(SYSTEMD_MAX_MICRO_DURATION)
694/// );
695/// ```
696pub fn parse(
697    source: &str,
698    default_unit: Option<TimeUnit>,
699    max: Option<Duration>,
700) -> Result<Duration, ParseError> {
701    match default_unit {
702        None | Some(TimeUnit::Second) => {
703            PARSER.parse_with_max(source, max.unwrap_or(SYSTEMD_MAX_MICRO_DURATION))
704        }
705        Some(time_unit) => {
706            let mut parser = PARSER;
707            parser.set_default_unit(time_unit);
708            parser.parse_with_max(source, max.unwrap_or(SYSTEMD_MAX_MICRO_DURATION))
709        }
710    }
711}
712
713/// Parse the `source` string into a [`Duration`] with nano second time units
714///
715/// This method does include the time units for nano seconds unlike the [`parse`] method. The parser
716/// saturates at the maximum [`Duration`] of `u64::MAX` nano seconds if not specified otherwise.
717/// Optionally, it's possible to specify a different default time unit than [`TimeUnit::Second`]
718///
719/// # Panics
720///
721/// This method panics if `max` is a a negative [`Duration`].
722///
723/// # Errors
724///
725/// Returns a [`ParseError`] if an error during the parsing process occurred
726///
727/// # Examples
728///
729/// ```rust
730/// use fundu::{Duration, TimeUnit};
731/// use fundu_systemd::{parse_nanos, SYSTEMD_MAX_NANOS_DURATION};
732///
733/// assert_eq!(
734///     parse_nanos("2nsec", None, None),
735///     Ok(Duration::positive(0, 2))
736/// );
737/// assert_eq!(
738///     parse_nanos("1y 12month", None, None),
739///     Ok(Duration::positive(63_115_200, 0))
740/// );
741/// assert_eq!(
742///     parse_nanos("12.3", Some(TimeUnit::MilliSecond), None),
743///     Ok(Duration::positive(0, 12_300_000))
744/// );
745/// assert_eq!(
746///     parse_nanos("100000000000000000000000000000years", None, None),
747///     Ok(SYSTEMD_MAX_NANOS_DURATION)
748/// );
749/// assert_eq!(
750///     parse_nanos(
751///         "100000000000000000000000000000years",
752///         None,
753///         Some(Duration::MAX)
754///     ),
755///     Ok(Duration::MAX)
756/// );
757/// assert_eq!(
758///     parse_nanos("infinity", None, None),
759///     Ok(SYSTEMD_MAX_NANOS_DURATION)
760/// );
761/// ```
762pub fn parse_nanos(
763    source: &str,
764    default_unit: Option<TimeUnit>,
765    max: Option<Duration>,
766) -> Result<Duration, ParseError> {
767    match default_unit {
768        None | Some(TimeUnit::Second) => {
769            PARSER.parse_nanos_with_max(source, max.unwrap_or(SYSTEMD_MAX_NANOS_DURATION))
770        }
771        Some(time_unit) => {
772            let mut parser = PARSER;
773            parser.set_default_unit(time_unit);
774            parser.parse_nanos_with_max(source, max.unwrap_or(SYSTEMD_MAX_NANOS_DURATION))
775        }
776    }
777}
778
779// This is a faster alternative to str::trim_matches. We're exploiting that we're using the posix
780// definition of whitespace which only contains ascii characters as whitespace
781fn trim_whitespace(source: &str) -> &str {
782    let mut bytes = source.as_bytes();
783    while let Some((byte, remainder)) = bytes.split_first() {
784        if byte == &b' ' || byte.wrapping_sub(9) < 5 {
785            bytes = remainder;
786        } else {
787            break;
788        }
789    }
790    while let Some((byte, remainder)) = bytes.split_last() {
791        if byte == &b' ' || byte.wrapping_sub(9) < 5 {
792            bytes = remainder;
793        } else {
794            break;
795        }
796    }
797    // SAFETY: We've trimmed only ascii characters and therefore valid utf-8
798    unsafe { std::str::from_utf8_unchecked(bytes) }
799}
800
801#[cfg(test)]
802mod tests {
803    use rstest::rstest;
804
805    use super::*;
806
807    #[test]
808    fn test_parser_new() {
809        let parser = TimeSpanParser::new();
810        assert_eq!(parser.raw.config, CONFIG);
811    }
812
813    #[rstest]
814    #[case::not_second(TimeUnit::Week)]
815    #[case::second(TimeUnit::Second)]
816    fn test_parser_with_default_unit(#[case] time_unit: TimeUnit) {
817        let parser = TimeSpanParser::with_default_unit(time_unit);
818        let mut config = CONFIG;
819        config.default_unit = time_unit;
820        assert_eq!(parser.raw.config, config);
821    }
822
823    #[rstest]
824    #[case::not_second(TimeUnit::Week)]
825    #[case::second(TimeUnit::Second)]
826    fn test_parser_set_default_unit(#[case] time_unit: TimeUnit) {
827        let mut config = CONFIG;
828        config.default_unit = time_unit;
829
830        let mut parser = TimeSpanParser::new();
831        parser.set_default_unit(time_unit);
832
833        assert_eq!(parser.raw.config, config);
834    }
835
836    #[test]
837    fn test_parser_default() {
838        assert_eq!(TimeSpanParser::new(), TimeSpanParser::default());
839        assert_eq!(TimeSpanParser::default(), PARSER);
840    }
841
842    #[rstest]
843    #[case::time_units(&TimeUnits {})]
844    #[case::time_units_with_nanos(&TimeUnitsWithNanos {})]
845    fn test_time_units_is_not_empty(#[case] time_units_like: &dyn TimeUnitsLike) {
846        assert!(!time_units_like.is_empty());
847    }
848}