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}