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, 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 input in the \"friendly\" duration format: \
260 expected to find unit designator suffix \
261 (e.g., `years` or `secs`) after parsing integer",
262);
263
264# Ok::<(), Box<dyn std::error::Error>>(())
265```
266
267In the other direction, Jiff's default formatting for the friendly duration
268isn't always parsable by `humantime`. This is because, for example, depending
269on the configuration, Jiff may use `mo` and `mos` for months, and `µs` for
270microseconds, none of which are supported by `humantime`. If you need it, to
271ensure `humantime` can parse a Jiff formatted friendly duration, Jiff provides
272a special mode that attempts compatibility with `humantime`:
273
274```
275use jiff::{fmt::friendly::{Designator, SpanPrinter}, ToSpan};
276
277
278let span =
279 2.years()
280 .months(1)
281 .days(15)
282 .hours(5)
283 .minutes(59)
284 .seconds(1)
285 .milliseconds(123)
286 .microseconds(456)
287 .nanoseconds(789);
288
289let printer = SpanPrinter::new().designator(Designator::HumanTime);
290assert_eq!(
291 printer.span_to_string(&span),
292 "2y 1month 15d 5h 59m 1s 123ms 456us 789ns",
293);
294```
295
296It's hard to provide solid guarantees here because `humantime`'s behavior could
297change, but at time of writing, `humantime` has not changed much in quite a
298long time (its last release is almost 4 years ago at time of writing). So the
299current behavior is likely pretty safe to rely upon.
300
301More generally, the friendly format is more flexible than what `humantime`
302supports. For example, the friendly format incorporates `HH:MM:SS` and
303fractional time units. It also supports more unit labels and permits commas
304to separate units.
305
306```
307use jiff::SignedDuration;
308
309// 10 hours and 30 minutes
310let expected = SignedDuration::new(10 * 60 * 60 + 30 * 60, 0);
311assert_eq!(expected, "10h30m".parse()?);
312assert_eq!(expected, "10hrs 30mins".parse()?);
313assert_eq!(expected, "10 hours 30 minutes".parse()?);
314assert_eq!(expected, "10 hours, 30 minutes".parse()?);
315assert_eq!(expected, "10:30:00".parse()?);
316assert_eq!(expected, "10.5 hours".parse()?);
317
318# Ok::<(), Box<dyn std::error::Error>>(())
319```
320
321Finally, it's important to point out that `humantime` only supports parsing
322variable width units like years, months and days by virtue of assigning fixed
323static values to them that aren't always correct. In contrast, Jiff always
324gets this right and specifically prevents you from getting it wrong.
325
326To begin, Jiff returns an error if you try to parse a varying unit into a
327[`SignedDuration`](crate::SignedDuration):
328
329```
330use jiff::SignedDuration;
331
332// works fine
333assert_eq!(
334 "1 hour".parse::<SignedDuration>().unwrap(),
335 SignedDuration::from_hours(1),
336);
337// Jiff is saving you from doing something wrong
338assert_eq!(
339 "1 day".parse::<SignedDuration>().unwrap_err().to_string(),
340 "failed to parse input in the \"friendly\" duration format: \
341 parsing calendar units (days in this case) in this context \
342 is not supported (perhaps try parsing into a `jiff::Span` instead)",
343);
344```
345
346As the error message suggests, parsing into a [`Span`](crate::Span) works fine:
347
348```
349use jiff::Span;
350
351assert_eq!("1 day".parse::<Span>().unwrap(), Span::new().days(1).fieldwise());
352```
353
354Jiff has this behavior because it's not possible to determine, in general,
355how long "1 day" (or "1 month" or "1 year") is without a reference date.
356Since a `SignedDuration` (along with a [`std::time::Duration`]) does not
357support expressing durations in anything other than a 96-bit integer number of
358nanoseconds, it's not possible to represent concepts like "1 month." But a
359[`Span`](crate::Span) can.
360
361To see this more concretely, consider the different behavior resulting from
362using `humantime` to parse durations and adding them to a date:
363
364```
365use jiff::{civil, Span};
366
367let span: Span = "1 month".parse()?;
368let dur = humantime::parse_duration("1 month")?;
369
370let datetime = civil::date(2024, 5, 1).at(0, 0, 0, 0);
371
372// Adding 1 month using a `Span` gives one possible expected result. That is,
373// 2024-06-01T00:00:00 is exactly one month later than 2024-05-01T00:00:00.
374assert_eq!(datetime + span, civil::date(2024, 6, 1).at(0, 0, 0, 0));
375// But if we add the duration representing "1 month" as interpreted by
376// humantime, we get a very odd result. This is because humantime uses
377// a duration of 30.44 days (where every day is 24 hours exactly) for
378// all months.
379assert_eq!(datetime + dur, civil::date(2024, 5, 31).at(10, 33, 36, 0));
380
381# Ok::<(), Box<dyn std::error::Error>>(())
382```
383
384The same is true for days when dealing with zoned date times:
385
386```
387use jiff::{civil, Span};
388
389let span: Span = "1 day".parse()?;
390let dur = humantime::parse_duration("1 day")?;
391
392let zdt = civil::date(2024, 3, 9).at(17, 0, 0, 0).in_tz("US/Eastern")?;
393
394// Adding 1 day gives the generally expected result of the same clock
395// time on the following day when adding a `Span`.
396assert_eq!(&zdt + span, civil::date(2024, 3, 10).at(17, 0, 0, 0).in_tz("US/Eastern")?);
397// But with humantime, all days are assumed to be exactly 24 hours. So
398// you get an instant in time that is 24 hours later, even when some
399// days are shorter and some are longer.
400assert_eq!(&zdt + dur, civil::date(2024, 3, 10).at(18, 0, 0, 0).in_tz("US/Eastern")?);
401
402// Notice also that this inaccuracy can occur merely by a duration that
403// _crosses_ a time zone transition boundary (like DST) at any point. It
404// doesn't require your datetimes to be "close" to when DST occurred.
405let dur = humantime::parse_duration("20 day")?;
406let zdt = civil::date(2024, 3, 1).at(17, 0, 0, 0).in_tz("US/Eastern")?;
407assert_eq!(&zdt + dur, civil::date(2024, 3, 21).at(18, 0, 0, 0).in_tz("US/Eastern")?);
408
409# Ok::<(), Box<dyn std::error::Error>>(())
410```
411
412It's worth pointing out that in some applications, the fixed values assigned
413by `humantime` might be perfectly acceptable. Namely, they introduce error
414into calculations, but the error might be small enough to be a non-issue in
415some applications. But this error _can_ be avoided and `humantime` commits
416it silently. Indeed, `humantime`'s API is itself not possible without either
417rejecting varying length units or assuming fixed values for them. This is
418because it parses varying length units but returns a duration expressed as a
419single 96-bit integer number of nanoseconds. In order to do this, you _must_
420assume a definite length for those varying units. To do this _correctly_, you
421really need to provide a reference date.
422
423For example, Jiff can parse `1 month` into a `std::time::Duration` too, but
424it requires parsing into a `Span` and then converting into a `Duration` by
425providing a reference date:
426
427```
428use std::time::Duration;
429
430use jiff::{civil, Span};
431
432let span: Span = "1 month".parse()?;
433// converts to signed duration
434let sdur = span.to_duration(civil::date(2024, 5, 1))?;
435// converts to standard library unsigned duration
436let dur = Duration::try_from(sdur)?;
437// exactly 31 days where each day is 24 hours long.
438assert_eq!(dur, Duration::from_secs(31 * 24 * 60 * 60));
439
440// Now change the reference date and notice that the
441// resulting duration is changed but still correct.
442let sdur = span.to_duration(civil::date(2024, 6, 1))?;
443let dur = Duration::try_from(sdur)?;
444// exactly 30 days where each day is 24 hours long.
445assert_eq!(dur, Duration::from_secs(30 * 24 * 60 * 60));
446
447# Ok::<(), Box<dyn std::error::Error>>(())
448```
449
450# Motivation
451
452This format was devised, in part, because the standard duration interchange
453format specified by [Temporal's ISO 8601 definition](super::temporal) is
454sub-optimal in two important respects:
455
4561. It doesn't support individual sub-second components.
4572. It is difficult to read.
458
459In the first case, ISO 8601 durations do support sub-second components, but are
460only expressible as fractional seconds. For example:
461
462```text
463PT1.100S
464```
465
466This is problematic in some cases because it doesn't permit distinguishing
467between some spans. For example, `1.second().milliseconds(100)` and
468`1100.milliseconds()` both serialize to the same ISO 8601 duration as shown
469above. At deserialization time, it's impossible to know what the span originally
470looked like. Thus, using the ISO 8601 format means the serialization and
471deserialization of [`Span`](crate::Span) values is lossy.
472
473In the second case, ISO 8601 durations appear somewhat difficult to quickly
474read. For example:
475
476```text
477P1Y2M3DT4H59M1.1S
478P1y2m3dT4h59m1.1S
479```
480
481When all of the unit designators are capital letters in particular (which
482is the default), everything runs together and it's hard for the eye to
483distinguish where digits stop and letters begin. Using lowercase letters for
484unit designators helps somewhat, but this is an extension to ISO 8601 that
485isn't broadly supported.
486
487The "friendly" format resolves both of these problems by permitting sub-second
488components and allowing the use of whitespace and longer unit designator labels
489to improve readability. For example, all of the following are equivalent and
490will parse to the same `Span`:
491
492```text
4931y 2mo 3d 4h 59m 1100ms
4941 year 2 months 3 days 4h59m1100ms
4951 year, 2 months, 3 days, 4h59m1100ms
4961 year, 2 months, 3 days, 4 hours 59 minutes 1100 milliseconds
497```
498
499At the same time, the friendly format continues to support fractional
500time components since they may be desirable in some cases. For example, all
501of the following are equivalent:
502
503```text
5041h 1m 1.5s
5051h 1m 1,5s
50601:01:01.5
50701:01:01,5
508```
509
510The idea with the friendly format is that end users who know how to write
511English durations are happy to both read and write durations in this format.
512And moreover, the format is flexible enough that end users generally don't need
513to stare at a grammar to figure out how to write a valid duration. Most of the
514intuitive things you'd expect to work will work.
515
516# Internationalization
517
518Currently, only US English unit designator labels are supported. In general,
519Jiff resists trying to solve the internationalization problem in favor
520of punting it to another crate, such as [`icu`] via [`jiff-icu`]. Jiff
521_could_ adopt unit designator labels for other languages, but it's not
522totally clear whether that's the right path to follow given the complexity
523of internationalization. If you'd like to discuss it, please
524[file an issue](https://github.com/BurntSushi/jiff/issues).
525
526# Grammar
527
528This section gives a more precise description of the "friendly" duration format
529in the form of a grammar.
530
531```text
532format =
533 format-signed-hms
534 | format-signed-designator
535
536format-signed-hms =
537 sign? format-hms
538
539format-hms =
540 [0-9]+ ':' [0-9]+ ':' [0-9]+ fractional?
541
542format-signed-designator =
543 sign? format-designator-units
544 | format-designator-units direction?
545format-designator-units =
546 years
547 | months
548 | weeks
549 | days
550 | hours
551 | minutes
552 | seconds
553 | milliseconds
554 | microseconds
555 | nanoseconds
556
557# This dance below is basically to ensure a few things:
558# First, that at least one unit appears. That is, that
559# we don't accept the empty string. Secondly, when a
560# fractional component appears in a time value, we don't
561# allow any subsequent units to appear. Thirdly, that
562# `HH:MM:SS[.f{1,9}]?` is allowed after years, months,
563# weeks or days.
564years =
565 unit-value unit-years comma? ws* format-hms
566 | unit-value unit-years comma? ws* months
567 | unit-value unit-years comma? ws* weeks
568 | unit-value unit-years comma? ws* days
569 | unit-value unit-years comma? ws* hours
570 | unit-value unit-years comma? ws* minutes
571 | unit-value unit-years comma? ws* seconds
572 | unit-value unit-years comma? ws* milliseconds
573 | unit-value unit-years comma? ws* microseconds
574 | unit-value unit-years comma? ws* nanoseconds
575 | unit-value unit-years
576months =
577 unit-value unit-months comma? ws* format-hms
578 | unit-value unit-months comma? ws* weeks
579 | unit-value unit-months comma? ws* days
580 | unit-value unit-months comma? ws* hours
581 | unit-value unit-months comma? ws* minutes
582 | unit-value unit-months comma? ws* seconds
583 | unit-value unit-months comma? ws* milliseconds
584 | unit-value unit-months comma? ws* microseconds
585 | unit-value unit-months comma? ws* nanoseconds
586 | unit-value unit-months
587weeks =
588 unit-value unit-weeks comma? ws* format-hms
589 | unit-value unit-weeks comma? ws* days
590 | unit-value unit-weeks comma? ws* hours
591 | unit-value unit-weeks comma? ws* minutes
592 | unit-value unit-weeks comma? ws* seconds
593 | unit-value unit-weeks comma? ws* milliseconds
594 | unit-value unit-weeks comma? ws* microseconds
595 | unit-value unit-weeks comma? ws* nanoseconds
596 | unit-value unit-weeks
597days =
598 unit-value unit-days comma? ws* format-hms
599 | unit-value unit-days comma? ws* hours
600 | unit-value unit-days comma? ws* minutes
601 | unit-value unit-days comma? ws* seconds
602 | unit-value unit-days comma? ws* milliseconds
603 | unit-value unit-days comma? ws* microseconds
604 | unit-value unit-days comma? ws* nanoseconds
605 | unit-value unit-days
606hours =
607 unit-value unit-hours comma? ws* minutes
608 | unit-value unit-hours comma? ws* seconds
609 | unit-value unit-hours comma? ws* milliseconds
610 | unit-value unit-hours comma? ws* microseconds
611 | unit-value unit-hours comma? ws* nanoseconds
612 | unit-value fractional? ws* unit-hours
613minutes =
614 unit-value unit-minutes comma? ws* seconds
615 | unit-value unit-minutes comma? ws* milliseconds
616 | unit-value unit-minutes comma? ws* microseconds
617 | unit-value unit-minutes comma? ws* nanoseconds
618 | unit-value fractional? ws* unit-minutes
619seconds =
620 unit-value unit-seconds comma? ws* milliseconds
621 | unit-value unit-seconds comma? ws* microseconds
622 | unit-value unit-seconds comma? ws* nanoseconds
623 | unit-value fractional? ws* unit-seconds
624milliseconds =
625 unit-value unit-milliseconds comma? ws* microseconds
626 | unit-value unit-milliseconds comma? ws* nanoseconds
627 | unit-value fractional? ws* unit-milliseconds
628microseconds =
629 unit-value unit-microseconds comma? ws* nanoseconds
630 | unit-value fractional? ws* unit-microseconds
631nanoseconds =
632 unit-value fractional? ws* unit-nanoseconds
633
634unit-value = [0-9]+ [ws*]
635unit-years = 'years' | 'year' | 'yrs' | 'yr' | 'y'
636unit-months = 'months' | 'month' | 'mos' | 'mo'
637unit-weeks = 'weeks' | 'week' | 'wks' | 'wk' | 'w'
638unit-days = 'days' | 'day' | 'd'
639unit-hours = 'hours' | 'hour' | 'hrs' | 'hr' | 'h'
640unit-minutes = 'minutes' | 'minute' | 'mins' | 'min' | 'm'
641unit-seconds = 'seconds' | 'second' | 'secs' | 'sec' | 's'
642unit-milliseconds =
643 'milliseconds'
644 | 'millisecond'
645 | 'millis'
646 | 'milli'
647 | 'msecs'
648 | 'msec'
649 | 'ms'
650unit-microseconds =
651 'microseconds'
652 | 'microsecond'
653 | 'micros'
654 | 'micro'
655 | 'usecs'
656 | 'usec'
657 | 'µ' (U+00B5 MICRO SIGN) 'secs'
658 | 'µ' (U+00B5 MICRO SIGN) 'sec'
659 | 'us'
660 | 'µ' (U+00B5 MICRO SIGN) 's'
661unit-nanoseconds =
662 'nanoseconds' | 'nanosecond' | 'nanos' | 'nano' | 'nsecs' | 'nsec' | 'ns'
663
664fractional = decimal-separator decimal-fraction
665decimal-separator = '.' | ','
666decimal-fraction = [0-9]{1,9}
667
668sign = '+' | '-'
669direction = ws 'ago'
670comma = ',' ws
671ws =
672 U+0020 SPACE
673 | U+0009 HORIZONTAL TAB
674 | U+000A LINE FEED
675 | U+000C FORM FEED
676 | U+000D CARRIAGE RETURN
677```
678
679One thing not specified by the grammar above are maximum values. Namely,
680there are no specific maximum values imposed for each individual unit, nor
681a maximum value for the entire duration (say, when converted to nanoseconds).
682Instead, implementations are expected to impose their own limitations.
683
684For Jiff, a `Span` is more limited than a `SignedDuration`. For example, a the
685year component of a `Span` is limited to `[-19,999, 19,999]`. In contrast,
686a `SignedDuration` is a 96-bit signed integer number of nanoseconds with no
687particular limits on the individual units. They just can't combine to something
688that overflows a 96-bit signed integer number of nanoseconds. (And parsing into
689a `SignedDuration` directly only supports units of hours or smaller, since
690bigger units do not have an invariant length.) In general, implementations
691should support a "reasonable" range of values.
692
693[`humantime`]: https://docs.rs/humantime
694[`humantime-serde`]: https://docs.rs/humantime-serde
695[`icu`]: https://docs.rs/icu
696[`jiff-icu`]: https://docs.rs/jiff-icu
697*/
698
699pub use self::{
700 parser::SpanParser,
701 printer::{Designator, Direction, FractionalUnit, Spacing, SpanPrinter},
702};
703
704/// The default span/duration parser that we use.
705pub(crate) static DEFAULT_SPAN_PARSER: SpanParser = SpanParser::new();
706
707/// The default span/duration printer that we use.
708pub(crate) static DEFAULT_SPAN_PRINTER: SpanPrinter = SpanPrinter::new();
709
710mod parser;
711mod parser_label;
712mod printer;