jiff/fmt/friendly/
mod.rs

1/*!
2A bespoke but easy to read format for [`Span`](crate::Span) and
3[`SignedDuration`](crate::SignedDuration).
4
5The "friendly" duration format is meant to be an alternative to [Temporal's
6ISO 8601 duration format](super::temporal) that is both easier to read and can
7losslessly serialize and deserialize all `Span` values.
8
9Here are a variety of examples showing valid friendly durations for `Span`:
10
11```
12use jiff::{Span, ToSpan};
13
14let spans = [
15    ("40d", 40.days()),
16    ("40 days", 40.days()),
17    ("1y1d", 1.year().days(1)),
18    ("1yr 1d", 1.year().days(1)),
19    ("3d4h59m", 3.days().hours(4).minutes(59)),
20    ("3 days, 4 hours, 59 minutes", 3.days().hours(4).minutes(59)),
21    ("3d 4h 59m", 3.days().hours(4).minutes(59)),
22    ("2h30m", 2.hours().minutes(30)),
23    ("2h 30m", 2.hours().minutes(30)),
24    ("1mo", 1.month()),
25    ("1w", 1.week()),
26    ("1 week", 1.week()),
27    ("1w4d", 1.week().days(4)),
28    ("1 wk 4 days", 1.week().days(4)),
29    ("1m", 1.minute()),
30    ("0.0021s", 2.milliseconds().microseconds(100)),
31    ("0s", 0.seconds()),
32    ("0d", 0.seconds()),
33    ("0 days", 0.seconds()),
34    ("3 mins 34s 123ms", 3.minutes().seconds(34).milliseconds(123)),
35    ("3 mins 34.123 secs", 3.minutes().seconds(34).milliseconds(123)),
36    ("3 mins 34,123s", 3.minutes().seconds(34).milliseconds(123)),
37    (
38        "1y1mo1d1h1m1.1s",
39        1.year().months(1).days(1).hours(1).minutes(1).seconds(1).milliseconds(100),
40    ),
41    (
42        "1yr 1mo 1day 1hr 1min 1.1sec",
43        1.year().months(1).days(1).hours(1).minutes(1).seconds(1).milliseconds(100),
44    ),
45    (
46        "1 year, 1 month, 1 day, 1 hour, 1 minute 1.1 seconds",
47        1.year().months(1).days(1).hours(1).minutes(1).seconds(1).milliseconds(100),
48    ),
49    (
50        "1 year, 1 month, 1 day, 01:01:01.1",
51        1.year().months(1).days(1).hours(1).minutes(1).seconds(1).milliseconds(100),
52    ),
53];
54for (string, span) in spans {
55    let parsed: Span = string.parse()?;
56    assert_eq!(
57        span.fieldwise(),
58        parsed.fieldwise(),
59        "result of parsing {string:?}",
60    );
61}
62
63# Ok::<(), Box<dyn std::error::Error>>(())
64```
65
66Note that for a `SignedDuration`, only units up to hours are supported. If you
67need to support bigger units, then you'll need to convert it to a `Span` before
68printing to the friendly format (or parse into a `Span` and then convert to a
69`SignedDuration`).
70
71# Integration points
72
73While this module can of course be used to parse and print durations in the
74friendly format, in most cases, you don't have to. Namely, it is already
75integrated into the `Span` and `SignedDuration` types.
76
77For example, the friendly format can be used by invoking the "alternate"
78format when using the `std::fmt::Display` trait implementation:
79
80```
81use jiff::{SignedDuration, ToSpan};
82
83let span = 2.months().days(35).hours(2).minutes(30);
84assert_eq!(format!("{span}"), "P2M35DT2H30M");      // ISO 8601
85assert_eq!(format!("{span:#}"), "2mo 35d 2h 30m");  // "friendly"
86
87let sdur = SignedDuration::new(2 * 60 * 60 + 30 * 60, 123_456_789);
88assert_eq!(format!("{sdur}"), "PT2H30M0.123456789S");         // ISO 8601
89assert_eq!(format!("{sdur:#}"), "2h 30m 123ms 456µs 789ns");  // "friendly"
90```
91
92Both `Span` and `SignedDuration` use the "friendly" format for its
93`std::fmt::Debug` trait implementation:
94
95```
96use jiff::{SignedDuration, ToSpan};
97
98let span = 2.months().days(35).hours(2).minutes(30);
99assert_eq!(format!("{span:?}"), "2mo 35d 2h 30m");
100
101let sdur = SignedDuration::new(2 * 60 * 60 + 30 * 60, 123_456_789);
102assert_eq!(format!("{sdur:?}"), "2h 30m 123ms 456µs 789ns");
103```
104
105Both `Span` and `SignedDuration` support parsing the ISO 8601 _and_ friendly
106formats via its `std::str::FromStr` trait:
107
108```
109use jiff::{SignedDuration, Span, ToSpan};
110
111let expected = 2.months().days(35).hours(2).minutes(30);
112let span: Span = "2 months, 35 days, 02:30:00".parse()?;
113assert_eq!(span, expected.fieldwise());
114let span: Span = "P2M35DT2H30M".parse()?;
115assert_eq!(span, expected.fieldwise());
116
117let expected = SignedDuration::new(2 * 60 * 60 + 30 * 60, 123_456_789);
118let sdur: SignedDuration = "2h 30m 0,123456789s".parse()?;
119assert_eq!(sdur, expected);
120let sdur: SignedDuration = "PT2h30m0.123456789s".parse()?;
121assert_eq!(sdur, expected);
122
123# Ok::<(), Box<dyn std::error::Error>>(())
124```
125
126If you need to parse _only_ the friendly format, then that would be a good use
127case for using [`SpanParser`] in this module.
128
129Finally, when the `serde` crate feature is enabled, the friendly format is
130automatically supported via the `serde::Deserialize` trait implementation, just
131like for the `std::str::FromStr` trait above. However, for `serde::Serialize`,
132both types use ISO 8601. In order to serialize the friendly format,
133you'll need to write your own serialization function or use one of the
134[`fmt::serde`](crate::fmt::serde) helpers provided by Jiff. For example:
135
136```
137use jiff::{ToSpan, Span};
138
139#[derive(Debug, serde::Deserialize, serde::Serialize)]
140struct Record {
141    #[serde(
142        serialize_with = "jiff::fmt::serde::span::friendly::compact::required"
143    )]
144    span: Span,
145}
146
147let json = r#"{"span":"1 year 2 months 36 hours 1100ms"}"#;
148let got: Record = serde_json::from_str(&json)?;
149assert_eq!(
150    got.span.fieldwise(),
151    1.year().months(2).hours(36).milliseconds(1100),
152);
153
154let expected = r#"{"span":"1y 2mo 36h 1100ms"}"#;
155assert_eq!(serde_json::to_string(&got).unwrap(), expected);
156
157# Ok::<(), Box<dyn std::error::Error>>(())
158```
159
160The ISO 8601 format is used by default since it is part of a standard and is
161more widely accepted. That is, if you need an interoperable interchange format,
162then ISO 8601 is probably the right choice.
163
164# Rounding
165
166The printer in this module has no options for rounding. Instead, it is intended
167for users to round a [`Span`](crate::Span) first, and then print it. The idea
168is that printing a `Span` is a relatively "dumb" operation that just emits
169whatever units are non-zero in the `Span`. This is possible with a `Span`
170because it represents each unit distinctly. (With a [`std::time::Duration`] or
171a [`jiff::SignedDuration`](crate::SignedDuration), more functionality would
172need to be coupled with the printing logic to achieve a similar result.)
173
174For example, if you want to print the duration since someone posted a comment
175to an English speaking end user, precision below one half hour might be "too
176much detail." You can remove this by rounding the `Span` to the nearest half
177hour before printing:
178
179```
180use jiff::{civil, RoundMode, ToSpan, Unit, ZonedDifference};
181
182let commented_at = civil::date(2024, 8, 1).at(19, 29, 13, 123_456_789).in_tz("US/Eastern")?;
183let now = civil::date(2024, 12, 26).at(12, 49, 0, 0).in_tz("US/Eastern")?;
184
185// The default, with units permitted up to years.
186let span = now.since((Unit::Year, &commented_at))?;
187assert_eq!(format!("{span:#}"), "4mo 24d 17h 19m 46s 876ms 543µs 211ns");
188
189// The same subtraction, but with more options to control
190// rounding the result. We could also do this with `Span::round`
191// directly by providing `now` as our relative zoned datetime.
192let rounded = now.since(
193    ZonedDifference::new(&commented_at)
194        .smallest(Unit::Minute)
195        .largest(Unit::Year)
196        .mode(RoundMode::HalfExpand)
197        .increment(30),
198)?;
199assert_eq!(format!("{rounded:#}"), "4mo 24d 17h 30m");
200
201# Ok::<(), Box<dyn std::error::Error>>(())
202```
203
204# Comparison with the [`humantime`] crate
205
206To a first approximation, Jiff should cover all `humantime` use cases,
207including [`humantime-serde`] for serialization support.
208
209To a second approximation, it was a design point of the friendly format to be
210mostly interoperable with what `humantime` supports. For example, any duration
211string formatted by `humantime` at time of writing is also a valid friendly
212duration:
213
214```
215use std::time::Duration;
216
217use jiff::{Span, ToSpan};
218
219// Just a duration that includes as many unit designator labels as possible.
220let dur = Duration::new(
221    2 * 31_557_600 + 1 * 2_630_016 + 15 * 86400 + 5 * 3600 + 59 * 60 + 1,
222    123_456_789,
223);
224let formatted = humantime::format_duration(dur).to_string();
225assert_eq!(formatted, "2years 1month 15days 5h 59m 1s 123ms 456us 789ns");
226
227let span: Span = formatted.parse()?;
228let expected =
229    2.years()
230        .months(1)
231        .days(15)
232        .hours(5)
233        .minutes(59)
234        .seconds(1)
235        .milliseconds(123)
236        .microseconds(456)
237        .nanoseconds(789);
238assert_eq!(span, expected.fieldwise());
239
240# Ok::<(), Box<dyn std::error::Error>>(())
241```
242
243The above somewhat relies on the implementation details of `humantime`. Namely,
244not everything parseable by `humantime` is also parseable by the friendly
245format (and vice versa). For example, `humantime` parses `M` as a label for
246months, but the friendly format specifically eschews `M` because of its
247confusability with minutes:
248
249```
250use std::time::Duration;
251
252let dur = humantime::parse_duration("1M")?;
253// The +38,016 is because `humantime` assigns 30.44 24-hour days to all months.
254assert_eq!(dur, Duration::new(30 * 24 * 60 * 60 + 38_016, 0));
255
256// In contrast, Jiff will reject `1M`:
257assert_eq!(
258    "1M".parse::<jiff::Span>().unwrap_err().to_string(),
259    "failed to parse \"1M\" in the \"friendly\" format: expected to find unit designator suffix (e.g., 'years' or 'secs'), but found input beginning with \"M\" instead",
260);
261
262# Ok::<(), Box<dyn std::error::Error>>(())
263```
264
265In the other direction, Jiff's default formatting for the friendly duration
266isn't always parsable by `humantime`. This is because, for example, depending
267on the configuration, Jiff may use `mo` and `mos` for months, and `µs` for
268microseconds, none of which are supported by `humantime`. If you need it, to
269ensure `humantime` can parse a Jiff formatted friendly duration, Jiff provides
270a special mode that attempts compatibility with `humantime`:
271
272```
273use jiff::{fmt::friendly::{Designator, SpanPrinter}, ToSpan};
274
275
276let span =
277    2.years()
278        .months(1)
279        .days(15)
280        .hours(5)
281        .minutes(59)
282        .seconds(1)
283        .milliseconds(123)
284        .microseconds(456)
285        .nanoseconds(789);
286
287let printer = SpanPrinter::new().designator(Designator::HumanTime);
288assert_eq!(
289    printer.span_to_string(&span),
290    "2y 1month 15d 5h 59m 1s 123ms 456us 789ns",
291);
292```
293
294It's hard to provide solid guarantees here because `humantime`'s behavior could
295change, but at time of writing, `humantime` has not changed much in quite a
296long time (its last release is almost 4 years ago at time of writing). So the
297current behavior is likely pretty safe to rely upon.
298
299More generally, the friendly format is more flexible than what `humantime`
300supports. For example, the friendly format incorporates `HH:MM:SS` and
301fractional time units. It also supports more unit labels and permits commas
302to separate units.
303
304```
305use jiff::SignedDuration;
306
307// 10 hours and 30 minutes
308let expected = SignedDuration::new(10 * 60 * 60 + 30 * 60, 0);
309assert_eq!(expected, "10h30m".parse()?);
310assert_eq!(expected, "10hrs 30mins".parse()?);
311assert_eq!(expected, "10 hours 30 minutes".parse()?);
312assert_eq!(expected, "10 hours, 30 minutes".parse()?);
313assert_eq!(expected, "10:30:00".parse()?);
314assert_eq!(expected, "10.5 hours".parse()?);
315
316# Ok::<(), Box<dyn std::error::Error>>(())
317```
318
319Finally, it's important to point out that `humantime` only supports parsing
320variable width units like years, months and days by virtue of assigning fixed
321static values to them that aren't always correct. In contrast, Jiff always
322gets this right and specifically prevents you from getting it wrong.
323
324To begin, Jiff returns an error if you try to parse a varying unit into a
325[`SignedDuration`](crate::SignedDuration):
326
327```
328use jiff::SignedDuration;
329
330// works fine
331assert_eq!(
332    "1 hour".parse::<SignedDuration>().unwrap(),
333    SignedDuration::from_hours(1),
334);
335// Jiff is saving you from doing something wrong
336assert_eq!(
337    "1 day".parse::<SignedDuration>().unwrap_err().to_string(),
338    "failed to parse \"1 day\" in the \"friendly\" format: parsing day units into a `SignedDuration` is not supported (perhaps try parsing into a `Span` instead)",
339);
340```
341
342As the error message suggests, parsing into a [`Span`](crate::Span) works fine:
343
344```
345use jiff::Span;
346
347assert_eq!("1 day".parse::<Span>().unwrap(), Span::new().days(1).fieldwise());
348```
349
350Jiff has this behavior because it's not possible to determine, in general,
351how long "1 day" (or "1 month" or "1 year") is without a reference date.
352Since a `SignedDuration` (along with a [`std::time::Duration`]) does not
353support expressing durations in anything other than a 96-bit integer number of
354nanoseconds, it's not possible to represent concepts like "1 month." But a
355[`Span`](crate::Span) can.
356
357To see this more concretely, consider the different behavior resulting from
358using `humantime` to parse durations and adding them to a date:
359
360```
361use jiff::{civil, Span};
362
363let span: Span = "1 month".parse()?;
364let dur = humantime::parse_duration("1 month")?;
365
366let datetime = civil::date(2024, 5, 1).at(0, 0, 0, 0);
367
368// Adding 1 month using a `Span` gives one possible expected result. That is,
369// 2024-06-01T00:00:00 is exactly one month later than 2024-05-01T00:00:00.
370assert_eq!(datetime + span, civil::date(2024, 6, 1).at(0, 0, 0, 0));
371// But if we add the duration representing "1 month" as interpreted by
372// humantime, we get a very odd result. This is because humantime uses
373// a duration of 30.44 days (where every day is 24 hours exactly) for
374// all months.
375assert_eq!(datetime + dur, civil::date(2024, 5, 31).at(10, 33, 36, 0));
376
377# Ok::<(), Box<dyn std::error::Error>>(())
378```
379
380The same is true for days when dealing with zoned date times:
381
382```
383use jiff::{civil, Span};
384
385let span: Span = "1 day".parse()?;
386let dur = humantime::parse_duration("1 day")?;
387
388let zdt = civil::date(2024, 3, 9).at(17, 0, 0, 0).in_tz("US/Eastern")?;
389
390// Adding 1 day gives the generally expected result of the same clock
391// time on the following day when adding a `Span`.
392assert_eq!(&zdt + span, civil::date(2024, 3, 10).at(17, 0, 0, 0).in_tz("US/Eastern")?);
393// But with humantime, all days are assumed to be exactly 24 hours. So
394// you get an instant in time that is 24 hours later, even when some
395// days are shorter and some are longer.
396assert_eq!(&zdt + dur, civil::date(2024, 3, 10).at(18, 0, 0, 0).in_tz("US/Eastern")?);
397
398// Notice also that this inaccuracy can occur merely by a duration that
399// _crosses_ a time zone transition boundary (like DST) at any point. It
400// doesn't require your datetimes to be "close" to when DST occurred.
401let dur = humantime::parse_duration("20 day")?;
402let zdt = civil::date(2024, 3, 1).at(17, 0, 0, 0).in_tz("US/Eastern")?;
403assert_eq!(&zdt + dur, civil::date(2024, 3, 21).at(18, 0, 0, 0).in_tz("US/Eastern")?);
404
405# Ok::<(), Box<dyn std::error::Error>>(())
406```
407
408It's worth pointing out that in some applications, the fixed values assigned
409by `humantime` might be perfectly acceptable. Namely, they introduce error
410into calculations, but the error might be small enough to be a non-issue in
411some applications. But this error _can_ be avoided and `humantime` commits
412it silently. Indeed, `humantime`'s API is itself not possible without either
413rejecting varying length units or assuming fixed values for them. This is
414because it parses varying length units but returns a duration expressed as a
415single 96-bit integer number of nanoseconds. In order to do this, you _must_
416assume a definite length for those varying units. To do this _correctly_, you
417really need to provide a reference date.
418
419For example, Jiff can parse `1 month` into a `std::time::Duration` too, but
420it requires parsing into a `Span` and then converting into a `Duration` by
421providing a reference date:
422
423```
424use std::time::Duration;
425
426use jiff::{civil, Span};
427
428let span: Span = "1 month".parse()?;
429// converts to signed duration
430let sdur = span.to_duration(civil::date(2024, 5, 1))?;
431// converts to standard library unsigned duration
432let dur = Duration::try_from(sdur)?;
433// exactly 31 days where each day is 24 hours long.
434assert_eq!(dur, Duration::from_secs(31 * 24 * 60 * 60));
435
436// Now change the reference date and notice that the
437// resulting duration is changed but still correct.
438let sdur = span.to_duration(civil::date(2024, 6, 1))?;
439let dur = Duration::try_from(sdur)?;
440// exactly 30 days where each day is 24 hours long.
441assert_eq!(dur, Duration::from_secs(30 * 24 * 60 * 60));
442
443# Ok::<(), Box<dyn std::error::Error>>(())
444```
445
446# Motivation
447
448This format was devised, in part, because the standard duration interchange
449format specified by [Temporal's ISO 8601 definition](super::temporal) is
450sub-optimal in two important respects:
451
4521. It doesn't support individual sub-second components.
4532. It is difficult to read.
454
455In the first case, ISO 8601 durations do support sub-second components, but are
456only expressible as fractional seconds. For example:
457
458```text
459PT1.100S
460```
461
462This is problematic in some cases because it doesn't permit distinguishing
463between some spans. For example, `1.second().milliseconds(100)` and
464`1100.milliseconds()` both serialize to the same ISO 8601 duration as shown
465above. At deserialization time, it's impossible to know what the span originally
466looked like. Thus, using the ISO 8601 format means the serialization and
467deserialization of [`Span`](crate::Span) values is lossy.
468
469In the second case, ISO 8601 durations appear somewhat difficult to quickly
470read. For example:
471
472```text
473P1Y2M3DT4H59M1.1S
474P1y2m3dT4h59m1.1S
475```
476
477When all of the unit designators are capital letters in particular (which
478is the default), everything runs together and it's hard for the eye to
479distinguish where digits stop and letters begin. Using lowercase letters for
480unit designators helps somewhat, but this is an extension to ISO 8601 that
481isn't broadly supported.
482
483The "friendly" format resolves both of these problems by permitting sub-second
484components and allowing the use of whitespace and longer unit designator labels
485to improve readability. For example, all of the following are equivalent and
486will parse to the same `Span`:
487
488```text
4891y 2mo 3d 4h 59m 1100ms
4901 year 2 months 3 days 4h59m1100ms
4911 year, 2 months, 3 days, 4h59m1100ms
4921 year, 2 months, 3 days, 4 hours 59 minutes 1100 milliseconds
493```
494
495At the same time, the friendly format continues to support fractional
496time components since they may be desirable in some cases. For example, all
497of the following are equivalent:
498
499```text
5001h 1m 1.5s
5011h 1m 1,5s
50201:01:01.5
50301:01:01,5
504```
505
506The idea with the friendly format is that end users who know how to write
507English durations are happy to both read and write durations in this format.
508And moreover, the format is flexible enough that end users generally don't need
509to stare at a grammar to figure out how to write a valid duration. Most of the
510intuitive things you'd expect to work will work.
511
512# Internationalization
513
514Currently, only US English unit designator labels are supported. In general,
515Jiff resists trying to solve the internationalization problem in favor
516of punting it to another crate, such as [`icu`] via [`jiff-icu`]. Jiff
517_could_ adopt unit designator labels for other languages, but it's not
518totally clear whether that's the right path to follow given the complexity
519of internationalization. If you'd like to discuss it, please
520[file an issue](https://github.com/BurntSushi/jiff/issues).
521
522# Grammar
523
524This section gives a more precise description of the "friendly" duration format
525in the form of a grammar.
526
527```text
528format =
529    format-signed-hms
530    | format-signed-designator
531
532format-signed-hms =
533    sign? format-hms
534
535format-hms =
536    [0-9]+ ':' [0-9]+ ':' [0-9]+ fractional?
537
538format-signed-designator =
539    sign? format-designator-units
540    | format-designator-units direction?
541format-designator-units =
542    years
543    | months
544    | weeks
545    | days
546    | hours
547    | minutes
548    | seconds
549    | milliseconds
550    | microseconds
551    | nanoseconds
552
553# This dance below is basically to ensure a few things:
554# First, that at least one unit appears. That is, that
555# we don't accept the empty string. Secondly, when a
556# fractional component appears in a time value, we don't
557# allow any subsequent units to appear. Thirdly, that
558# `HH:MM:SS[.f{1,9}]?` is allowed after years, months,
559# weeks or days.
560years =
561    unit-value unit-years comma? ws* format-hms
562    | unit-value unit-years comma? ws* months
563    | unit-value unit-years comma? ws* weeks
564    | unit-value unit-years comma? ws* days
565    | unit-value unit-years comma? ws* hours
566    | unit-value unit-years comma? ws* minutes
567    | unit-value unit-years comma? ws* seconds
568    | unit-value unit-years comma? ws* milliseconds
569    | unit-value unit-years comma? ws* microseconds
570    | unit-value unit-years comma? ws* nanoseconds
571    | unit-value unit-years
572months =
573    unit-value unit-months comma? ws* format-hms
574    | unit-value unit-months comma? ws* weeks
575    | unit-value unit-months comma? ws* days
576    | unit-value unit-months comma? ws* hours
577    | unit-value unit-months comma? ws* minutes
578    | unit-value unit-months comma? ws* seconds
579    | unit-value unit-months comma? ws* milliseconds
580    | unit-value unit-months comma? ws* microseconds
581    | unit-value unit-months comma? ws* nanoseconds
582    | unit-value unit-months
583weeks =
584    unit-value unit-weeks comma? ws* format-hms
585    | unit-value unit-weeks comma? ws* days
586    | unit-value unit-weeks comma? ws* hours
587    | unit-value unit-weeks comma? ws* minutes
588    | unit-value unit-weeks comma? ws* seconds
589    | unit-value unit-weeks comma? ws* milliseconds
590    | unit-value unit-weeks comma? ws* microseconds
591    | unit-value unit-weeks comma? ws* nanoseconds
592    | unit-value unit-weeks
593days =
594    unit-value unit-days comma? ws* format-hms
595    | unit-value unit-days comma? ws* hours
596    | unit-value unit-days comma? ws* minutes
597    | unit-value unit-days comma? ws* seconds
598    | unit-value unit-days comma? ws* milliseconds
599    | unit-value unit-days comma? ws* microseconds
600    | unit-value unit-days comma? ws* nanoseconds
601    | unit-value unit-days
602hours =
603    unit-value unit-hours comma? ws* minutes
604    | unit-value unit-hours comma? ws* seconds
605    | unit-value unit-hours comma? ws* milliseconds
606    | unit-value unit-hours comma? ws* microseconds
607    | unit-value unit-hours comma? ws* nanoseconds
608    | unit-value fractional? ws* unit-hours
609minutes =
610    unit-value unit-minutes comma? ws* seconds
611    | unit-value unit-minutes comma? ws* milliseconds
612    | unit-value unit-minutes comma? ws* microseconds
613    | unit-value unit-minutes comma? ws* nanoseconds
614    | unit-value fractional? ws* unit-minutes
615seconds =
616    unit-value unit-seconds comma? ws* milliseconds
617    | unit-value unit-seconds comma? ws* microseconds
618    | unit-value unit-seconds comma? ws* nanoseconds
619    | unit-value fractional? ws* unit-seconds
620milliseconds =
621    unit-value unit-milliseconds comma? ws* microseconds
622    | unit-value unit-milliseconds comma? ws* nanoseconds
623    | unit-value fractional? ws* unit-milliseconds
624microseconds =
625    unit-value unit-microseconds comma? ws* nanoseconds
626    | unit-value fractional? ws* unit-microseconds
627nanoseconds =
628    unit-value fractional? ws* unit-nanoseconds
629
630unit-value = [0-9]+ [ws*]
631unit-years = 'years' | 'year' | 'yrs' | 'yr' | 'y'
632unit-months = 'months' | 'month' | 'mos' | 'mo'
633unit-weeks = 'weeks' | 'week' | 'wks' | 'wk' | 'w'
634unit-days = 'days' | 'day' | 'd'
635unit-hours = 'hours' | 'hour' | 'hrs' | 'hr' | 'h'
636unit-minutes = 'minutes' | 'minute' | 'mins' | 'min' | 'm'
637unit-seconds = 'seconds' | 'second' | 'secs' | 'sec' | 's'
638unit-milliseconds =
639    'milliseconds'
640    | 'millisecond'
641    | 'millis'
642    | 'milli'
643    | 'msecs'
644    | 'msec'
645    | 'ms'
646unit-microseconds =
647    'microseconds'
648    | 'microsecond'
649    | 'micros'
650    | 'micro'
651    | 'usecs'
652    | 'usec'
653    | 'µ' (U+00B5 MICRO SIGN) 'secs'
654    | 'µ' (U+00B5 MICRO SIGN) 'sec'
655    | 'us'
656    | 'µ' (U+00B5 MICRO SIGN) 's'
657unit-nanoseconds =
658    'nanoseconds' | 'nanosecond' | 'nanos' | 'nano' | 'nsecs' | 'nsec' | 'ns'
659
660fractional = decimal-separator decimal-fraction
661decimal-separator = '.' | ','
662decimal-fraction = [0-9]{1,9}
663
664sign = '+' | '-'
665direction = ws 'ago'
666comma = ',' ws
667ws =
668    U+0020 SPACE
669    | U+0009 HORIZONTAL TAB
670    | U+000A LINE FEED
671    | U+000C FORM FEED
672    | U+000D CARRIAGE RETURN
673```
674
675One thing not specified by the grammar above are maximum values. Namely,
676there are no specific maximum values imposed for each individual unit, nor
677a maximum value for the entire duration (say, when converted to nanoseconds).
678Instead, implementations are expected to impose their own limitations.
679
680For Jiff, a `Span` is more limited than a `SignedDuration`. For example, a the
681year component of a `Span` is limited to `[-19,999, 19,999]`. In contrast,
682a `SignedDuration` is a 96-bit signed integer number of nanoseconds with no
683particular limits on the individual units. They just can't combine to something
684that overflows a 96-bit signed integer number of nanoseconds. (And parsing into
685a `SignedDuration` directly only supports units of hours or smaller, since
686bigger units do not have an invariant length.) In general, implementations
687should support a "reasonable" range of values.
688
689[`humantime`]: https://docs.rs/humantime
690[`humantime-serde`]: https://docs.rs/humantime-serde
691[`icu`]: https://docs.rs/icu
692[`jiff-icu`]: https://docs.rs/jiff-icu
693*/
694
695pub use self::{
696    parser::SpanParser,
697    printer::{Designator, Direction, FractionalUnit, Spacing, SpanPrinter},
698};
699
700/// The default span/duration parser that we use.
701pub(crate) static DEFAULT_SPAN_PARSER: SpanParser = SpanParser::new();
702
703/// The default span/duration printer that we use.
704pub(crate) static DEFAULT_SPAN_PRINTER: SpanPrinter = SpanPrinter::new();
705
706mod parser;
707mod parser_label;
708mod printer;