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