fundu_gnu/
lib.rs

1// Copyright (c) 2023 Joining7943 <joining@posteo.de>
2//
3// This software is released under the MIT License.
4// https://opensource.org/licenses/MIT
5
6//! A simple to use, fast and precise gnu relative time parser fully compatible with the [gnu
7//! relative time
8//! format](https://www.gnu.org/software/coreutils/manual/html_node/Relative-items-in-date-strings.html)
9//!
10//! `fundu-gnu` can parse rust strings like
11//!
12//! `&str` | Duration |
13//! -- | -- |
14//! `"1hour"`| `Duration::positive(60 * 60, 0)` |
15//! `"minute"`| `Duration::positive(60, 0)` |
16//! `"2 hours"`| `Duration::positive(2 * 60 * 60, 0)` |
17//! `"-3minutes"`| `Duration::negative(3 * 60, 0)` |
18//! `"3 mins ago"`| `Duration::negative(3 * 60, 0)` |
19//! `"999sec +1day"`| `Duration::positive(86_400 + 999, 0)` |
20//! `"55secs500week"`| `Duration::positive(55 + 500 * 604_800, 0)` |
21//! `"123456789"`| `Duration::positive(123_456_789, 0)` |
22//! `"42fortnight"`| `Duration::positive(42 * 2 * 604_800, 0)` |
23//! `"yesterday"`| `Duration::negative(24 * 60 * 60, 0)` |
24//! `"now"`| `Duration::positive(0, 0)` |
25//! `"today -10seconds"`| `Duration::negative(10, 0)` |
26//!
27//! `fundu` parses into its own [`Duration`] which is a superset of other `Durations` like
28//! [`std::time::Duration`], [`chrono::Duration`] and [`time::Duration`]. See the
29//! [documentation](https://docs.rs/fundu/latest/fundu/index.html#fundus-duration) how to easily
30//! handle the conversion between these durations.
31//!
32//! # The Format
33//! Supported time units:
34//!
35//! - `seconds`, `second`, `secs`, `sec`
36//! - `minutes`, `minute`, `mins`, `min`
37//! - `hours`, `hour`
38//! - `days`, `day`
39//! - `weeks`, `week`
40//! - `fortnights`, `fortnight` (2 weeks)
41//! - `months`, `month` (fuzzy)
42//! - `years`, `year` (fuzzy)
43//!
44//! Fuzzy time units are not all of equal duration and depend on a given date. If no date is given
45//! when parsing, the system time of `now` in UTC +0 is assumed.
46//!
47//! The special keywords `yesterday` worth `-1 day`, `tomorrow` worth `+1 day`, `today` and `now`
48//! each worth a zero duration are allowed, too. These keywords count as a full duration and don't
49//! accept a number, time unit or the `ago` time unit suffix.
50//!
51//! Summary of the rest of the format:
52//!
53//! - Only numbers like `"123 days"` and without exponent (like `"3e9 days"`) are allowed. Only
54//!   seconds time units allow a fraction (like in `"1.123456 secs"`)
55//! - Multiple durations like `"1sec 2min"` or `"1week2secs"` in the source string accumulate
56//! - Time units without a number (like in `"second"`) are allowed and a value of `1` is assumed.
57//! - The parsed duration represents the value exactly (without rounding errors as would occur in
58//!   floating point calculations) as it is specified in the source string.
59//! - The maximum supported duration (`Duration::MAX`) has `u64::MAX` seconds
60//!   (`18_446_744_073_709_551_615`) and `999_999_999` nano seconds
61//! - parsed durations larger than the maximum duration saturate at the maximum duration
62//! - Negative durations like `"-1min"` or `"1 week ago"` are allowed
63//! - Any leading, trailing whitespace or whitespace between the number and the time unit (like in
64//!   `"1 \n sec"`) and multiple durations (like in `"1week \n 2minutes"`) is ignored and follows
65//!   the posix definition of whitespace which is:
66//!     - Space (`' '`)
67//!     - Horizontal Tab (`'\x09'`)
68//!     - Line Feed (`'\x0A'`)
69//!     - Vertical Tab (`'\x0B'`)
70//!     - Form Feed (`'\x0C'`)
71//!     - Carriage Return (`'\x0D'`)
72//!
73//! Please see also the gnu
74//! [documentation](https://www.gnu.org/software/coreutils/manual/html_node/Relative-items-in-date-strings.html)
75//! for a description of their format.
76//!
77//! # Examples
78//!
79//! ```rust
80//! use fundu_gnu::{Duration, RelativeTimeParser};
81//!
82//! let parser = RelativeTimeParser::new();
83//! assert_eq!(parser.parse("1hour"), Ok(Duration::positive(60 * 60, 0)));
84//! assert_eq!(parser.parse("minute"), Ok(Duration::positive(60, 0)));
85//! assert_eq!(
86//!     parser.parse("2 hours"),
87//!     Ok(Duration::positive(2 * 60 * 60, 0))
88//! );
89//! assert_eq!(parser.parse("second"), Ok(Duration::positive(1, 0)));
90//! assert_eq!(parser.parse("-3minutes"), Ok(Duration::negative(3 * 60, 0)));
91//! assert_eq!(
92//!     parser.parse("3 mins ago"),
93//!     Ok(Duration::negative(3 * 60, 0))
94//! );
95//! assert_eq!(
96//!     parser.parse("999sec +1day"),
97//!     Ok(Duration::positive(86_400 + 999, 0))
98//! );
99//! assert_eq!(
100//!     parser.parse("55secs500week"),
101//!     Ok(Duration::positive(55 + 500 * 7 * 24 * 60 * 60, 0))
102//! );
103//! assert_eq!(
104//!     parser.parse("300mins20secs 5hour"),
105//!     Ok(Duration::positive(300 * 60 + 20 + 5 * 60 * 60, 0))
106//! );
107//! assert_eq!(
108//!     parser.parse("123456789"),
109//!     Ok(Duration::positive(123_456_789, 0))
110//! );
111//! assert_eq!(
112//!     parser.parse("42fortnight"),
113//!     Ok(Duration::positive(42 * 2 * 7 * 24 * 60 * 60, 0))
114//! );
115//! assert_eq!(
116//!     parser.parse("yesterday"),
117//!     Ok(Duration::negative(24 * 60 * 60, 0))
118//! );
119//! assert_eq!(parser.parse("now"), Ok(Duration::positive(0, 0)));
120//! assert_eq!(
121//!     parser.parse("today -10seconds"),
122//!     Ok(Duration::negative(10, 0))
123//! );
124//! ```
125//!
126//! If parsing fuzzy units then the fuzz can cause different [`Duration`] based on the given
127//! [`DateTime`]:
128//!
129//! ```rust
130//! use fundu_gnu::{DateTime, Duration, RelativeTimeParser};
131//!
132//! let parser = RelativeTimeParser::new();
133//! let date_time = DateTime::from_gregorian_date_time(1970, 1, 1, 0, 0, 0, 0);
134//! assert_eq!(
135//!     parser.parse_with_date("+1year", Some(date_time)),
136//!     Ok(Duration::positive(365 * 86400, 0))
137//! );
138//! assert_eq!(
139//!     parser.parse_with_date("+2month", Some(date_time)),
140//!     Ok(Duration::positive((31 + 28) * 86400, 0))
141//! );
142//!
143//! // 1972 is a leap year
144//! let date_time = DateTime::from_gregorian_date_time(1972, 1, 1, 0, 0, 0, 0);
145//! assert_eq!(
146//!     parser.parse_with_date("+1year", Some(date_time)),
147//!     Ok(Duration::positive(366 * 86400, 0))
148//! );
149//! assert_eq!(
150//!     parser.parse_with_date("+2month", Some(date_time)),
151//!     Ok(Duration::positive((31 + 29) * 86400, 0))
152//! );
153//! ```
154//!
155//! If parsing fuzzy units with [`RelativeTimeParser::parse`], the [`DateTime`] of `now` in UTC +0
156//! is assumed.
157//!
158//! The global [`parse`] method does the same without the need to create a [`RelativeTimeParser`].
159//!
160//! ```rust
161//! use fundu_gnu::{parse, Duration};
162//!
163//! assert_eq!(parse("123 sec"), Ok(Duration::positive(123, 0)));
164//! assert_eq!(parse("1sec3min"), Ok(Duration::positive(1 + 3 * 60, 0)));
165//! ```
166//!
167//! Convert fundu's `Duration` into a [`std::time::Duration`]. Converting to [`chrono::Duration`] or
168//! [`time::Duration`] works the same but needs the `chrono` or `time` feature activated.
169//!
170//! ```rust
171//! use fundu_gnu::{parse, SaturatingInto};
172//!
173//! let duration = parse("123 sec").unwrap();
174//! assert_eq!(
175//!     TryInto::<std::time::Duration>::try_into(duration),
176//!     Ok(std::time::Duration::new(123, 0))
177//! );
178//!
179//! // With saturating_into the duration will saturate at the minimum and maximum of
180//! // std::time::Duration, so for negative values at std::time::Duration::ZERO and for positive values
181//! // at std::time::Duration::MAX
182//! assert_eq!(
183//!     SaturatingInto::<std::time::Duration>::saturating_into(duration),
184//!     std::time::Duration::new(123, 0)
185//! );
186//! ```
187//!
188//! [`chrono::Duration`]: https://docs.rs/chrono/latest/chrono/struct.Duration.html
189//! [`time::Duration`]: https://docs.rs/time/latest/time/struct.Duration.html
190
191#![cfg_attr(docsrs, feature(doc_auto_cfg))]
192#![doc(test(attr(warn(unused))))]
193#![doc(test(attr(allow(unused_extern_crates))))]
194#![warn(missing_docs)]
195#![warn(clippy::pedantic)]
196#![warn(clippy::default_numeric_fallback)]
197#![warn(clippy::else_if_without_else)]
198#![warn(clippy::fn_to_numeric_cast_any)]
199#![warn(clippy::get_unwrap)]
200#![warn(clippy::if_then_some_else_none)]
201#![warn(clippy::mixed_read_write_in_expression)]
202#![warn(clippy::partial_pub_fields)]
203#![warn(clippy::rest_pat_in_fully_bound_structs)]
204#![warn(clippy::str_to_string)]
205#![warn(clippy::string_to_string)]
206#![warn(clippy::todo)]
207#![warn(clippy::try_err)]
208#![warn(clippy::undocumented_unsafe_blocks)]
209#![warn(clippy::unneeded_field_pattern)]
210#![allow(clippy::must_use_candidate)]
211#![allow(clippy::return_self_not_must_use)]
212#![allow(clippy::enum_glob_use)]
213#![allow(clippy::module_name_repetitions)]
214
215macro_rules! validate {
216    ($id:ident, $min:expr, $max:expr) => {{
217        #[allow(unused_comparisons)]
218        if $id < $min || $id > $max {
219            panic!(concat!(
220                "Invalid ",
221                stringify!($id),
222                ": Valid range is ",
223                stringify!($min),
224                " <= ",
225                stringify!($id),
226                " <= ",
227                stringify!($max)
228            ));
229        }
230    }};
231
232    ($id:ident <= $max:expr) => {{
233        #[allow(unused_comparisons)]
234        if $id > $max {
235            panic!(concat!(
236                "Invalid ",
237                stringify!($id),
238                ": Valid maximum ",
239                stringify!($id),
240                " is ",
241                stringify!($max)
242            ));
243        }
244    }};
245}
246
247mod datetime;
248mod util;
249
250pub use datetime::{DateTime, JulianDay};
251use fundu_core::config::{Config, ConfigBuilder, Delimiter, NumbersLike};
252pub use fundu_core::error::{ParseError, TryFromDurationError};
253use fundu_core::parse::{
254    DurationRepr, Fract, Parser, ReprParserMultiple, ReprParserTemplate, Whole,
255};
256use fundu_core::time::TimeUnit::*;
257pub use fundu_core::time::{Duration, SaturatingInto};
258use fundu_core::time::{Multiplier, TimeUnit, TimeUnitsLike};
259#[cfg(test)]
260pub use rstest_reuse;
261use util::{to_lowercase_u64, trim_whitespace};
262
263// whitespace definition of: b' ', b'\x09', b'\x0A', b'\x0B', b'\x0C', b'\x0D'
264const DELIMITER: Delimiter = |byte| byte == b' ' || byte.wrapping_sub(9) < 5;
265
266const CONFIG: Config = ConfigBuilder::new()
267    .allow_time_unit_delimiter()
268    .allow_ago()
269    .disable_exponent()
270    .disable_infinity()
271    .allow_negative()
272    .number_is_optional()
273    .parse_multiple(None)
274    .allow_sign_delimiter()
275    .inner_delimiter(DELIMITER)
276    .outer_delimiter(DELIMITER)
277    .build();
278
279const TIME_UNITS: TimeUnits = TimeUnits {};
280const TIME_KEYWORDS: TimeKeywords = TimeKeywords {};
281const NUMERALS: Numerals = Numerals {};
282
283const SECOND_UNIT: (TimeUnit, Multiplier) = (Second, Multiplier(1, 0));
284const MINUTE_UNIT: (TimeUnit, Multiplier) = (Minute, Multiplier(1, 0));
285const HOUR_UNIT: (TimeUnit, Multiplier) = (Hour, Multiplier(1, 0));
286const DAY_UNIT: (TimeUnit, Multiplier) = (Day, Multiplier(1, 0));
287const WEEK_UNIT: (TimeUnit, Multiplier) = (Week, Multiplier(1, 0));
288const FORTNIGHT_UNIT: (TimeUnit, Multiplier) = (Week, Multiplier(2, 0));
289const MONTH_UNIT: (TimeUnit, Multiplier) = (Month, Multiplier(1, 0));
290const YEAR_UNIT: (TimeUnit, Multiplier) = (Year, Multiplier(1, 0));
291
292const PARSER: RelativeTimeParser<'static> = RelativeTimeParser::new();
293
294enum FuzzyUnit {
295    Month,
296    Year,
297}
298
299struct FuzzyTime {
300    unit: FuzzyUnit,
301    value: i64,
302}
303
304impl FuzzyTime {}
305
306enum ParseFuzzyOutput {
307    Duration(Duration),
308    FuzzyTime(FuzzyTime),
309}
310
311struct DurationReprParser<'a>(DurationRepr<'a>);
312
313impl<'a> DurationReprParser<'a> {
314    fn parse(&mut self) -> Result<Duration, ParseError> {
315        let is_negative = self.0.is_negative.unwrap_or_default();
316        let time_unit = self.0.unit.unwrap_or(self.0.default_unit);
317
318        let digits = self.0.input;
319        match (&self.0.whole, &self.0.fract) {
320            (None, None) if self.0.numeral.is_some() => {
321                let Multiplier(coefficient, exponent) =
322                    self.0.numeral.unwrap() * time_unit.multiplier() * self.0.multiplier;
323                Ok(self
324                    .0
325                    .parse_duration_with_fixed_number(coefficient, exponent))
326            }
327            (None, None) if self.0.unit.is_some() => {
328                let Multiplier(coefficient, _) = time_unit.multiplier() * self.0.multiplier;
329                let duration_is_negative = is_negative ^ coefficient.is_negative();
330                Ok(DurationRepr::calculate_duration(
331                    duration_is_negative,
332                    1,
333                    0,
334                    coefficient,
335                ))
336            }
337            (None, None) => {
338                unreachable!() // cov:excl-line
339            }
340            (None, Some(_)) if time_unit == TimeUnit::Second => Err(ParseError::InvalidInput(
341                "Fraction without a whole number".to_owned(),
342            )),
343            (Some(whole), None) => {
344                let Multiplier(coefficient, _) = time_unit.multiplier() * self.0.multiplier;
345                let duration_is_negative = is_negative ^ coefficient.is_negative();
346                let (seconds, attos) = match Whole::parse(&digits[whole.0..whole.1], None, None) {
347                    Some(seconds) => (seconds, 0),
348                    None if duration_is_negative => return Ok(Duration::MIN),
349                    None => return Ok(Duration::MAX),
350                };
351                Ok(DurationRepr::calculate_duration(
352                    duration_is_negative,
353                    seconds,
354                    attos,
355                    coefficient,
356                ))
357            }
358            (Some(_), Some(fract)) if time_unit == TimeUnit::Second && fract.is_empty() => Err(
359                ParseError::InvalidInput("Fraction without a fractional number".to_owned()),
360            ),
361            (Some(whole), Some(fract)) if time_unit == TimeUnit::Second => {
362                let Multiplier(coefficient, _) = time_unit.multiplier() * self.0.multiplier;
363                let duration_is_negative = is_negative ^ coefficient.is_negative();
364                let (seconds, attos) = match Whole::parse(&digits[whole.0..whole.1], None, None) {
365                    Some(seconds) => (seconds, Fract::parse(&digits[fract.0..fract.1], None, None)),
366                    None if duration_is_negative => return Ok(Duration::MIN),
367                    None => return Ok(Duration::MAX),
368                };
369                Ok(DurationRepr::calculate_duration(
370                    duration_is_negative,
371                    seconds,
372                    attos,
373                    coefficient,
374                ))
375            }
376            (Some(_) | None, Some(_)) => Err(ParseError::InvalidInput(
377                "Fraction only allowed together with seconds as time unit".to_owned(),
378            )),
379        }
380    }
381
382    fn parse_fuzzy(&mut self) -> Result<ParseFuzzyOutput, ParseError> {
383        let fuzzy_unit = match self.0.unit {
384            Some(Month) => FuzzyUnit::Month,
385            Some(Year) => FuzzyUnit::Year,
386            _ => return self.parse().map(ParseFuzzyOutput::Duration),
387        };
388
389        if self.0.fract.is_some() {
390            return Err(ParseError::InvalidInput(
391                "Fraction only allowed together with seconds as time unit".to_owned(),
392            ));
393        }
394
395        match self.0.whole {
396            None if self.0.numeral.is_some() => {
397                let Multiplier(coefficient, _) = self.0.numeral.unwrap() * self.0.multiplier;
398                Ok(ParseFuzzyOutput::FuzzyTime(FuzzyTime {
399                    unit: fuzzy_unit,
400                    value: if self.0.is_negative.unwrap_or_default() {
401                        coefficient.saturating_neg()
402                    } else {
403                        coefficient
404                    },
405                }))
406            }
407            // We're here when we've encountered just a time unit
408            None => Ok(ParseFuzzyOutput::FuzzyTime(FuzzyTime {
409                unit: fuzzy_unit,
410                value: if self.0.is_negative.unwrap_or_default() ^ self.0.multiplier.is_negative() {
411                    -1
412                } else {
413                    1
414                },
415            })),
416            Some(whole) => {
417                let is_negative =
418                    self.0.is_negative.unwrap_or_default() ^ self.0.multiplier.is_negative();
419                match Whole::parse(&self.0.input[whole.0..whole.1], None, None) {
420                    Some(value) => match i64::try_from(value) {
421                        Ok(value) if is_negative => Ok(ParseFuzzyOutput::FuzzyTime(FuzzyTime {
422                            unit: fuzzy_unit,
423                            value: -value,
424                        })),
425                        Ok(value) => Ok(ParseFuzzyOutput::FuzzyTime(FuzzyTime {
426                            unit: fuzzy_unit,
427                            value,
428                        })),
429                        Err(_) if is_negative => Ok(ParseFuzzyOutput::FuzzyTime(FuzzyTime {
430                            unit: fuzzy_unit,
431                            value: i64::MIN,
432                        })),
433                        Err(_) => Ok(ParseFuzzyOutput::FuzzyTime(FuzzyTime {
434                            unit: fuzzy_unit,
435                            value: i64::MAX,
436                        })),
437                    },
438                    None if is_negative => Ok(ParseFuzzyOutput::FuzzyTime(FuzzyTime {
439                        unit: fuzzy_unit,
440                        value: i64::MIN,
441                    })),
442                    None => Ok(ParseFuzzyOutput::FuzzyTime(FuzzyTime {
443                        unit: fuzzy_unit,
444                        value: i64::MAX,
445                    })),
446                }
447            }
448        }
449    }
450}
451
452/// The main gnu relative time parser
453///
454/// Note this parser can be created as const at compile time.
455///
456/// # Examples
457///
458/// ```rust
459/// use fundu_gnu::{Duration, RelativeTimeParser};
460///
461/// const PARSER: RelativeTimeParser = RelativeTimeParser::new();
462///
463/// let parser = &PARSER;
464/// assert_eq!(parser.parse("1hour"), Ok(Duration::positive(60 * 60, 0)));
465/// assert_eq!(parser.parse("minute"), Ok(Duration::positive(60, 0)));
466/// assert_eq!(
467///     parser.parse("2 hours"),
468///     Ok(Duration::positive(2 * 60 * 60, 0))
469/// );
470/// assert_eq!(parser.parse("second"), Ok(Duration::positive(1, 0)));
471/// assert_eq!(parser.parse("-3minutes"), Ok(Duration::negative(3 * 60, 0)));
472/// assert_eq!(
473///     parser.parse("3 mins ago"),
474///     Ok(Duration::negative(3 * 60, 0))
475/// );
476/// assert_eq!(
477///     parser.parse("999sec +1day"),
478///     Ok(Duration::positive(86_400 + 999, 0))
479/// );
480/// assert_eq!(
481///     parser.parse("55secs500week"),
482///     Ok(Duration::positive(55 + 500 * 7 * 24 * 60 * 60, 0))
483/// );
484/// assert_eq!(
485///     parser.parse("300mins20secs 5hour"),
486///     Ok(Duration::positive(300 * 60 + 20 + 5 * 60 * 60, 0))
487/// );
488/// assert_eq!(
489///     parser.parse("123456789"),
490///     Ok(Duration::positive(123_456_789, 0))
491/// );
492/// assert_eq!(
493///     parser.parse("42fortnight"),
494///     Ok(Duration::positive(42 * 2 * 7 * 24 * 60 * 60, 0))
495/// );
496/// assert_eq!(
497///     parser.parse("yesterday"),
498///     Ok(Duration::negative(24 * 60 * 60, 0))
499/// );
500/// assert_eq!(parser.parse("now"), Ok(Duration::positive(0, 0)));
501/// assert_eq!(
502///     parser.parse("today -10seconds"),
503///     Ok(Duration::negative(10, 0))
504/// );
505/// ```
506#[derive(Debug, Eq, PartialEq)]
507pub struct RelativeTimeParser<'a> {
508    raw: Parser<'a>,
509}
510
511impl<'a> RelativeTimeParser<'a> {
512    /// Create a new `RelativeTimeParser`
513    ///
514    /// # Examples
515    ///
516    /// ```rust
517    /// use fundu_gnu::{Duration, RelativeTimeParser};
518    ///
519    /// let parser = RelativeTimeParser::new();
520    /// assert_eq!(
521    ///     parser.parse("2hours"),
522    ///     Ok(Duration::positive(2 * 60 * 60, 0))
523    /// );
524    /// assert_eq!(parser.parse("123"), Ok(Duration::positive(123, 0)));
525    /// assert_eq!(
526    ///     parser.parse("3min +10sec"),
527    ///     Ok(Duration::positive(3 * 60 + 10, 0))
528    /// );
529    /// ```
530    pub const fn new() -> Self {
531        Self {
532            raw: Parser::with_config(CONFIG),
533        }
534    }
535    /// Parse the `source` string into a [`Duration`] relative to the date and time of `now`
536    ///
537    /// Any leading and trailing whitespace is ignored. The parser saturates at the maximum of
538    /// [`Duration::MAX`].
539    ///
540    /// # Errors
541    ///
542    /// Returns a [`ParseError`] if an error during the parsing process occurred
543    ///
544    /// # Examples
545    ///
546    /// ```rust
547    /// use fundu_gnu::{Duration, RelativeTimeParser};
548    ///
549    /// let parser = RelativeTimeParser::new();
550    /// assert_eq!(
551    ///     parser.parse("2hours"),
552    ///     Ok(Duration::positive(2 * 60 * 60, 0))
553    /// );
554    /// assert_eq!(parser.parse("12 seconds"), Ok(Duration::positive(12, 0)));
555    /// assert_eq!(
556    ///     parser.parse("123456789"),
557    ///     Ok(Duration::positive(123_456_789, 0))
558    /// );
559    /// assert_eq!(
560    ///     parser.parse("yesterday"),
561    ///     Ok(Duration::negative(24 * 60 * 60, 0))
562    /// );
563    /// ```
564    #[inline]
565    pub fn parse(&self, source: &str) -> Result<Duration, ParseError> {
566        self.parse_with_date(source, None)
567    }
568
569    /// Parse the `source` string into a [`Duration`] relative to the optionally given `date`
570    ///
571    /// If the `date` is `None`, then the system time of `now` is assumed. Time units of `year` and
572    /// `month` are parsed fuzzy since years and months are not all of equal length. Any leading and
573    /// trailing whitespace is ignored. The parser saturates at the maximum of [`Duration::MAX`].
574    ///
575    /// # Errors
576    ///
577    /// Returns a [`ParseError`] if an error during the parsing process occurred or the calculation
578    /// of the calculation of the given `date` plus the duration of the `source` string overflows.
579    ///
580    /// # Examples
581    ///
582    /// ```rust
583    /// use fundu_gnu::{DateTime, Duration, RelativeTimeParser};
584    ///
585    /// let parser = RelativeTimeParser::new();
586    /// assert_eq!(
587    ///     parser.parse_with_date("2hours", None),
588    ///     Ok(Duration::positive(2 * 60 * 60, 0))
589    /// );
590    ///
591    /// let date_time = DateTime::from_gregorian_date_time(1970, 2, 1, 0, 0, 0, 0);
592    /// assert_eq!(
593    ///     parser.parse_with_date("+1month", Some(date_time)),
594    ///     Ok(Duration::positive(28 * 86400, 0))
595    /// );
596    /// assert_eq!(
597    ///     parser.parse_with_date("+1year", Some(date_time)),
598    ///     Ok(Duration::positive(365 * 86400, 0))
599    /// );
600    ///
601    /// // 1972 is a leap year
602    /// let date_time = DateTime::from_gregorian_date_time(1972, 2, 1, 0, 0, 0, 0);
603    /// assert_eq!(
604    ///     parser.parse_with_date("+1month", Some(date_time)),
605    ///     Ok(Duration::positive(29 * 86400, 0))
606    /// );
607    /// assert_eq!(
608    ///     parser.parse_with_date("+1year", Some(date_time)),
609    ///     Ok(Duration::positive(366 * 86400, 0))
610    /// );
611    /// ```
612    pub fn parse_with_date(
613        &self,
614        source: &str,
615        date: Option<DateTime>,
616    ) -> Result<Duration, ParseError> {
617        let (years, months, duration) = self.parse_fuzzy(source)?;
618        if years == 0 && months == 0 {
619            return Ok(duration);
620        }
621
622        // Delay the costly system call to get the utc time as late as possible
623        let orig = date.unwrap_or_else(DateTime::now_utc);
624        orig.checked_add_duration(&duration)
625            .and_then(|date| {
626                date.checked_add_gregorian(years, months, 0)
627                    .and_then(|date| date.duration_since(orig))
628            })
629            .ok_or(ParseError::Overflow)
630    }
631
632    /// Parse the `source` string extracting `year` and `month` time units from the [`Duration`]
633    ///
634    /// Unlike [`RelativeTimeParser::parse`] and [`RelativeTimeParser::parse_with_date`] this method
635    /// won't interpret the parsed `year` and `month` time units but simply returns the values
636    /// parsed from the `source` string.
637    ///
638    /// The returned tuple (`years`, `months`, `Duration`) contains in the first component the
639    /// amount parsed `years` as `i64`, in the second component the parsed `months` as `i64` and in
640    /// the last component the rest of the parsed time units accumulated as [`Duration`].
641    ///
642    /// # Errors
643    ///
644    /// Returns a [`ParseError`] if an error during the parsing process occurred.
645    ///
646    /// # Examples
647    ///
648    /// ```rust
649    /// use fundu_gnu::{Duration, RelativeTimeParser};
650    ///
651    /// let parser = RelativeTimeParser::new();
652    /// assert_eq!(
653    ///     parser.parse_fuzzy("2hours"),
654    ///     Ok((0, 0, Duration::positive(2 * 60 * 60, 0)))
655    /// );
656    /// assert_eq!(
657    ///     parser.parse_fuzzy("2hours +123month -10years"),
658    ///     Ok((-10, 123, Duration::positive(2 * 60 * 60, 0)))
659    /// );
660    /// ```
661    #[allow(clippy::missing_panics_doc)]
662    pub fn parse_fuzzy(&self, source: &str) -> Result<(i64, i64, Duration), ParseError> {
663        let trimmed = trim_whitespace(source);
664
665        let mut duration = Duration::ZERO;
666        let mut years = 0i64;
667        let mut months = 0i64;
668
669        let mut parser = &mut ReprParserMultiple::new(trimmed);
670
671        loop {
672            let (duration_repr, maybe_parser) = parser.parse(
673                &self.raw.config,
674                &TIME_UNITS,
675                Some(&TIME_KEYWORDS),
676                Some(&NUMERALS),
677            )?;
678
679            match DurationReprParser(duration_repr).parse_fuzzy()? {
680                ParseFuzzyOutput::Duration(parsed_duration) => {
681                    duration = if duration.is_zero() {
682                        parsed_duration
683                    } else if parsed_duration.is_zero() {
684                        duration
685                    } else {
686                        duration.saturating_add(parsed_duration)
687                    }
688                }
689                ParseFuzzyOutput::FuzzyTime(fuzzy) => match fuzzy.unit {
690                    FuzzyUnit::Month => months = months.saturating_add(fuzzy.value),
691                    FuzzyUnit::Year => years = years.saturating_add(fuzzy.value),
692                },
693            }
694            match maybe_parser {
695                Some(p) => parser = p,
696                None => break Ok((years, months, duration)),
697            }
698        }
699    }
700}
701
702impl<'a> Default for RelativeTimeParser<'a> {
703    fn default() -> Self {
704        Self::new()
705    }
706}
707
708/// This struct is used internally to hold the time units used by gnu
709struct TimeUnits {}
710
711impl TimeUnitsLike for TimeUnits {
712    #[inline]
713    fn is_empty(&self) -> bool {
714        false
715    }
716
717    #[inline]
718    fn get(&self, identifier: &str) -> Option<(TimeUnit, Multiplier)> {
719        const SEC: [u64; 2] = [0x0000_0000_0063_6573, 0];
720        const SECS: [u64; 2] = [0x0000_0000_7363_6573, 0];
721        const SECOND: [u64; 2] = [0x0000_646E_6F63_6573, 0];
722        const SECONDS: [u64; 2] = [0x0073_646E_6F63_6573, 0];
723        const MIN: [u64; 2] = [0x0000_0000_006E_696D, 0];
724        const MINS: [u64; 2] = [0x0000_0000_736E_696D, 0];
725        const MINUTE: [u64; 2] = [0x0000_6574_756E_696D, 0];
726        const MINUTES: [u64; 2] = [0x0073_6574_756E_696D, 0];
727        const HOUR: [u64; 2] = [0x0000_0000_7275_6F68, 0];
728        const HOURS: [u64; 2] = [0x0000_0073_7275_6F68, 0];
729        const DAY: [u64; 2] = [0x0000_0000_0079_6164, 0];
730        const DAYS: [u64; 2] = [0x0000_0000_7379_6164, 0];
731        const WEEK: [u64; 2] = [0x0000_0000_6B65_6577, 0];
732        const WEEKS: [u64; 2] = [0x0000_0073_6B65_6577, 0];
733        const FORTNIGHT: [u64; 2] = [0x6867_696E_7472_6F66, 0x0000_0000_0000_0074];
734        const FORTNIGHTS: [u64; 2] = [0x6867_696E_7472_6F66, 0x0000_0000_0000_7374];
735        const MONTH: [u64; 2] = [0x0000_0068_746E_6F6D, 0];
736        const MONTHS: [u64; 2] = [0x0000_7368_746E_6F6D, 0];
737        const YEAR: [u64; 2] = [0x0000_0000_7261_6579, 0];
738        const YEARS: [u64; 2] = [0x0000_0073_7261_6579, 0];
739
740        match identifier.len() {
741            3 => match to_lowercase_u64(identifier) {
742                SEC => Some(SECOND_UNIT),
743                MIN => Some(MINUTE_UNIT),
744                DAY => Some(DAY_UNIT),
745                _ => None,
746            },
747            4 => match to_lowercase_u64(identifier) {
748                SECS => Some(SECOND_UNIT),
749                MINS => Some(MINUTE_UNIT),
750                DAYS => Some(DAY_UNIT),
751                HOUR => Some(HOUR_UNIT),
752                WEEK => Some(WEEK_UNIT),
753                YEAR => Some(YEAR_UNIT),
754                _ => None,
755            },
756            5 => match to_lowercase_u64(identifier) {
757                HOURS => Some(HOUR_UNIT),
758                WEEKS => Some(WEEK_UNIT),
759                YEARS => Some(YEAR_UNIT),
760                MONTH => Some(MONTH_UNIT),
761                _ => None,
762            },
763            6 => match to_lowercase_u64(identifier) {
764                SECOND => Some(SECOND_UNIT),
765                MINUTE => Some(MINUTE_UNIT),
766                MONTHS => Some(MONTH_UNIT),
767                _ => None,
768            },
769            7 => match to_lowercase_u64(identifier) {
770                SECONDS => Some(SECOND_UNIT),
771                MINUTES => Some(MINUTE_UNIT),
772                _ => None,
773            },
774            9 => (to_lowercase_u64(identifier) == FORTNIGHT).then_some(FORTNIGHT_UNIT),
775            10 => (to_lowercase_u64(identifier) == FORTNIGHTS).then_some(FORTNIGHT_UNIT),
776            _ => None,
777        }
778    }
779}
780
781/// This struct is used internally to hold the time keywords used by gnu
782struct TimeKeywords {}
783
784impl TimeUnitsLike for TimeKeywords {
785    #[inline]
786    fn is_empty(&self) -> bool {
787        false
788    }
789
790    #[inline]
791    fn get(&self, identifier: &str) -> Option<(TimeUnit, Multiplier)> {
792        const NOW: [u64; 2] = [0x0000_0000_0077_6F6E, 0];
793        const YESTERDAY: [u64; 2] = [0x6164_7265_7473_6579, 0x0000_0000_0000_0079];
794        const TOMORROW: [u64; 2] = [0x776F_7272_6F6D_6F74, 0];
795        const TODAY: [u64; 2] = [0x0000_0079_6164_6F74, 0];
796
797        match identifier.len() {
798            3 => (to_lowercase_u64(identifier) == NOW).then_some((TimeUnit::Day, Multiplier(0, 0))),
799            5 => {
800                (to_lowercase_u64(identifier) == TODAY).then_some((TimeUnit::Day, Multiplier(0, 0)))
801            }
802            8 => (to_lowercase_u64(identifier) == TOMORROW)
803                .then_some((TimeUnit::Day, Multiplier(1, 0))),
804            9 => (to_lowercase_u64(identifier) == YESTERDAY)
805                .then_some((TimeUnit::Day, Multiplier(-1, 0))),
806            _ => None,
807        }
808    }
809}
810
811struct Numerals {}
812
813impl NumbersLike for Numerals {
814    #[inline]
815    fn get(&self, identifier: &str) -> Option<Multiplier> {
816        const LAST: [u64; 2] = [0x0000_0000_7473_616C, 0];
817        const THIS: [u64; 2] = [0x0000_0000_7369_6874, 0];
818        const NEXT: [u64; 2] = [0x0000_0000_7478_656E, 0];
819        const FIRST: [u64; 2] = [0x0000_0074_7372_6966, 0];
820        const THIRD: [u64; 2] = [0x0000_0064_7269_6874, 0];
821        const FOURTH: [u64; 2] = [0x0000_6874_7275_6F66, 0];
822        const FIFTH: [u64; 2] = [0x0000_0068_7466_6966, 0];
823        const SIXTH: [u64; 2] = [0x0000_0068_7478_6973, 0];
824        const SEVENTH: [u64; 2] = [0x0068_746E_6576_6573, 0];
825        const EIGHTH: [u64; 2] = [0x0000_6874_6867_6965, 0];
826        const NINTH: [u64; 2] = [0x0000_0068_746E_696E, 0];
827        const TENTH: [u64; 2] = [0x0000_0068_746E_6574, 0];
828        const ELEVENTH: [u64; 2] = [0x6874_6E65_7665_6C65, 0];
829        const TWELFTH: [u64; 2] = [0x0068_7466_6C65_7774, 0];
830
831        match identifier.len() {
832            4 => match to_lowercase_u64(identifier) {
833                LAST => Some(Multiplier(-1, 0)),
834                THIS => Some(Multiplier(0, 0)),
835                NEXT => Some(Multiplier(1, 0)),
836                _ => None,
837            },
838            5 => match to_lowercase_u64(identifier) {
839                FIRST => Some(Multiplier(1, 0)),
840                THIRD => Some(Multiplier(3, 0)),
841                FIFTH => Some(Multiplier(5, 0)),
842                SIXTH => Some(Multiplier(6, 0)),
843                NINTH => Some(Multiplier(9, 0)),
844                TENTH => Some(Multiplier(10, 0)),
845                _ => None,
846            },
847            6 => match to_lowercase_u64(identifier) {
848                FOURTH => Some(Multiplier(4, 0)),
849                EIGHTH => Some(Multiplier(8, 0)),
850                _ => None,
851            },
852            7 => match to_lowercase_u64(identifier) {
853                SEVENTH => Some(Multiplier(7, 0)),
854                TWELFTH => Some(Multiplier(12, 0)),
855                _ => None,
856            },
857            8 => (ELEVENTH == to_lowercase_u64(identifier)).then_some(Multiplier(11, 0)),
858            _ => None,
859        }
860    }
861}
862
863/// Parse the `source` string into a [`Duration`]
864///
865/// Any leading and trailing whitespace is ignored. The parser saturates at the maximum of
866/// [`Duration::MAX`].
867///
868/// This method is equivalent to [`RelativeTimeParser::parse`]. See also the documentation of
869/// [`RelativeTimeParser::parse`].
870///
871/// # Errors
872///
873/// Returns a [`ParseError`] if an error during the parsing process occurred
874///
875/// # Examples
876///
877/// ```rust
878/// use fundu_gnu::{parse, Duration};
879///
880/// assert_eq!(parse("2hours"), Ok(Duration::positive(2 * 60 * 60, 0)));
881/// assert_eq!(parse("12 seconds"), Ok(Duration::positive(12, 0)));
882/// assert_eq!(parse("123456789"), Ok(Duration::positive(123_456_789, 0)));
883/// assert_eq!(parse("yesterday"), Ok(Duration::negative(24 * 60 * 60, 0)));
884/// ```
885pub fn parse(source: &str) -> Result<Duration, ParseError> {
886    PARSER.parse(source)
887}
888
889/// Parse the `source` string into a [`Duration`] relative to the optionally given `date`
890///
891/// If the `date` is `None`, then the system time of `now` is assumed. Time units of `year` and
892/// `month` are parsed fuzzy since years and months are not all of equal length. Any leading and
893/// trailing whitespace is ignored. The parser saturates at the maximum of [`Duration::MAX`].
894///
895/// This method is equivalent to [`RelativeTimeParser::parse_with_date`]. See also the documentation
896/// of [`RelativeTimeParser::parse_with_date`].
897///
898/// # Errors
899///
900/// Returns a [`ParseError`] if an error during the parsing process occurred or the calculation
901/// of the calculation of the given `date` plus the duration of the `source` string overflows.
902///
903/// # Examples
904///
905/// ```rust
906/// use fundu_gnu::{parse_with_date, DateTime, Duration};
907///
908/// assert_eq!(
909///     parse_with_date("2hours", None),
910///     Ok(Duration::positive(2 * 60 * 60, 0))
911/// );
912///
913/// let date_time = DateTime::from_gregorian_date_time(1970, 2, 1, 0, 0, 0, 0);
914/// assert_eq!(
915///     parse_with_date("+1month", Some(date_time)),
916///     Ok(Duration::positive(28 * 86400, 0))
917/// );
918/// assert_eq!(
919///     parse_with_date("+1year", Some(date_time)),
920///     Ok(Duration::positive(365 * 86400, 0))
921/// );
922///
923/// // 1972 is a leap year
924/// let date_time = DateTime::from_gregorian_date_time(1972, 2, 1, 0, 0, 0, 0);
925/// assert_eq!(
926///     parse_with_date("+1month", Some(date_time)),
927///     Ok(Duration::positive(29 * 86400, 0))
928/// );
929/// assert_eq!(
930///     parse_with_date("+1year", Some(date_time)),
931///     Ok(Duration::positive(366 * 86400, 0))
932/// );
933/// ```
934pub fn parse_with_date(source: &str, date: Option<DateTime>) -> Result<Duration, ParseError> {
935    PARSER.parse_with_date(source, date)
936}
937
938/// Parse the `source` string extracting `year` and `month` time units from the [`Duration`]
939///
940/// Unlike [`RelativeTimeParser::parse`] and [`RelativeTimeParser::parse_with_date`] this method
941/// won't interpret the parsed `year` and `month` time units but simply returns the values
942/// parsed from the `source` string.
943///
944/// The returned tuple (`years`, `months`, `Duration`) contains in the first component the
945/// amount parsed `years` as `i64`, in the second component the parsed `months` as `i64` and in
946/// the last component the rest of the parsed time units accumulated as [`Duration`].
947///
948/// This method is equivalent to [`RelativeTimeParser::parse_fuzzy`]. See also the documentation of
949/// [`RelativeTimeParser::parse_fuzzy`].
950///
951/// # Errors
952///
953/// Returns a [`ParseError`] if an error during the parsing process occurred.
954///
955/// # Examples
956///
957/// ```rust
958/// use fundu_gnu::{parse_fuzzy, Duration};
959///
960/// assert_eq!(
961///     parse_fuzzy("2hours"),
962///     Ok((0, 0, Duration::positive(2 * 60 * 60, 0)))
963/// );
964/// assert_eq!(
965///     parse_fuzzy("2hours +123month -10years"),
966///     Ok((-10, 123, Duration::positive(2 * 60 * 60, 0)))
967/// );
968/// ```
969pub fn parse_fuzzy(source: &str) -> Result<(i64, i64, Duration), ParseError> {
970    PARSER.parse_fuzzy(source)
971}
972
973#[cfg(test)]
974mod tests {
975    use super::*;
976
977    #[test]
978    fn test_relative_time_parser_new() {
979        assert_eq!(RelativeTimeParser::new(), RelativeTimeParser::default());
980    }
981
982    #[test]
983    fn test_time_units_is_empty_returns_false() {
984        assert!(!TimeUnits {}.is_empty());
985    }
986
987    #[test]
988    fn test_keywords_is_empty_returns_false() {
989        assert!(!TimeKeywords {}.is_empty());
990    }
991}