jiff/fmt/friendly/
parser.rs

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