jiff/fmt/friendly/
parser.rs

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