Skip to main content

jiff/fmt/friendly/
parser.rs

1use jcore::bounds::Sign;
2
3use crate::{
4    error::{fmt::friendly::Error as E, ErrorContext},
5    fmt::{
6        friendly::parser_label,
7        util::{parse_temporal_fraction, DurationUnits},
8        Parsed,
9    },
10    util::parse,
11    Error, SignedDuration, Span, Unit,
12};
13
14/// A parser for Jiff's "friendly" duration format.
15///
16/// See the [module documentation](super) for more details on the precise
17/// format supported by this parser.
18///
19/// Unlike [`SpanPrinter`](super::SpanPrinter), this parser doesn't have any
20/// configuration knobs. While it may grow some in the future, the approach
21/// taken here is for the parser to support the entire grammar. That is, the
22/// parser can parse anything emitted by `SpanPrinter`. (And indeed, the
23/// parser can even handle things that the printer can't emit due to lack of
24/// configurability. For example, `1hour1m` is a valid friendly duration,
25/// but `SpanPrinter` cannot emit it due to a mixing of verbose and compact
26/// designator labels.)
27///
28/// # Advice
29///
30/// Since this parser has no configuration, there are generally only two reasons
31/// why you might want to use this type specifically:
32///
33/// 1. You need to parse from `&[u8]`.
34/// 2. You need to parse _only_ the "friendly" format.
35///
36/// Otherwise, you can use the `FromStr` implementations on both `Span` and
37/// `SignedDuration`, which automatically support the friendly format in
38/// addition to the ISO 8601 format simultaneously:
39///
40/// ```
41/// use jiff::{SignedDuration, Span, ToSpan};
42///
43/// let span: Span = "5 years, 2 months".parse()?;
44/// assert_eq!(span, 5.years().months(2).fieldwise());
45///
46/// let sdur: SignedDuration = "5 hours, 2 minutes".parse()?;
47/// assert_eq!(sdur, SignedDuration::new(5 * 60 * 60 + 2 * 60, 0));
48///
49/// # Ok::<(), Box<dyn std::error::Error>>(())
50/// ```
51///
52/// # Example
53///
54/// This example shows how to parse a `Span` directly from `&str`:
55///
56/// ```
57/// use jiff::{fmt::friendly::SpanParser, ToSpan};
58///
59/// static PARSER: SpanParser = SpanParser::new();
60///
61/// let string = "1 year, 3 months, 15:00:01.3";
62/// let span = PARSER.parse_span(string)?;
63/// assert_eq!(
64///     span,
65///     1.year().months(3).hours(15).seconds(1).milliseconds(300).fieldwise(),
66/// );
67///
68/// // Negative durations are supported too!
69/// let string = "1 year, 3 months, 15:00:01.3 ago";
70/// let span = PARSER.parse_span(string)?;
71/// assert_eq!(
72///     span,
73///     -1.year().months(3).hours(15).seconds(1).milliseconds(300).fieldwise(),
74/// );
75///
76/// # Ok::<(), Box<dyn std::error::Error>>(())
77/// ```
78#[derive(Clone, Debug, Default)]
79pub struct SpanParser {
80    _private: (),
81}
82
83impl SpanParser {
84    /// Creates a new parser for the "friendly" duration format.
85    ///
86    /// The parser returned uses the default configuration. (Although, at time
87    /// of writing, there are no available configuration options for this
88    /// parser.) This is identical to `SpanParser::default`, but it can be used
89    /// in a `const` context.
90    ///
91    /// # Example
92    ///
93    /// This example shows how to parse a `Span` directly from `&[u8]`:
94    ///
95    /// ```
96    /// use jiff::{fmt::friendly::SpanParser, ToSpan};
97    ///
98    /// static PARSER: SpanParser = SpanParser::new();
99    ///
100    /// let bytes = b"1 year 3 months 15 hours 1300ms";
101    /// let span = PARSER.parse_span(bytes)?;
102    /// assert_eq!(
103    ///     span,
104    ///     1.year().months(3).hours(15).milliseconds(1300).fieldwise(),
105    /// );
106    ///
107    /// # Ok::<(), Box<dyn std::error::Error>>(())
108    /// ```
109    #[inline]
110    pub const fn new() -> SpanParser {
111        SpanParser { _private: () }
112    }
113
114    /// Run the parser on the given string (which may be plain bytes) and,
115    /// if successful, return the parsed `Span`.
116    ///
117    /// See the [module documentation](super) for more details on the specific
118    /// grammar supported by this parser.
119    ///
120    /// # Example
121    ///
122    /// This shows a number of different duration formats that can be parsed
123    /// into a `Span`:
124    ///
125    /// ```
126    /// use jiff::{fmt::friendly::SpanParser, ToSpan};
127    ///
128    /// let spans = [
129    ///     ("40d", 40.days()),
130    ///     ("40 days", 40.days()),
131    ///     ("1y1d", 1.year().days(1)),
132    ///     ("1yr 1d", 1.year().days(1)),
133    ///     ("3d4h59m", 3.days().hours(4).minutes(59)),
134    ///     ("3 days, 4 hours, 59 minutes", 3.days().hours(4).minutes(59)),
135    ///     ("3d 4h 59m", 3.days().hours(4).minutes(59)),
136    ///     ("2h30m", 2.hours().minutes(30)),
137    ///     ("2h 30m", 2.hours().minutes(30)),
138    ///     ("1mo", 1.month()),
139    ///     ("1w", 1.week()),
140    ///     ("1 week", 1.week()),
141    ///     ("1w4d", 1.week().days(4)),
142    ///     ("1 wk 4 days", 1.week().days(4)),
143    ///     ("1m", 1.minute()),
144    ///     ("0.0021s", 2.milliseconds().microseconds(100)),
145    ///     ("0s", 0.seconds()),
146    ///     ("0d", 0.seconds()),
147    ///     ("0 days", 0.seconds()),
148    ///     (
149    ///         "1y1mo1d1h1m1.1s",
150    ///         1.year().months(1).days(1).hours(1).minutes(1).seconds(1).milliseconds(100),
151    ///     ),
152    ///     (
153    ///         "1yr 1mo 1day 1hr 1min 1.1sec",
154    ///         1.year().months(1).days(1).hours(1).minutes(1).seconds(1).milliseconds(100),
155    ///     ),
156    ///     (
157    ///         "1 year, 1 month, 1 day, 1 hour, 1 minute 1.1 seconds",
158    ///         1.year().months(1).days(1).hours(1).minutes(1).seconds(1).milliseconds(100),
159    ///     ),
160    ///     (
161    ///         "1 year, 1 month, 1 day, 01:01:01.1",
162    ///         1.year().months(1).days(1).hours(1).minutes(1).seconds(1).milliseconds(100),
163    ///     ),
164    ///     (
165    ///         "1 yr, 1 month, 1 d, 1 h, 1 min 1.1 second",
166    ///         1.year().months(1).days(1).hours(1).minutes(1).seconds(1).milliseconds(100),
167    ///     ),
168    /// ];
169    ///
170    /// static PARSER: SpanParser = SpanParser::new();
171    /// for (string, span) in spans {
172    ///     let parsed = PARSER.parse_span(string)?;
173    ///     assert_eq!(
174    ///         span.fieldwise(),
175    ///         parsed.fieldwise(),
176    ///         "result of parsing {string:?}",
177    ///     );
178    /// }
179    ///
180    /// # Ok::<(), Box<dyn std::error::Error>>(())
181    /// ```
182    #[inline]
183    pub fn parse_span<I: AsRef<[u8]>>(&self, input: I) -> Result<Span, Error> {
184        #[inline(never)]
185        fn imp(span_parser: &SpanParser, input: &[u8]) -> Result<Span, Error> {
186            let mut builder = DurationUnits::default();
187            let parsed = span_parser.parse(input, &mut builder)?;
188            let parsed = parsed.and_then(|_| builder.to_span())?;
189            parsed.into_full()
190        }
191
192        let input = input.as_ref();
193        imp(self, input).context(E::Failed)
194    }
195
196    /// Run the parser on the given string (which may be plain bytes) and,
197    /// if successful, return the parsed `SignedDuration`.
198    ///
199    /// See the [module documentation](super) for more details on the specific
200    /// grammar supported by this parser.
201    ///
202    /// # Example
203    ///
204    /// This shows a number of different duration formats that can be parsed
205    /// into a `SignedDuration`:
206    ///
207    /// ```
208    /// use jiff::{fmt::friendly::SpanParser, SignedDuration};
209    ///
210    /// let durations = [
211    ///     ("2h30m", SignedDuration::from_secs(2 * 60 * 60 + 30 * 60)),
212    ///     ("2 hrs 30 mins", SignedDuration::from_secs(2 * 60 * 60 + 30 * 60)),
213    ///     ("2 hours 30 minutes", SignedDuration::from_secs(2 * 60 * 60 + 30 * 60)),
214    ///     ("2 hrs 30 minutes", SignedDuration::from_secs(2 * 60 * 60 + 30 * 60)),
215    ///     ("2.5h", SignedDuration::from_secs(2 * 60 * 60 + 30 * 60)),
216    ///     ("1m", SignedDuration::from_mins(1)),
217    ///     ("1.5m", SignedDuration::from_secs(90)),
218    ///     ("0.0021s", SignedDuration::new(0, 2_100_000)),
219    ///     ("0s", SignedDuration::ZERO),
220    ///     ("0.000000001s", SignedDuration::from_nanos(1)),
221    /// ];
222    ///
223    /// static PARSER: SpanParser = SpanParser::new();
224    /// for (string, duration) in durations {
225    ///     let parsed = PARSER.parse_duration(string)?;
226    ///     assert_eq!(duration, parsed, "result of parsing {string:?}");
227    /// }
228    ///
229    /// # Ok::<(), Box<dyn std::error::Error>>(())
230    /// ```
231    #[inline]
232    pub fn parse_duration<I: AsRef<[u8]>>(
233        &self,
234        input: I,
235    ) -> Result<SignedDuration, Error> {
236        #[inline(never)]
237        fn imp(
238            span_parser: &SpanParser,
239            input: &[u8],
240        ) -> Result<SignedDuration, Error> {
241            let mut builder = DurationUnits::default();
242            let parsed = span_parser.parse(input, &mut builder)?;
243            let parsed = parsed.and_then(|_| builder.to_signed_duration())?;
244            parsed.into_full()
245        }
246
247        let input = input.as_ref();
248        imp(self, input).context(E::Failed)
249    }
250
251    /// Run the parser on the given string (which may be plain bytes) and,
252    /// if successful, return the parsed `std::time::Duration`.
253    ///
254    /// See the [module documentation](super) for more details on the specific
255    /// grammar supported by this parser.
256    ///
257    /// # Example
258    ///
259    /// This shows a number of different duration formats that can be parsed
260    /// into a `std::time::Duration`:
261    ///
262    /// ```
263    /// use std::time::Duration;
264    ///
265    /// use jiff::fmt::friendly::SpanParser;
266    ///
267    /// let durations = [
268    ///     ("2h30m", Duration::from_secs(2 * 60 * 60 + 30 * 60)),
269    ///     ("2 hrs 30 mins", Duration::from_secs(2 * 60 * 60 + 30 * 60)),
270    ///     ("2 hours 30 minutes", Duration::from_secs(2 * 60 * 60 + 30 * 60)),
271    ///     ("2 hrs 30 minutes", Duration::from_secs(2 * 60 * 60 + 30 * 60)),
272    ///     ("2.5h", Duration::from_secs(2 * 60 * 60 + 30 * 60)),
273    ///     ("1m", Duration::from_secs(1 * 60)),
274    ///     ("1.5m", Duration::from_secs(90)),
275    ///     ("0.0021s", Duration::new(0, 2_100_000)),
276    ///     ("0s", Duration::ZERO),
277    ///     ("0.000000001s", Duration::from_nanos(1)),
278    /// ];
279    ///
280    /// static PARSER: SpanParser = SpanParser::new();
281    /// for (string, duration) in durations {
282    ///     let parsed = PARSER.parse_unsigned_duration(string)?;
283    ///     assert_eq!(duration, parsed, "result of parsing {string:?}");
284    /// }
285    ///
286    /// # Ok::<(), Box<dyn std::error::Error>>(())
287    /// ```
288    #[inline]
289    pub fn parse_unsigned_duration<I: AsRef<[u8]>>(
290        &self,
291        input: I,
292    ) -> Result<core::time::Duration, Error> {
293        #[inline(never)]
294        fn imp(
295            span_parser: &SpanParser,
296            input: &[u8],
297        ) -> Result<core::time::Duration, Error> {
298            let mut builder = DurationUnits::default();
299            let parsed = span_parser.parse(input, &mut builder)?;
300            let parsed =
301                parsed.and_then(|_| builder.to_unsigned_duration())?;
302            let d = parsed.value;
303            parsed.into_full_with(format_args!("{d:?}"))
304        }
305
306        let input = input.as_ref();
307        imp(self, input).context(E::Failed)
308    }
309
310    #[cfg_attr(feature = "perf-inline", inline(always))]
311    fn parse<'i>(
312        &self,
313        input: &'i [u8],
314        builder: &mut DurationUnits,
315    ) -> Result<Parsed<'i, ()>, Error> {
316        if input.is_empty() {
317            return Err(Error::from(E::Empty));
318        }
319        // Guard prefix sign parsing to avoid the function call, which is
320        // marked unlineable to keep the fast path tighter.
321        let (sign, input) =
322            if !input.first().map_or(false, |&b| matches!(b, b'+' | b'-')) {
323                (None, input)
324            } else {
325                let Parsed { value: sign, input } =
326                    self.parse_prefix_sign(input);
327                (sign, input)
328            };
329
330        let Parsed { value, input } = self.parse_unit_value(input)?;
331        let Some(first_unit_value) = value else {
332            return Err(Error::from(E::ExpectedIntegerAfterSign));
333        };
334
335        let Parsed { input, .. } =
336            self.parse_duration_units(input, first_unit_value, builder)?;
337
338        // As with the prefix sign parsing, guard it to avoid calling the
339        // function.
340        let (sign, input) = if !input.first().map_or(false, is_whitespace) {
341            (sign.unwrap_or(Sign::Positive), input)
342        } else {
343            let parsed = self.parse_suffix_sign(sign, input)?;
344            (parsed.value, parsed.input)
345        };
346        builder.set_sign(sign);
347        Ok(Parsed { value: (), input })
348    }
349
350    #[cfg_attr(feature = "perf-inline", inline(always))]
351    fn parse_duration_units<'i>(
352        &self,
353        mut input: &'i [u8],
354        first_unit_value: u64,
355        builder: &mut DurationUnits,
356    ) -> Result<Parsed<'i, ()>, Error> {
357        let mut parsed_any_after_comma = true;
358        let mut value = first_unit_value;
359        loop {
360            let parsed = self.parse_hms_maybe(input, value)?;
361            input = parsed.input;
362            if let Some(hms) = parsed.value {
363                builder.set_hms(
364                    hms.hour,
365                    hms.minute,
366                    hms.second,
367                    hms.fraction,
368                )?;
369                break;
370            }
371
372            let fraction =
373                if input.first().map_or(false, |&b| b == b'.' || b == b',') {
374                    let parsed = parse_temporal_fraction(input)?;
375                    input = parsed.input;
376                    parsed.value
377                } else {
378                    None
379                };
380
381            // Eat any optional whitespace between the unit value and label.
382            input = self.parse_optional_whitespace(input).input;
383
384            // Parse the actual unit label/designator.
385            let parsed = self.parse_unit_designator(input)?;
386            input = parsed.input;
387            let unit = parsed.value;
388
389            // A comma is allowed to immediately follow the designator.
390            // Since this is a rarer case, we guard it with a check to see
391            // if the comma is there and only then call the function (which is
392            // marked unlineable to try and keep the hot path tighter).
393            if input.first().map_or(false, |&b| b == b',') {
394                input = self.parse_optional_comma(input)?.input;
395                parsed_any_after_comma = false;
396            }
397
398            builder.set_unit_value(unit, value)?;
399            if let Some(fraction) = fraction {
400                builder.set_fraction(fraction)?;
401                // Once we see a fraction, we are done. We don't permit parsing
402                // any more units. That is, a fraction can only occur on the
403                // lowest unit of time.
404                break;
405            }
406
407            // Eat any optional whitespace after the designator (or comma) and
408            // before the next unit value. But if we don't see a unit value,
409            // we don't eat the whitespace.
410            let after_whitespace = self.parse_optional_whitespace(input).input;
411            let parsed = self.parse_unit_value(after_whitespace)?;
412            value = match parsed.value {
413                None => break,
414                Some(value) => value,
415            };
416            input = parsed.input;
417            parsed_any_after_comma = true;
418        }
419        if !parsed_any_after_comma {
420            return Err(Error::from(E::ExpectedOneMoreUnitAfterComma));
421        }
422        Ok(Parsed { value: (), input })
423    }
424
425    /// This possibly parses a `HH:MM:SS[.fraction]`.
426    ///
427    /// This expects that a unit value has been parsed and looks for a `:`
428    /// at `input[0]`. If `:` is found, then this proceeds to parse HMS.
429    /// Otherwise, a `None` value is returned.
430    #[cfg_attr(feature = "perf-inline", inline(always))]
431    fn parse_hms_maybe<'i>(
432        &self,
433        input: &'i [u8],
434        hour: u64,
435    ) -> Result<Parsed<'i, Option<HMS>>, Error> {
436        let Some((&first, tail)) = input.split_first() else {
437            return Ok(Parsed { input, value: None });
438        };
439        if first != b':' {
440            return Ok(Parsed { input, value: None });
441        }
442        let Parsed { input, value } = self.parse_hms(tail, hour)?;
443        Ok(Parsed { input, value: Some(value) })
444    }
445
446    /// This parses a `HH:MM:SS[.fraction]` when it is known/expected to be
447    /// present.
448    ///
449    /// This is also marked as non-inlined since we expect this to be a
450    /// less common case. Where as `parse_hms_maybe` is called unconditionally
451    /// to check to see if the HMS should be parsed.
452    ///
453    /// This assumes that the beginning of `input` immediately follows the
454    /// first `:` in `HH:MM:SS[.fraction]`.
455    #[inline(never)]
456    fn parse_hms<'i>(
457        &self,
458        input: &'i [u8],
459        hour: u64,
460    ) -> Result<Parsed<'i, HMS>, Error> {
461        let Parsed { input, value } = self.parse_unit_value(input)?;
462        let minute = value.ok_or(E::ExpectedMinuteAfterHour)?;
463
464        let (&first, input) =
465            input.split_first().ok_or(E::ExpectedColonAfterMinute)?;
466        if first != b':' {
467            return Err(Error::from(E::ExpectedColonAfterMinute));
468        }
469
470        let Parsed { input, value } = self.parse_unit_value(input)?;
471        let second = value.ok_or(E::ExpectedSecondAfterMinute)?;
472        let (fraction, input) =
473            if input.first().map_or(false, |&b| b == b'.' || b == b',') {
474                let parsed = parse_temporal_fraction(input)?;
475                (parsed.value, parsed.input)
476            } else {
477                (None, input)
478            };
479        let hms = HMS { hour, minute, second, fraction };
480        Ok(Parsed { input, value: hms })
481    }
482
483    /// Parsed a unit value, i.e., an integer.
484    ///
485    /// If no digits (`[0-9]`) were found at the current position of the parser
486    /// then `None` is returned. This means, for example, that parsing a
487    /// duration should stop.
488    ///
489    /// Note that this is safe to call on untrusted input. It will not attempt
490    /// to consume more input than could possibly fit into a parsed integer.
491    ///
492    /// Since this returns a `u64`, it is possible that an integer that cannot
493    /// fit into an `i64` is returned. Callers should handle this. (Indeed,
494    /// `DurationUnits` handles this case.)
495    #[cfg_attr(feature = "perf-inline", inline(always))]
496    fn parse_unit_value<'i>(
497        &self,
498        input: &'i [u8],
499    ) -> Result<Parsed<'i, Option<u64>>, Error> {
500        let (value, input) = parse::u64_prefix(input)?;
501        Ok(Parsed { value, input })
502    }
503
504    /// Parse a unit designator, e.g., `years` or `nano`.
505    ///
506    /// If no designator could be found, including if the given `input` is
507    /// empty, then this return an error.
508    ///
509    /// This does not attempt to handle leading or trailing whitespace.
510    #[cfg_attr(feature = "perf-inline", inline(always))]
511    fn parse_unit_designator<'i>(
512        &self,
513        input: &'i [u8],
514    ) -> Result<Parsed<'i, Unit>, Error> {
515        let (unit, len) =
516            parser_label::find(input).ok_or(E::ExpectedUnitSuffix)?;
517        Ok(Parsed { value: unit, input: &input[len..] })
518    }
519
520    /// Parses an optional prefix sign from the given input.
521    ///
522    /// A prefix sign is either a `+` or a `-`. If neither are found, then
523    /// `None` is returned.
524    #[inline(never)]
525    fn parse_prefix_sign<'i>(
526        &self,
527        input: &'i [u8],
528    ) -> Parsed<'i, Option<Sign>> {
529        let Some(sign) = input.first().copied() else {
530            return Parsed { value: None, input };
531        };
532        let sign = if sign == b'+' {
533            Sign::Positive
534        } else if sign == b'-' {
535            Sign::Negative
536        } else {
537            return Parsed { value: None, input };
538        };
539        Parsed { value: Some(sign), input: &input[1..] }
540    }
541
542    /// Parses an optional suffix sign from the given input.
543    ///
544    /// This requires, as input, the result of parsing a prefix sign since this
545    /// will return an error if both a prefix and a suffix sign were found.
546    ///
547    /// A suffix sign is the string `ago`. Any other string means that there is
548    /// no suffix sign. This will also look for mandatory whitespace and eat
549    /// any additional optional whitespace. i.e., This should be called
550    /// immediately after parsing the last unit designator/label.
551    ///
552    /// Regardless of whether a prefix or suffix sign was found, a definitive
553    /// sign is returned. (When there's no prefix or suffix sign, then the sign
554    /// returned is positive.)
555    #[inline(never)]
556    fn parse_suffix_sign<'i>(
557        &self,
558        prefix_sign: Option<Sign>,
559        mut input: &'i [u8],
560    ) -> Result<Parsed<'i, Sign>, Error> {
561        if !input.first().map_or(false, is_whitespace) {
562            let sign = prefix_sign.unwrap_or(Sign::Positive);
563            return Ok(Parsed { value: sign, input });
564        }
565        // Eat any additional whitespace we find before looking for 'ago'.
566        input = self.parse_optional_whitespace(&input[1..]).input;
567        let (suffix_sign, input) =
568            if let Some(tail) = input.strip_prefix(b"ago") {
569                (Some(Sign::Negative), tail)
570            } else {
571                (None, input)
572            };
573        let sign = match (prefix_sign, suffix_sign) {
574            (Some(_), Some(_)) => {
575                return Err(Error::from(E::ExpectedOneSign));
576            }
577            (Some(sign), None) => sign,
578            (None, Some(sign)) => sign,
579            (None, None) => Sign::Positive,
580        };
581        Ok(Parsed { value: sign, input })
582    }
583
584    /// Parses an optional comma following a unit designator.
585    ///
586    /// If a comma is seen, then it is mandatory that it be followed by
587    /// whitespace.
588    ///
589    /// This also takes care to provide a custom error message if the end of
590    /// input is seen after a comma.
591    ///
592    /// If `input` doesn't start with a comma, then this is a no-op.
593    #[inline(never)]
594    fn parse_optional_comma<'i>(
595        &self,
596        input: &'i [u8],
597    ) -> Result<Parsed<'i, ()>, Error> {
598        let Some((&first, tail)) = input.split_first() else {
599            return Ok(Parsed { value: (), input });
600        };
601        if first != b',' {
602            return Ok(Parsed { value: (), input });
603        }
604
605        let (second, input) = tail
606            .split_first()
607            .ok_or(E::ExpectedWhitespaceAfterCommaEndOfInput)?;
608        if !is_whitespace(second) {
609            return Err(Error::from(E::ExpectedWhitespaceAfterComma {
610                byte: *second,
611            }));
612        }
613        Ok(Parsed { value: (), input })
614    }
615
616    /// Parses zero or more bytes of ASCII whitespace.
617    #[cfg_attr(feature = "perf-inline", inline(always))]
618    fn parse_optional_whitespace<'i>(
619        &self,
620        mut input: &'i [u8],
621    ) -> Parsed<'i, ()> {
622        while input.first().map_or(false, is_whitespace) {
623            input = &input[1..];
624        }
625        Parsed { value: (), input }
626    }
627}
628
629/// A type that represents the parsed components of `HH:MM:SS[.fraction]`.
630#[derive(Debug)]
631struct HMS {
632    hour: u64,
633    minute: u64,
634    second: u64,
635    fraction: Option<u32>,
636}
637
638/// Returns true if the byte is ASCII whitespace.
639#[cfg_attr(feature = "perf-inline", inline(always))]
640fn is_whitespace(byte: &u8) -> bool {
641    matches!(*byte, b' ' | b'\t' | b'\n' | b'\r' | b'\x0C')
642}
643
644#[cfg(feature = "alloc")]
645#[cfg(test)]
646mod tests {
647    use super::*;
648
649    #[test]
650    fn parse_span_basic() {
651        let p = |s: &str| SpanParser::new().parse_span(s).unwrap();
652
653        insta::assert_snapshot!(p("5 years"), @"P5Y");
654        insta::assert_snapshot!(p("5 years 4 months"), @"P5Y4M");
655        insta::assert_snapshot!(p("5 years 4 months 3 hours"), @"P5Y4MT3H");
656        insta::assert_snapshot!(p("5 years, 4 months, 3 hours"), @"P5Y4MT3H");
657
658        insta::assert_snapshot!(p("01:02:03"), @"PT1H2M3S");
659        insta::assert_snapshot!(p("5 days 01:02:03"), @"P5DT1H2M3S");
660        // This is Python's `str(timedelta)` format!
661        insta::assert_snapshot!(p("5 days, 01:02:03"), @"P5DT1H2M3S");
662        insta::assert_snapshot!(p("3yrs 5 days 01:02:03"), @"P3Y5DT1H2M3S");
663        insta::assert_snapshot!(p("3yrs 5 days, 01:02:03"), @"P3Y5DT1H2M3S");
664        insta::assert_snapshot!(
665            p("3yrs 5 days, 01:02:03.123456789"),
666            @"P3Y5DT1H2M3.123456789S",
667        );
668        insta::assert_snapshot!(p("999:999:999"), @"PT999H999M999S");
669    }
670
671    #[test]
672    fn parse_span_fractional() {
673        let p = |s: &str| SpanParser::new().parse_span(s).unwrap();
674
675        insta::assert_snapshot!(p("1.5hrs"), @"PT1H30M");
676        insta::assert_snapshot!(p("1.5mins"), @"PT1M30S");
677        insta::assert_snapshot!(p("1.5secs"), @"PT1.5S");
678        insta::assert_snapshot!(p("1.5msecs"), @"PT0.0015S");
679        insta::assert_snapshot!(p("1.5µsecs"), @"PT0.0000015S");
680        insta::assert_snapshot!(p("-0.5secs"), @"-PT0.5S");
681        insta::assert_snapshot!(p("0.5secs ago"), @"-PT0.5S");
682
683        insta::assert_snapshot!(p("1d 1.5hrs"), @"P1DT1H30M");
684        insta::assert_snapshot!(p("1h 1.5mins"), @"PT1H1M30S");
685        insta::assert_snapshot!(p("1m 1.5secs"), @"PT1M1.5S");
686        insta::assert_snapshot!(p("1s 1.5msecs"), @"PT1.0015S");
687        insta::assert_snapshot!(p("1ms 1.5µsecs"), @"PT0.0010015S");
688
689        insta::assert_snapshot!(p("1s2000ms"), @"PT3S");
690    }
691
692    #[test]
693    fn parse_span_boundaries() {
694        let p = |s: &str| SpanParser::new().parse_span(s).unwrap();
695
696        insta::assert_snapshot!(p("19998 years"), @"P19998Y");
697        insta::assert_snapshot!(p("19998 years ago"), @"-P19998Y");
698        insta::assert_snapshot!(p("239976 months"), @"P239976M");
699        insta::assert_snapshot!(p("239976 months ago"), @"-P239976M");
700        insta::assert_snapshot!(p("1043497 weeks"), @"P1043497W");
701        insta::assert_snapshot!(p("1043497 weeks ago"), @"-P1043497W");
702        insta::assert_snapshot!(p("7304484 days"), @"P7304484D");
703        insta::assert_snapshot!(p("7304484 days ago"), @"-P7304484D");
704        insta::assert_snapshot!(p("175307616 hours"), @"PT175307616H");
705        insta::assert_snapshot!(p("175307616 hours ago"), @"-PT175307616H");
706        insta::assert_snapshot!(p("10518456960 minutes"), @"PT10518456960M");
707        insta::assert_snapshot!(p("10518456960 minutes ago"), @"-PT10518456960M");
708        insta::assert_snapshot!(p("631107417600 seconds"), @"PT631107417600S");
709        insta::assert_snapshot!(p("631107417600 seconds ago"), @"-PT631107417600S");
710        insta::assert_snapshot!(p("631107417600000 milliseconds"), @"PT631107417600S");
711        insta::assert_snapshot!(p("631107417600000 milliseconds ago"), @"-PT631107417600S");
712        insta::assert_snapshot!(p("631107417600000000 microseconds"), @"PT631107417600S");
713        insta::assert_snapshot!(p("631107417600000000 microseconds ago"), @"-PT631107417600S");
714        insta::assert_snapshot!(p("9223372036854775807 nanoseconds"), @"PT9223372036.854775807S");
715        insta::assert_snapshot!(p("9223372036854775807 nanoseconds ago"), @"-PT9223372036.854775807S");
716
717        insta::assert_snapshot!(p("175307617 hours"), @"PT175307616H60M");
718        insta::assert_snapshot!(p("175307617 hours ago"), @"-PT175307616H60M");
719        insta::assert_snapshot!(p("10518456961 minutes"), @"PT10518456960M60S");
720        insta::assert_snapshot!(p("10518456961 minutes ago"), @"-PT10518456960M60S");
721        insta::assert_snapshot!(p("631107417601 seconds"), @"PT631107417601S");
722        insta::assert_snapshot!(p("631107417601 seconds ago"), @"-PT631107417601S");
723        insta::assert_snapshot!(p("631107417600001 milliseconds"), @"PT631107417600.001S");
724        insta::assert_snapshot!(p("631107417600001 milliseconds ago"), @"-PT631107417600.001S");
725        insta::assert_snapshot!(p("631107417600000001 microseconds"), @"PT631107417600.000001S");
726        insta::assert_snapshot!(p("631107417600000001 microseconds ago"), @"-PT631107417600.000001S");
727        // We don't include nanoseconds here, because that will fail to
728        // parse due to overflowing i64.
729    }
730
731    #[test]
732    fn err_span_basic() {
733        let p = |s: &str| SpanParser::new().parse_span(s).unwrap_err();
734
735        insta::assert_snapshot!(
736            p(""),
737            @r#"failed to parse input in the "friendly" duration format: an empty string is not valid"#,
738        );
739        insta::assert_snapshot!(
740            p(" "),
741            @r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#,
742        );
743        insta::assert_snapshot!(
744            p("a"),
745            @r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#,
746        );
747        insta::assert_snapshot!(
748            p("2 months 1 year"),
749            @r#"failed to parse input in the "friendly" duration format: found value with unit year after unit month, but units must be written from largest to smallest (and they can't be repeated)"#,
750        );
751        insta::assert_snapshot!(
752            p("1 year 1 mont"),
753            @r#"failed to parse input in the "friendly" duration format: parsed value 'P1Y1M', but unparsed input "nt" remains (expected no unparsed input)"#,
754        );
755        insta::assert_snapshot!(
756            p("2 months,"),
757            @r#"failed to parse input in the "friendly" duration format: expected whitespace after comma, but found end of input"#,
758        );
759        insta::assert_snapshot!(
760            p("2 months, "),
761            @r#"failed to parse input in the "friendly" duration format: found comma at the end of duration, but a comma indicates at least one more unit follows"#,
762        );
763        insta::assert_snapshot!(
764            p("2 months ,"),
765            @r#"failed to parse input in the "friendly" duration format: parsed value 'P2M', but unparsed input "," remains (expected no unparsed input)"#,
766        );
767    }
768
769    #[test]
770    fn err_span_sign() {
771        let p = |s: &str| SpanParser::new().parse_span(s).unwrap_err();
772
773        insta::assert_snapshot!(
774            p("1yago"),
775            @r#"failed to parse input in the "friendly" duration format: parsed value 'P1Y', but unparsed input "ago" remains (expected no unparsed input)"#,
776        );
777        insta::assert_snapshot!(
778            p("1 year 1 monthago"),
779            @r#"failed to parse input in the "friendly" duration format: parsed value 'P1Y1M', but unparsed input "ago" remains (expected no unparsed input)"#,
780        );
781        insta::assert_snapshot!(
782            p("+1 year 1 month ago"),
783            @r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#,
784        );
785        insta::assert_snapshot!(
786            p("-1 year 1 month ago"),
787            @r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#,
788        );
789    }
790
791    #[test]
792    fn err_span_overflow_fraction() {
793        let p = |s: &str| SpanParser::new().parse_span(s).unwrap();
794        let pe = |s: &str| SpanParser::new().parse_span(s).unwrap_err();
795
796        insta::assert_snapshot!(
797            // One fewer micro, and this parses okay. The error occurs because
798            // the maximum number of microseconds is subtracted off, and we're
799            // left over with a value that overflows an i64.
800            pe("640330789636854776 micros"),
801            @r#"failed to parse input in the "friendly" duration format: failed to set value for microsecond unit on span: failed to set nanosecond value from fractional component"#,
802        );
803        // one fewer is okay
804        insta::assert_snapshot!(
805            p("640330789636854775 micros"),
806            @"PT640330789636.854775S"
807        );
808
809        insta::assert_snapshot!(
810            // This is like the test above, but actually exercises a slightly
811            // different error path by using an explicit fraction. Here, if
812            // we had x.807 micros, it would parse successfully.
813            pe("640330789636854775.808 micros"),
814            @r#"failed to parse input in the "friendly" duration format: failed to set nanosecond value from fractional component"#,
815        );
816        // one fewer is okay
817        insta::assert_snapshot!(
818            p("640330789636854775.807 micros"),
819            @"PT640330789636.854775807S"
820        );
821    }
822
823    #[test]
824    fn err_span_overflow_units() {
825        let p = |s: &str| SpanParser::new().parse_span(s).unwrap_err();
826
827        insta::assert_snapshot!(
828            p("19999 years"),
829            @r#"failed to parse input in the "friendly" duration format: failed to set value for year unit on span: parameter 'years' is not in the required range of -19998..=19998"#,
830        );
831        insta::assert_snapshot!(
832            p("19999 years ago"),
833            @r#"failed to parse input in the "friendly" duration format: failed to set value for year unit on span: parameter 'years' is not in the required range of -19998..=19998"#,
834        );
835
836        insta::assert_snapshot!(
837            p("239977 months"),
838            @r#"failed to parse input in the "friendly" duration format: failed to set value for month unit on span: parameter 'months' is not in the required range of -239976..=239976"#,
839        );
840        insta::assert_snapshot!(
841            p("239977 months ago"),
842            @r#"failed to parse input in the "friendly" duration format: failed to set value for month unit on span: parameter 'months' is not in the required range of -239976..=239976"#,
843        );
844
845        insta::assert_snapshot!(
846            p("1043498 weeks"),
847            @r#"failed to parse input in the "friendly" duration format: failed to set value for week unit on span: parameter 'weeks' is not in the required range of -1043497..=1043497"#,
848        );
849        insta::assert_snapshot!(
850            p("1043498 weeks ago"),
851            @r#"failed to parse input in the "friendly" duration format: failed to set value for week unit on span: parameter 'weeks' is not in the required range of -1043497..=1043497"#,
852        );
853
854        insta::assert_snapshot!(
855            p("7304485 days"),
856            @r#"failed to parse input in the "friendly" duration format: failed to set value for day unit on span: parameter 'days' is not in the required range of -7304484..=7304484"#,
857        );
858        insta::assert_snapshot!(
859            p("7304485 days ago"),
860            @r#"failed to parse input in the "friendly" duration format: failed to set value for day unit on span: parameter 'days' is not in the required range of -7304484..=7304484"#,
861        );
862
863        insta::assert_snapshot!(
864            p("9223372036854775808 nanoseconds"),
865            @r#"failed to parse input in the "friendly" duration format: value for nanoseconds is too big (or small) to fit into a signed 64-bit integer"#,
866        );
867        insta::assert_snapshot!(
868            p("9223372036854775808 nanoseconds ago"),
869            @r#"failed to parse input in the "friendly" duration format: failed to set value for nanosecond unit on span: parameter 'nanoseconds' is not in the required range of -9223372036854775807..=9223372036854775807"#,
870        );
871    }
872
873    #[test]
874    fn err_span_fraction() {
875        let p = |s: &str| SpanParser::new().parse_span(s).unwrap_err();
876
877        insta::assert_snapshot!(
878            p("1.5 years"),
879            @r#"failed to parse input in the "friendly" duration format: fractional years are not supported"#,
880        );
881        insta::assert_snapshot!(
882            p("1.5 nanos"),
883            @r#"failed to parse input in the "friendly" duration format: fractional nanoseconds are not supported"#,
884        );
885    }
886
887    #[test]
888    fn err_span_hms() {
889        let p = |s: &str| SpanParser::new().parse_span(s).unwrap_err();
890
891        insta::assert_snapshot!(
892            p("05:"),
893            @r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse minute following hour"#,
894        );
895        insta::assert_snapshot!(
896            p("05:06"),
897            @r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse `:` following minute"#,
898        );
899        insta::assert_snapshot!(
900            p("05:06:"),
901            @r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse second following minute"#,
902        );
903        insta::assert_snapshot!(
904            p("2 hours, 05:06:07"),
905            @r#"failed to parse input in the "friendly" duration format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#,
906        );
907    }
908
909    #[test]
910    fn parse_signed_duration_basic() {
911        let p = |s: &str| SpanParser::new().parse_duration(s).unwrap();
912
913        insta::assert_snapshot!(p("1 hour, 2 minutes, 3 seconds"), @"PT1H2M3S");
914        insta::assert_snapshot!(p("01:02:03"), @"PT1H2M3S");
915        insta::assert_snapshot!(p("999:999:999"), @"PT1015H55M39S");
916    }
917
918    #[test]
919    fn parse_signed_duration_negate() {
920        let p = |s: &str| SpanParser::new().parse_duration(s).unwrap();
921        let perr = |s: &str| SpanParser::new().parse_duration(s).unwrap_err();
922
923        insta::assert_snapshot!(
924            p("9223372036854775807s"),
925            @"PT2562047788015215H30M7S",
926        );
927        insta::assert_snapshot!(
928            perr("9223372036854775808s"),
929            @r#"failed to parse input in the "friendly" duration format: value for seconds is too big (or small) to fit into a signed 64-bit integer"#,
930        );
931        insta::assert_snapshot!(
932            p("-9223372036854775808s"),
933            @"-PT2562047788015215H30M8S",
934        );
935    }
936
937    #[test]
938    fn parse_signed_duration_fractional() {
939        let p = |s: &str| SpanParser::new().parse_duration(s).unwrap();
940
941        insta::assert_snapshot!(p("1.5hrs"), @"PT1H30M");
942        insta::assert_snapshot!(p("1.5mins"), @"PT1M30S");
943        insta::assert_snapshot!(p("1.5secs"), @"PT1.5S");
944        insta::assert_snapshot!(p("1.5msecs"), @"PT0.0015S");
945        insta::assert_snapshot!(p("1.5µsecs"), @"PT0.0000015S");
946        insta::assert_snapshot!(p("-0.5secs"), @"-PT0.5S");
947        insta::assert_snapshot!(p("0.5secs ago"), @"-PT0.5S");
948
949        insta::assert_snapshot!(p("1h 1.5mins"), @"PT1H1M30S");
950        insta::assert_snapshot!(p("1m 1.5secs"), @"PT1M1.5S");
951        insta::assert_snapshot!(p("1s 1.5msecs"), @"PT1.0015S");
952        insta::assert_snapshot!(p("1ms 1.5µsecs"), @"PT0.0010015S");
953
954        insta::assert_snapshot!(p("1s2000ms"), @"PT3S");
955    }
956
957    #[test]
958    fn parse_signed_duration_boundaries() {
959        let p = |s: &str| SpanParser::new().parse_duration(s).unwrap();
960        let pe = |s: &str| SpanParser::new().parse_duration(s).unwrap_err();
961
962        insta::assert_snapshot!(p("175307616 hours"), @"PT175307616H");
963        insta::assert_snapshot!(p("175307616 hours ago"), @"-PT175307616H");
964        insta::assert_snapshot!(p("10518456960 minutes"), @"PT175307616H");
965        insta::assert_snapshot!(p("10518456960 minutes ago"), @"-PT175307616H");
966        insta::assert_snapshot!(p("631107417600 seconds"), @"PT175307616H");
967        insta::assert_snapshot!(p("631107417600 seconds ago"), @"-PT175307616H");
968        insta::assert_snapshot!(p("631107417600000 milliseconds"), @"PT175307616H");
969        insta::assert_snapshot!(p("631107417600000 milliseconds ago"), @"-PT175307616H");
970        insta::assert_snapshot!(p("631107417600000000 microseconds"), @"PT175307616H");
971        insta::assert_snapshot!(p("631107417600000000 microseconds ago"), @"-PT175307616H");
972        insta::assert_snapshot!(p("9223372036854775807 nanoseconds"), @"PT2562047H47M16.854775807S");
973        insta::assert_snapshot!(p("9223372036854775807 nanoseconds ago"), @"-PT2562047H47M16.854775807S");
974
975        insta::assert_snapshot!(p("175307617 hours"), @"PT175307617H");
976        insta::assert_snapshot!(p("175307617 hours ago"), @"-PT175307617H");
977        insta::assert_snapshot!(p("10518456961 minutes"), @"PT175307616H1M");
978        insta::assert_snapshot!(p("10518456961 minutes ago"), @"-PT175307616H1M");
979        insta::assert_snapshot!(p("631107417601 seconds"), @"PT175307616H1S");
980        insta::assert_snapshot!(p("631107417601 seconds ago"), @"-PT175307616H1S");
981        insta::assert_snapshot!(p("631107417600001 milliseconds"), @"PT175307616H0.001S");
982        insta::assert_snapshot!(p("631107417600001 milliseconds ago"), @"-PT175307616H0.001S");
983        insta::assert_snapshot!(p("631107417600000001 microseconds"), @"PT175307616H0.000001S");
984        insta::assert_snapshot!(p("631107417600000001 microseconds ago"), @"-PT175307616H0.000001S");
985        // We don't include nanoseconds here, because that will fail to
986        // parse due to overflowing i64.
987
988        // The above were copied from the corresponding `Span` test, which has
989        // tighter limits on components. But a `SignedDuration` supports the
990        // full range of `i64` seconds.
991        insta::assert_snapshot!(p("2562047788015215hours"), @"PT2562047788015215H");
992        insta::assert_snapshot!(p("-2562047788015215hours"), @"-PT2562047788015215H");
993        insta::assert_snapshot!(
994            pe("2562047788015216hrs"),
995            @r#"failed to parse input in the "friendly" duration format: accumulated duration overflowed when adding value to unit hour"#,
996        );
997
998        insta::assert_snapshot!(p("153722867280912930minutes"), @"PT2562047788015215H30M");
999        insta::assert_snapshot!(p("153722867280912930minutes ago"), @"-PT2562047788015215H30M");
1000        insta::assert_snapshot!(
1001            pe("153722867280912931mins"),
1002            @r#"failed to parse input in the "friendly" duration format: accumulated duration overflowed when adding value to unit minute"#,
1003        );
1004
1005        insta::assert_snapshot!(p("9223372036854775807seconds"), @"PT2562047788015215H30M7S");
1006        insta::assert_snapshot!(p("-9223372036854775807seconds"), @"-PT2562047788015215H30M7S");
1007        insta::assert_snapshot!(
1008            pe("9223372036854775808s"),
1009            @r#"failed to parse input in the "friendly" duration format: value for seconds is too big (or small) to fit into a signed 64-bit integer"#,
1010        );
1011        insta::assert_snapshot!(
1012            p("-9223372036854775808s"),
1013            @"-PT2562047788015215H30M8S",
1014        );
1015    }
1016
1017    #[test]
1018    fn err_signed_duration_basic() {
1019        let p = |s: &str| SpanParser::new().parse_duration(s).unwrap_err();
1020
1021        insta::assert_snapshot!(
1022            p(""),
1023            @r#"failed to parse input in the "friendly" duration format: an empty string is not valid"#,
1024        );
1025        insta::assert_snapshot!(
1026            p(" "),
1027            @r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#,
1028        );
1029        insta::assert_snapshot!(
1030            p("5"),
1031            @r#"failed to parse input in the "friendly" duration format: expected to find unit designator suffix (e.g., `years` or `secs`) after parsing integer"#,
1032        );
1033        insta::assert_snapshot!(
1034            p("a"),
1035            @r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#,
1036        );
1037        insta::assert_snapshot!(
1038            p("2 minutes 1 hour"),
1039            @r#"failed to parse input in the "friendly" duration format: found value with unit hour after unit minute, but units must be written from largest to smallest (and they can't be repeated)"#,
1040        );
1041        insta::assert_snapshot!(
1042            p("1 hour 1 minut"),
1043            @r#"failed to parse input in the "friendly" duration format: parsed value 'PT1H1M', but unparsed input "ut" remains (expected no unparsed input)"#,
1044        );
1045        insta::assert_snapshot!(
1046            p("2 minutes,"),
1047            @r#"failed to parse input in the "friendly" duration format: expected whitespace after comma, but found end of input"#,
1048        );
1049        insta::assert_snapshot!(
1050            p("2 minutes, "),
1051            @r#"failed to parse input in the "friendly" duration format: found comma at the end of duration, but a comma indicates at least one more unit follows"#,
1052        );
1053        insta::assert_snapshot!(
1054            p("2 minutes ,"),
1055            @r#"failed to parse input in the "friendly" duration format: parsed value 'PT2M', but unparsed input "," remains (expected no unparsed input)"#,
1056        );
1057    }
1058
1059    #[test]
1060    fn err_signed_duration_sign() {
1061        let p = |s: &str| SpanParser::new().parse_duration(s).unwrap_err();
1062
1063        insta::assert_snapshot!(
1064            p("1hago"),
1065            @r#"failed to parse input in the "friendly" duration format: parsed value 'PT1H', but unparsed input "ago" remains (expected no unparsed input)"#,
1066        );
1067        insta::assert_snapshot!(
1068            p("1 hour 1 minuteago"),
1069            @r#"failed to parse input in the "friendly" duration format: parsed value 'PT1H1M', but unparsed input "ago" remains (expected no unparsed input)"#,
1070        );
1071        insta::assert_snapshot!(
1072            p("+1 hour 1 minute ago"),
1073            @r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#,
1074        );
1075        insta::assert_snapshot!(
1076            p("-1 hour 1 minute ago"),
1077            @r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#,
1078        );
1079    }
1080
1081    #[test]
1082    fn err_signed_duration_overflow_fraction() {
1083        let p = |s: &str| SpanParser::new().parse_duration(s).unwrap();
1084        let pe = |s: &str| SpanParser::new().parse_duration(s).unwrap_err();
1085
1086        insta::assert_snapshot!(
1087            // Unlike `Span`, this just overflows because it can't be parsed
1088            // as a 64-bit integer.
1089            pe("9223372036854775808 micros"),
1090            @r#"failed to parse input in the "friendly" duration format: value for microseconds is too big (or small) to fit into a signed 64-bit integer"#,
1091        );
1092        // one fewer is okay
1093        insta::assert_snapshot!(
1094            p("9223372036854775807 micros"),
1095            @"PT2562047788H54.775807S"
1096        );
1097    }
1098
1099    #[test]
1100    fn err_signed_duration_fraction() {
1101        let p = |s: &str| SpanParser::new().parse_duration(s).unwrap_err();
1102
1103        insta::assert_snapshot!(
1104            p("1.5 nanos"),
1105            @r#"failed to parse input in the "friendly" duration format: fractional nanoseconds are not supported"#,
1106        );
1107    }
1108
1109    #[test]
1110    fn err_signed_duration_hms() {
1111        let p = |s: &str| SpanParser::new().parse_duration(s).unwrap_err();
1112
1113        insta::assert_snapshot!(
1114            p("05:"),
1115            @r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse minute following hour"#,
1116        );
1117        insta::assert_snapshot!(
1118            p("05:06"),
1119            @r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse `:` following minute"#,
1120        );
1121        insta::assert_snapshot!(
1122            p("05:06:"),
1123            @r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse second following minute"#,
1124        );
1125        insta::assert_snapshot!(
1126            p("2 hours, 05:06:07"),
1127            @r#"failed to parse input in the "friendly" duration format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#,
1128        );
1129    }
1130
1131    #[test]
1132    fn parse_unsigned_duration_basic() {
1133        let p = |s: &str| {
1134            let dur = SpanParser::new().parse_unsigned_duration(s).unwrap();
1135            crate::fmt::temporal::SpanPrinter::new()
1136                .unsigned_duration_to_string(&dur)
1137        };
1138
1139        insta::assert_snapshot!(
1140            p("1 hour, 2 minutes, 3 seconds"),
1141            @"PT1H2M3S",
1142        );
1143        insta::assert_snapshot!(p("01:02:03"), @"PT1H2M3S");
1144        insta::assert_snapshot!(p("999:999:999"), @"PT1015H55M39S");
1145        insta::assert_snapshot!(
1146            p("+1hr"),
1147            @"PT1H",
1148        );
1149    }
1150
1151    #[test]
1152    fn parse_unsigned_duration_negate() {
1153        let p = |s: &str| {
1154            let dur = SpanParser::new().parse_unsigned_duration(s).unwrap();
1155            crate::fmt::temporal::SpanPrinter::new()
1156                .unsigned_duration_to_string(&dur)
1157        };
1158        let perr = |s: &str| {
1159            SpanParser::new().parse_unsigned_duration(s).unwrap_err()
1160        };
1161
1162        insta::assert_snapshot!(
1163            p("18446744073709551615s"),
1164            @"PT5124095576030431H15S",
1165        );
1166        insta::assert_snapshot!(
1167            perr("18446744073709551616s"),
1168            @r#"failed to parse input in the "friendly" duration format: number too big to parse into 64-bit integer"#,
1169        );
1170        insta::assert_snapshot!(
1171            perr("-1s"),
1172            @r#"failed to parse input in the "friendly" duration format: cannot parse negative duration into unsigned `std::time::Duration`"#,
1173        );
1174    }
1175
1176    #[test]
1177    fn parse_unsigned_duration_fractional() {
1178        let p = |s: &str| {
1179            let dur = SpanParser::new().parse_unsigned_duration(s).unwrap();
1180            crate::fmt::temporal::SpanPrinter::new()
1181                .unsigned_duration_to_string(&dur)
1182        };
1183
1184        insta::assert_snapshot!(p("1.5hrs"), @"PT1H30M");
1185        insta::assert_snapshot!(p("1.5mins"), @"PT1M30S");
1186        insta::assert_snapshot!(p("1.5secs"), @"PT1.5S");
1187        insta::assert_snapshot!(p("1.5msecs"), @"PT0.0015S");
1188        insta::assert_snapshot!(p("1.5µsecs"), @"PT0.0000015S");
1189
1190        insta::assert_snapshot!(p("1h 1.5mins"), @"PT1H1M30S");
1191        insta::assert_snapshot!(p("1m 1.5secs"), @"PT1M1.5S");
1192        insta::assert_snapshot!(p("1s 1.5msecs"), @"PT1.0015S");
1193        insta::assert_snapshot!(p("1ms 1.5µsecs"), @"PT0.0010015S");
1194
1195        insta::assert_snapshot!(p("1s2000ms"), @"PT3S");
1196    }
1197
1198    #[test]
1199    fn parse_unsigned_duration_boundaries() {
1200        let p = |s: &str| {
1201            let dur = SpanParser::new().parse_unsigned_duration(s).unwrap();
1202            crate::fmt::temporal::SpanPrinter::new()
1203                .unsigned_duration_to_string(&dur)
1204        };
1205        let pe = |s: &str| SpanParser::new().parse_duration(s).unwrap_err();
1206
1207        insta::assert_snapshot!(p("175307616 hours"), @"PT175307616H");
1208        insta::assert_snapshot!(p("10518456960 minutes"), @"PT175307616H");
1209        insta::assert_snapshot!(p("631107417600 seconds"), @"PT175307616H");
1210        insta::assert_snapshot!(p("631107417600000 milliseconds"), @"PT175307616H");
1211        insta::assert_snapshot!(p("631107417600000000 microseconds"), @"PT175307616H");
1212        insta::assert_snapshot!(p("9223372036854775807 nanoseconds"), @"PT2562047H47M16.854775807S");
1213
1214        insta::assert_snapshot!(p("175307617 hours"), @"PT175307617H");
1215        insta::assert_snapshot!(p("10518456961 minutes"), @"PT175307616H1M");
1216        insta::assert_snapshot!(p("631107417601 seconds"), @"PT175307616H1S");
1217        insta::assert_snapshot!(p("631107417600001 milliseconds"), @"PT175307616H0.001S");
1218        insta::assert_snapshot!(p("631107417600000001 microseconds"), @"PT175307616H0.000001S");
1219
1220        // The above were copied from the corresponding `Span` test, which has
1221        // tighter limits on components. But a `std::time::Duration` supports
1222        // the full range of `u64` seconds.
1223        insta::assert_snapshot!(p("5124095576030431hours"), @"PT5124095576030431H");
1224        insta::assert_snapshot!(
1225            pe("5124095576030432hrs"),
1226            @r#"failed to parse input in the "friendly" duration format: accumulated duration overflowed when adding value to unit hour"#,
1227        );
1228
1229        insta::assert_snapshot!(p("307445734561825860minutes"), @"PT5124095576030431H");
1230        insta::assert_snapshot!(
1231            pe("307445734561825861mins"),
1232            @r#"failed to parse input in the "friendly" duration format: accumulated duration overflowed when adding value to unit minute"#,
1233        );
1234
1235        insta::assert_snapshot!(p("18446744073709551615seconds"), @"PT5124095576030431H15S");
1236        insta::assert_snapshot!(
1237            pe("18446744073709551616s"),
1238            @r#"failed to parse input in the "friendly" duration format: number too big to parse into 64-bit integer"#,
1239        );
1240    }
1241
1242    #[test]
1243    fn err_unsigned_duration_basic() {
1244        let p = |s: &str| {
1245            SpanParser::new().parse_unsigned_duration(s).unwrap_err()
1246        };
1247
1248        insta::assert_snapshot!(
1249            p(""),
1250            @r#"failed to parse input in the "friendly" duration format: an empty string is not valid"#,
1251        );
1252        insta::assert_snapshot!(
1253            p(" "),
1254            @r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#,
1255        );
1256        insta::assert_snapshot!(
1257            p("5"),
1258            @r#"failed to parse input in the "friendly" duration format: expected to find unit designator suffix (e.g., `years` or `secs`) after parsing integer"#,
1259        );
1260        insta::assert_snapshot!(
1261            p("a"),
1262            @r#"failed to parse input in the "friendly" duration format: expected duration to start with a unit value (a decimal integer) after an optional sign, but no integer was found"#,
1263        );
1264        insta::assert_snapshot!(
1265            p("2 minutes 1 hour"),
1266            @r#"failed to parse input in the "friendly" duration format: found value with unit hour after unit minute, but units must be written from largest to smallest (and they can't be repeated)"#,
1267        );
1268        insta::assert_snapshot!(
1269            p("1 hour 1 minut"),
1270            @r#"failed to parse input in the "friendly" duration format: parsed value '3660s', but unparsed input "ut" remains (expected no unparsed input)"#,
1271        );
1272        insta::assert_snapshot!(
1273            p("2 minutes,"),
1274            @r#"failed to parse input in the "friendly" duration format: expected whitespace after comma, but found end of input"#,
1275        );
1276        insta::assert_snapshot!(
1277            p("2 minutes, "),
1278            @r#"failed to parse input in the "friendly" duration format: found comma at the end of duration, but a comma indicates at least one more unit follows"#,
1279        );
1280        insta::assert_snapshot!(
1281            p("2 minutes ,"),
1282            @r#"failed to parse input in the "friendly" duration format: parsed value '120s', but unparsed input "," remains (expected no unparsed input)"#,
1283        );
1284    }
1285
1286    #[test]
1287    fn err_unsigned_duration_sign() {
1288        let p = |s: &str| {
1289            SpanParser::new().parse_unsigned_duration(s).unwrap_err()
1290        };
1291
1292        insta::assert_snapshot!(
1293            p("1hago"),
1294            @r#"failed to parse input in the "friendly" duration format: parsed value '3600s', but unparsed input "ago" remains (expected no unparsed input)"#,
1295        );
1296        insta::assert_snapshot!(
1297            p("1 hour 1 minuteago"),
1298            @r#"failed to parse input in the "friendly" duration format: parsed value '3660s', but unparsed input "ago" remains (expected no unparsed input)"#,
1299        );
1300        insta::assert_snapshot!(
1301            p("+1 hour 1 minute ago"),
1302            @r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#,
1303        );
1304        insta::assert_snapshot!(
1305            p("-1 hour 1 minute ago"),
1306            @r#"failed to parse input in the "friendly" duration format: expected to find either a prefix sign (+/-) or a suffix sign (`ago`), but found both"#,
1307        );
1308    }
1309
1310    #[test]
1311    fn err_unsigned_duration_overflow_fraction() {
1312        let p = |s: &str| {
1313            let dur = SpanParser::new().parse_unsigned_duration(s).unwrap();
1314            crate::fmt::temporal::SpanPrinter::new()
1315                .unsigned_duration_to_string(&dur)
1316        };
1317        let pe = |s: &str| {
1318            SpanParser::new().parse_unsigned_duration(s).unwrap_err()
1319        };
1320
1321        insta::assert_snapshot!(
1322            // Unlike `Span`, this just overflows because it can't be parsed
1323            // as a 64-bit integer.
1324            pe("18446744073709551616 micros"),
1325            @r#"failed to parse input in the "friendly" duration format: number too big to parse into 64-bit integer"#,
1326        );
1327        // one fewer is okay
1328        insta::assert_snapshot!(
1329            p("18446744073709551615 micros"),
1330            @"PT5124095576H1M49.551615S"
1331        );
1332    }
1333
1334    #[test]
1335    fn err_unsigned_duration_fraction() {
1336        let p = |s: &str| {
1337            SpanParser::new().parse_unsigned_duration(s).unwrap_err()
1338        };
1339
1340        insta::assert_snapshot!(
1341            p("1.5 nanos"),
1342            @r#"failed to parse input in the "friendly" duration format: fractional nanoseconds are not supported"#,
1343        );
1344    }
1345
1346    #[test]
1347    fn err_unsigned_duration_hms() {
1348        let p = |s: &str| {
1349            SpanParser::new().parse_unsigned_duration(s).unwrap_err()
1350        };
1351
1352        insta::assert_snapshot!(
1353            p("05:"),
1354            @r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse minute following hour"#,
1355        );
1356        insta::assert_snapshot!(
1357            p("05:06"),
1358            @r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse `:` following minute"#,
1359        );
1360        insta::assert_snapshot!(
1361            p("05:06:"),
1362            @r#"failed to parse input in the "friendly" duration format: when parsing the `HH:MM:SS` format, expected to parse second following minute"#,
1363        );
1364        insta::assert_snapshot!(
1365            p("2 hours, 05:06:07"),
1366            @r#"failed to parse input in the "friendly" duration format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#,
1367        );
1368    }
1369}