| 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 | |