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}