1 | /*! |
2 | A bespoke but easy to read format for [`Span`](crate::Span) and |
3 | [`SignedDuration`](crate::SignedDuration). |
4 | |
5 | The "friendly" duration format is meant to be an alternative to [Temporal's |
6 | ISO 8601 duration format](super::temporal) that is both easier to read and can |
7 | losslessly serialize and deserialize all `Span` values. |
8 | |
9 | Here are a variety of examples showing valid friendly durations for `Span`: |
10 | |
11 | ``` |
12 | use jiff::{Span, ToSpan}; |
13 | |
14 | let 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 | ]; |
54 | for (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 | |
66 | Note that for a `SignedDuration`, only units up to hours are supported. If you |
67 | need to support bigger units, then you'll need to convert it to a `Span` before |
68 | printing to the friendly format (or parse into a `Span` and then convert to a |
69 | `SignedDuration`). |
70 | |
71 | # Integration points |
72 | |
73 | While this module can of course be used to parse and print durations in the |
74 | friendly format, in most cases, you don't have to. Namely, it is already |
75 | integrated into the `Span` and `SignedDuration` types. |
76 | |
77 | For example, the friendly format can be used by invoking the "alternate" |
78 | format when using the `std::fmt::Display` trait implementation: |
79 | |
80 | ``` |
81 | use jiff::{SignedDuration, ToSpan}; |
82 | |
83 | let span = 2.months().days(35).hours(2).minutes(30); |
84 | assert_eq!(format!("{span}" ), "P2M35DT2H30M" ); // ISO 8601 |
85 | assert_eq!(format!("{span:#}" ), "2mo 35d 2h 30m" ); // "friendly" |
86 | |
87 | let sdur = SignedDuration::new(2 * 60 * 60 + 30 * 60, 123_456_789); |
88 | assert_eq!(format!("{sdur}" ), "PT2H30M0.123456789S" ); // ISO 8601 |
89 | assert_eq!(format!("{sdur:#}" ), "2h 30m 123ms 456µs 789ns" ); // "friendly" |
90 | ``` |
91 | |
92 | Both `Span` and `SignedDuration` use the "friendly" format for its |
93 | `std::fmt::Debug` trait implementation: |
94 | |
95 | ``` |
96 | use jiff::{SignedDuration, ToSpan}; |
97 | |
98 | let span = 2.months().days(35).hours(2).minutes(30); |
99 | assert_eq!(format!("{span:?}" ), "2mo 35d 2h 30m" ); |
100 | |
101 | let sdur = SignedDuration::new(2 * 60 * 60 + 30 * 60, 123_456_789); |
102 | assert_eq!(format!("{sdur:?}" ), "2h 30m 123ms 456µs 789ns" ); |
103 | ``` |
104 | |
105 | Both `Span` and `SignedDuration` support parsing the ISO 8601 _and_ friendly |
106 | formats via its `std::str::FromStr` trait: |
107 | |
108 | ``` |
109 | use jiff::{SignedDuration, Span, ToSpan}; |
110 | |
111 | let expected = 2.months().days(35).hours(2).minutes(30); |
112 | let span: Span = "2 months, 35 days, 02:30:00" .parse()?; |
113 | assert_eq!(span, expected.fieldwise()); |
114 | let span: Span = "P2M35DT2H30M" .parse()?; |
115 | assert_eq!(span, expected.fieldwise()); |
116 | |
117 | let expected = SignedDuration::new(2 * 60 * 60 + 30 * 60, 123_456_789); |
118 | let sdur: SignedDuration = "2h 30m 0,123456789s" .parse()?; |
119 | assert_eq!(sdur, expected); |
120 | let sdur: SignedDuration = "PT2h30m0.123456789s" .parse()?; |
121 | assert_eq!(sdur, expected); |
122 | |
123 | # Ok::<(), Box<dyn std::error::Error>>(()) |
124 | ``` |
125 | |
126 | If you need to parse _only_ the friendly format, then that would be a good use |
127 | case for using [`SpanParser`] in this module. |
128 | |
129 | Finally, when the `serde` crate feature is enabled, the friendly format is |
130 | automatically supported via the `serde::Deserialize` trait implementation, just |
131 | like for the `std::str::FromStr` trait above. However, for `serde::Serialize`, |
132 | both types use ISO 8601. In order to serialize the friendly format, |
133 | you'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 | ``` |
137 | use jiff::{ToSpan, Span}; |
138 | |
139 | #[derive(Debug, serde::Deserialize, serde::Serialize)] |
140 | struct Record { |
141 | #[serde( |
142 | serialize_with = "jiff::fmt::serde::span::friendly::compact::required" |
143 | )] |
144 | span: Span, |
145 | } |
146 | |
147 | let json = r#"{"span":"1 year 2 months 36 hours 1100ms"}"# ; |
148 | let got: Record = serde_json::from_str(&json)?; |
149 | assert_eq!( |
150 | got.span.fieldwise(), |
151 | 1.year().months(2).hours(36).milliseconds(1100), |
152 | ); |
153 | |
154 | let expected = r#"{"span":"1y 2mo 36h 1100ms"}"# ; |
155 | assert_eq!(serde_json::to_string(&got).unwrap(), expected); |
156 | |
157 | # Ok::<(), Box<dyn std::error::Error>>(()) |
158 | ``` |
159 | |
160 | The ISO 8601 format is used by default since it is part of a standard and is |
161 | more widely accepted. That is, if you need an interoperable interchange format, |
162 | then ISO 8601 is probably the right choice. |
163 | |
164 | # Rounding |
165 | |
166 | The printer in this module has no options for rounding. Instead, it is intended |
167 | for users to round a [`Span`](crate::Span) first, and then print it. The idea |
168 | is that printing a `Span` is a relatively "dumb" operation that just emits |
169 | whatever units are non-zero in the `Span`. This is possible with a `Span` |
170 | because it represents each unit distinctly. (With a [`std::time::Duration`] or |
171 | a [`jiff::SignedDuration`](crate::SignedDuration), more functionality would |
172 | need to be coupled with the printing logic to achieve a similar result.) |
173 | |
174 | For example, if you want to print the duration since someone posted a comment |
175 | to an English speaking end user, precision below one half hour might be "too |
176 | much detail." You can remove this by rounding the `Span` to the nearest half |
177 | hour before printing: |
178 | |
179 | ``` |
180 | use jiff::{civil, RoundMode, ToSpan, Unit, ZonedDifference}; |
181 | |
182 | let commented_at = civil::date(2024, 8, 1).at(19, 29, 13, 123_456_789).in_tz("US/Eastern" )?; |
183 | let 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. |
186 | let span = now.since((Unit::Year, &commented_at))?; |
187 | assert_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. |
192 | let rounded = now.since( |
193 | ZonedDifference::new(&commented_at) |
194 | .smallest(Unit::Minute) |
195 | .largest(Unit::Year) |
196 | .mode(RoundMode::HalfExpand) |
197 | .increment(30), |
198 | )?; |
199 | assert_eq!(format!("{rounded:#}" ), "4mo 24d 17h 30m" ); |
200 | |
201 | # Ok::<(), Box<dyn std::error::Error>>(()) |
202 | ``` |
203 | |
204 | # Comparison with the [`humantime`] crate |
205 | |
206 | To a first approximation, Jiff should cover all `humantime` use cases, |
207 | including [`humantime-serde`] for serialization support. |
208 | |
209 | To a second approximation, it was a design point of the friendly format to be |
210 | mostly interoperable with what `humantime` supports. For example, any duration |
211 | string formatted by `humantime` at time of writing is also a valid friendly |
212 | duration: |
213 | |
214 | ``` |
215 | use std::time::Duration; |
216 | |
217 | use jiff::{Span, ToSpan}; |
218 | |
219 | // Just a duration that includes as many unit designator labels as possible. |
220 | let dur = Duration::new( |
221 | 2 * 31_557_600 + 1 * 2_630_016 + 15 * 86400 + 5 * 3600 + 59 * 60 + 1, |
222 | 123_456_789, |
223 | ); |
224 | let formatted = humantime::format_duration(dur).to_string(); |
225 | assert_eq!(formatted, "2years 1month 15days 5h 59m 1s 123ms 456us 789ns" ); |
226 | |
227 | let span: Span = formatted.parse()?; |
228 | let 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); |
238 | assert_eq!(span, expected.fieldwise()); |
239 | |
240 | # Ok::<(), Box<dyn std::error::Error>>(()) |
241 | ``` |
242 | |
243 | The above somewhat relies on the implementation details of `humantime`. Namely, |
244 | not everything parseable by `humantime` is also parseable by the friendly |
245 | format (and vice versa). For example, `humantime` parses `M` as a label for |
246 | months, but the friendly format specifically eschews `M` because of its |
247 | confusability with minutes: |
248 | |
249 | ``` |
250 | use std::time::Duration; |
251 | |
252 | let dur = humantime::parse_duration("1M" )?; |
253 | // The +38,016 is because `humantime` assigns 30.44 24-hour days to all months. |
254 | assert_eq!(dur, Duration::new(30 * 24 * 60 * 60 + 38_016, 0)); |
255 | |
256 | // In contrast, Jiff will reject `1M`: |
257 | assert_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 | |
265 | In the other direction, Jiff's default formatting for the friendly duration |
266 | isn't always parsable by `humantime`. This is because, for example, depending |
267 | on the configuration, Jiff may use `mo` and `mos` for months, and `µs` for |
268 | microseconds, none of which are supported by `humantime`. If you need it, to |
269 | ensure `humantime` can parse a Jiff formatted friendly duration, Jiff provides |
270 | a special mode that attempts compatibility with `humantime`: |
271 | |
272 | ``` |
273 | use jiff::{fmt::friendly::{Designator, SpanPrinter}, ToSpan}; |
274 | |
275 | |
276 | let 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 | |
287 | let printer = SpanPrinter::new().designator(Designator::HumanTime); |
288 | assert_eq!( |
289 | printer.span_to_string(&span), |
290 | "2y 1month 15d 5h 59m 1s 123ms 456us 789ns" , |
291 | ); |
292 | ``` |
293 | |
294 | It's hard to provide solid guarantees here because `humantime`'s behavior could |
295 | change, but at time of writing, `humantime` has not changed much in quite a |
296 | long time (its last release is almost 4 years ago at time of writing). So the |
297 | current behavior is likely pretty safe to rely upon. |
298 | |
299 | More generally, the friendly format is more flexible than what `humantime` |
300 | supports. For example, the friendly format incorporates `HH:MM:SS` and |
301 | fractional time units. It also supports more unit labels and permits commas |
302 | to separate units. |
303 | |
304 | ``` |
305 | use jiff::SignedDuration; |
306 | |
307 | // 10 hours and 30 minutes |
308 | let expected = SignedDuration::new(10 * 60 * 60 + 30 * 60, 0); |
309 | assert_eq!(expected, "10h30m" .parse()?); |
310 | assert_eq!(expected, "10hrs 30mins" .parse()?); |
311 | assert_eq!(expected, "10 hours 30 minutes" .parse()?); |
312 | assert_eq!(expected, "10 hours, 30 minutes" .parse()?); |
313 | assert_eq!(expected, "10:30:00" .parse()?); |
314 | assert_eq!(expected, "10.5 hours" .parse()?); |
315 | |
316 | # Ok::<(), Box<dyn std::error::Error>>(()) |
317 | ``` |
318 | |
319 | Finally, it's important to point out that `humantime` only supports parsing |
320 | variable width units like years, months and days by virtue of assigning fixed |
321 | static values to them that aren't always correct. In contrast, Jiff always |
322 | gets this right and specifically prevents you from getting it wrong. |
323 | |
324 | To begin, Jiff returns an error if you try to parse a varying unit into a |
325 | [`SignedDuration`](crate::SignedDuration): |
326 | |
327 | ``` |
328 | use jiff::SignedDuration; |
329 | |
330 | // works fine |
331 | assert_eq!( |
332 | "1 hour" .parse::<SignedDuration>().unwrap(), |
333 | SignedDuration::from_hours(1), |
334 | ); |
335 | // Jiff is saving you from doing something wrong |
336 | assert_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 | |
342 | As the error message suggests, parsing into a [`Span`](crate::Span) works fine: |
343 | |
344 | ``` |
345 | use jiff::Span; |
346 | |
347 | assert_eq!("1 day" .parse::<Span>().unwrap(), Span::new().days(1).fieldwise()); |
348 | ``` |
349 | |
350 | Jiff has this behavior because it's not possible to determine, in general, |
351 | how long "1 day" (or "1 month" or "1 year") is without a reference date. |
352 | Since a `SignedDuration` (along with a [`std::time::Duration`]) does not |
353 | support expressing durations in anything other than a 96-bit integer number of |
354 | nanoseconds, it's not possible to represent concepts like "1 month." But a |
355 | [`Span`](crate::Span) can. |
356 | |
357 | To see this more concretely, consider the different behavior resulting from |
358 | using `humantime` to parse durations and adding them to a date: |
359 | |
360 | ``` |
361 | use jiff::{civil, Span}; |
362 | |
363 | let span: Span = "1 month" .parse()?; |
364 | let dur = humantime::parse_duration("1 month" )?; |
365 | |
366 | let 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. |
370 | assert_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. |
375 | assert_eq!(datetime + dur, civil::date(2024, 5, 31).at(10, 33, 36, 0)); |
376 | |
377 | # Ok::<(), Box<dyn std::error::Error>>(()) |
378 | ``` |
379 | |
380 | The same is true for days when dealing with zoned date times: |
381 | |
382 | ``` |
383 | use jiff::{civil, Span}; |
384 | |
385 | let span: Span = "1 day" .parse()?; |
386 | let dur = humantime::parse_duration("1 day" )?; |
387 | |
388 | let 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`. |
392 | assert_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. |
396 | assert_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. |
401 | let dur = humantime::parse_duration("20 day" )?; |
402 | let zdt = civil::date(2024, 3, 1).at(17, 0, 0, 0).in_tz("US/Eastern" )?; |
403 | assert_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 | |
408 | It's worth pointing out that in some applications, the fixed values assigned |
409 | by `humantime` might be perfectly acceptable. Namely, they introduce error |
410 | into calculations, but the error might be small enough to be a non-issue in |
411 | some applications. But this error _can_ be avoided and `humantime` commits |
412 | it silently. Indeed, `humantime`'s API is itself not possible without either |
413 | rejecting varying length units or assuming fixed values for them. This is |
414 | because it parses varying length units but returns a duration expressed as a |
415 | single 96-bit integer number of nanoseconds. In order to do this, you _must_ |
416 | assume a definite length for those varying units. To do this _correctly_, you |
417 | really need to provide a reference date. |
418 | |
419 | For example, Jiff can parse `1 month` into a `std::time::Duration` too, but |
420 | it requires parsing into a `Span` and then converting into a `Duration` by |
421 | providing a reference date: |
422 | |
423 | ``` |
424 | use std::time::Duration; |
425 | |
426 | use jiff::{civil, Span}; |
427 | |
428 | let span: Span = "1 month" .parse()?; |
429 | // converts to signed duration |
430 | let sdur = span.to_duration(civil::date(2024, 5, 1))?; |
431 | // converts to standard library unsigned duration |
432 | let dur = Duration::try_from(sdur)?; |
433 | // exactly 31 days where each day is 24 hours long. |
434 | assert_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. |
438 | let sdur = span.to_duration(civil::date(2024, 6, 1))?; |
439 | let dur = Duration::try_from(sdur)?; |
440 | // exactly 30 days where each day is 24 hours long. |
441 | assert_eq!(dur, Duration::from_secs(30 * 24 * 60 * 60)); |
442 | |
443 | # Ok::<(), Box<dyn std::error::Error>>(()) |
444 | ``` |
445 | |
446 | # Motivation |
447 | |
448 | This format was devised, in part, because the standard duration interchange |
449 | format specified by [Temporal's ISO 8601 definition](super::temporal) is |
450 | sub-optimal in two important respects: |
451 | |
452 | 1. It doesn't support individual sub-second components. |
453 | 2. It is difficult to read. |
454 | |
455 | In the first case, ISO 8601 durations do support sub-second components, but are |
456 | only expressible as fractional seconds. For example: |
457 | |
458 | ```text |
459 | PT1.100S |
460 | ``` |
461 | |
462 | This is problematic in some cases because it doesn't permit distinguishing |
463 | between some spans. For example, `1.second().milliseconds(100)` and |
464 | `1100.milliseconds()` both serialize to the same ISO 8601 duration as shown |
465 | above. At deserialization time, it's impossible to know what the span originally |
466 | looked like. Thus, using the ISO 8601 format means the serialization and |
467 | deserialization of [`Span`](crate::Span) values is lossy. |
468 | |
469 | In the second case, ISO 8601 durations appear somewhat difficult to quickly |
470 | read. For example: |
471 | |
472 | ```text |
473 | P1Y2M3DT4H59M1.1S |
474 | P1y2m3dT4h59m1.1S |
475 | ``` |
476 | |
477 | When all of the unit designators are capital letters in particular (which |
478 | is the default), everything runs together and it's hard for the eye to |
479 | distinguish where digits stop and letters begin. Using lowercase letters for |
480 | unit designators helps somewhat, but this is an extension to ISO 8601 that |
481 | isn't broadly supported. |
482 | |
483 | The "friendly" format resolves both of these problems by permitting sub-second |
484 | components and allowing the use of whitespace and longer unit designator labels |
485 | to improve readability. For example, all of the following are equivalent and |
486 | will parse to the same `Span`: |
487 | |
488 | ```text |
489 | 1y 2mo 3d 4h 59m 1100ms |
490 | 1 year 2 months 3 days 4h59m1100ms |
491 | 1 year, 2 months, 3 days, 4h59m1100ms |
492 | 1 year, 2 months, 3 days, 4 hours 59 minutes 1100 milliseconds |
493 | ``` |
494 | |
495 | At the same time, the friendly format continues to support fractional |
496 | time components since they may be desirable in some cases. For example, all |
497 | of the following are equivalent: |
498 | |
499 | ```text |
500 | 1h 1m 1.5s |
501 | 1h 1m 1,5s |
502 | 01:01:01.5 |
503 | 01:01:01,5 |
504 | ``` |
505 | |
506 | The idea with the friendly format is that end users who know how to write |
507 | English durations are happy to both read and write durations in this format. |
508 | And moreover, the format is flexible enough that end users generally don't need |
509 | to stare at a grammar to figure out how to write a valid duration. Most of the |
510 | intuitive things you'd expect to work will work. |
511 | |
512 | # Internationalization |
513 | |
514 | Currently, only US English unit designator labels are supported. In general, |
515 | Jiff resists trying to solve the internationalization problem in favor |
516 | of 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 |
518 | totally clear whether that's the right path to follow given the complexity |
519 | of internationalization. If you'd like to discuss it, please |
520 | [file an issue](https://github.com/BurntSushi/jiff/issues). |
521 | |
522 | # Grammar |
523 | |
524 | This section gives a more precise description of the "friendly" duration format |
525 | in the form of a grammar. |
526 | |
527 | ```text |
528 | format = |
529 | format-signed-hms |
530 | | format-signed-designator |
531 | |
532 | format-signed-hms = |
533 | sign? format-hms |
534 | |
535 | format-hms = |
536 | [0-9]+ ':' [0-9]+ ':' [0-9]+ fractional? |
537 | |
538 | format-signed-designator = |
539 | sign? format-designator-units |
540 | | format-designator-units direction? |
541 | format-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. |
560 | years = |
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 |
572 | months = |
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 |
583 | weeks = |
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 |
593 | days = |
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 |
602 | hours = |
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 |
609 | minutes = |
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 |
615 | seconds = |
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 |
620 | milliseconds = |
621 | unit-value unit-milliseconds comma? ws* microseconds |
622 | | unit-value unit-milliseconds comma? ws* nanoseconds |
623 | | unit-value fractional? ws* unit-milliseconds |
624 | microseconds = |
625 | unit-value unit-microseconds comma? ws* nanoseconds |
626 | | unit-value fractional? ws* unit-microseconds |
627 | nanoseconds = |
628 | unit-value fractional? ws* unit-nanoseconds |
629 | |
630 | unit-value = [0-9]+ [ws*] |
631 | unit-years = 'years' | 'year' | 'yrs' | 'yr' | 'y' |
632 | unit-months = 'months' | 'month' | 'mos' | 'mo' |
633 | unit-weeks = 'weeks' | 'week' | 'wks' | 'wk' | 'w' |
634 | unit-days = 'days' | 'day' | 'd' |
635 | unit-hours = 'hours' | 'hour' | 'hrs' | 'hr' | 'h' |
636 | unit-minutes = 'minutes' | 'minute' | 'mins' | 'min' | 'm' |
637 | unit-seconds = 'seconds' | 'second' | 'secs' | 'sec' | 's' |
638 | unit-milliseconds = |
639 | 'milliseconds' |
640 | | 'millisecond' |
641 | | 'millis' |
642 | | 'milli' |
643 | | 'msecs' |
644 | | 'msec' |
645 | | 'ms' |
646 | unit-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' |
657 | unit-nanoseconds = |
658 | 'nanoseconds' | 'nanosecond' | 'nanos' | 'nano' | 'nsecs' | 'nsec' | 'ns' |
659 | |
660 | fractional = decimal-separator decimal-fraction |
661 | decimal-separator = '.' | ',' |
662 | decimal-fraction = [0-9]{1,9} |
663 | |
664 | sign = '+' | '-' |
665 | direction = ws 'ago' |
666 | comma = ',' ws |
667 | ws = |
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 | |
675 | One thing not specified by the grammar above are maximum values. Namely, |
676 | there are no specific maximum values imposed for each individual unit, nor |
677 | a maximum value for the entire duration (say, when converted to nanoseconds). |
678 | Instead, implementations are expected to impose their own limitations. |
679 | |
680 | For Jiff, a `Span` is more limited than a `SignedDuration`. For example, a the |
681 | year component of a `Span` is limited to `[-19,999, 19,999]`. In contrast, |
682 | a `SignedDuration` is a 96-bit signed integer number of nanoseconds with no |
683 | particular limits on the individual units. They just can't combine to something |
684 | that overflows a 96-bit signed integer number of nanoseconds. (And parsing into |
685 | a `SignedDuration` directly only supports units of hours or smaller, since |
686 | bigger units do not have an invariant length.) In general, implementations |
687 | should 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 | |
695 | pub use self::{ |
696 | parser::SpanParser, |
697 | printer::{Designator, Direction, FractionalUnit, Spacing, SpanPrinter}, |
698 | }; |
699 | |
700 | /// The default span/duration parser that we use. |
701 | pub(crate) static DEFAULT_SPAN_PARSER: SpanParser = SpanParser::new(); |
702 | |
703 | /// The default span/duration printer that we use. |
704 | pub(crate) static DEFAULT_SPAN_PRINTER: SpanPrinter = SpanPrinter::new(); |
705 | |
706 | mod parser; |
707 | mod parser_label; |
708 | mod printer; |
709 | |