Skip to content

Commit

Permalink
temporal: use uppercase unit designator labels by default
Browse files Browse the repository at this point in the history
This somewhat revives #22, but makes it possible to restore the previous
behavior by enabling `jiff::fmt::temporal::SpanPrinter::lowercase`.

The main motivation here is also detailed in #22, and it came up again
in #188. I was previously reluctant to do this because I find
`P1Y2M3DT4H5M6S` hideously difficult to read and `P1y2m3dT4h5m6s`
somewhat less difficult to read. But now that `jiff::fmt::friendly` is a
thing and users have easy access to a more readable duration display
format, I feel less bad about this. It's still a shame that it's the
default via `span.to_string()`, but I tried to sprinkle a few
`format!("{span:#}")` in places to nudge users toward the friendly
format.

It's a shame more systems don't accept lowercase unit designator labels,
but since Jiff uses the ISO 8601 by default specifically for its
interoperability, it makes sense to be as interoperable as we can by
default.

Fixes #188
  • Loading branch information
BurntSushi committed Jan 2, 2025
1 parent 21218a0 commit d100b15
Show file tree
Hide file tree
Showing 17 changed files with 337 additions and 285 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ Bug fixes:

* [#155](https://github.com/BurntSushi/jiff/issues/155):
Relax `strftime` format strings from ASCII-only to all of UTF-8.
* [#188](https://github.com/BurntSushi/jiff/issues/188):
`Span` and `SignedDuration` now use uppercase unit designator labels in their
default ISO 8601 `Display` implementation.


0.1.18 (2024-12-31)
Expand Down
2 changes: 1 addition & 1 deletion COMPARE.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ use jiff::{Span, ToSpan};
fn main() -> anyhow::Result<()> {
let span = 5.years().months(2).days(1).hours(20);
let json = serde_json::to_string_pretty(&span)?;
assert_eq!(json, "\"P5y2m1dT20h\"");
assert_eq!(json, "\"P5Y2M1DT20H\"");

let got: Span = serde_json::from_str(&json)?;
assert_eq!(got, span);
Expand Down
4 changes: 2 additions & 2 deletions src/civil/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1635,11 +1635,11 @@ impl Date {
///
/// // The default limits durations to using "days" as the biggest unit.
/// let span = d1.until(d2)?;
/// assert_eq!(span.to_string(), "P8456d");
/// assert_eq!(span.to_string(), "P8456D");
///
/// // But we can ask for units all the way up to years.
/// let span = d1.until((Unit::Year, d2))?;
/// assert_eq!(span.to_string(), "P23y1m24d");
/// assert_eq!(span.to_string(), "P23Y1M24D");
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
Expand Down
4 changes: 2 additions & 2 deletions src/civil/datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1775,11 +1775,11 @@ impl DateTime {
///
/// // The default limits durations to using "days" as the biggest unit.
/// let span = dt1.until(dt2)?;
/// assert_eq!(span.to_string(), "P8456dT12h5m29.9999965s");
/// assert_eq!(span.to_string(), "P8456DT12H5M29.9999965S");
///
/// // But we can ask for units all the way up to years.
/// let span = dt1.until((Unit::Year, dt2))?;
/// assert_eq!(span.to_string(), "P23y1m24dT12h5m29.9999965s");
/// assert_eq!(span.to_string(), "P23Y1M24DT12H5M29.9999965S");
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
Expand Down
4 changes: 2 additions & 2 deletions src/civil/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1198,12 +1198,12 @@ impl Time {
///
/// // The default limits spans to using "hours" as the biggest unit.
/// let span = t1.until(t2)?;
/// assert_eq!(span.to_string(), "PT12h5m29.9999965s");
/// assert_eq!(span.to_string(), "PT12H5M29.9999965S");
///
/// // But we can ask for smaller units, like capping the biggest unit
/// // to minutes instead of hours.
/// let span = t1.until((Unit::Minute, t2))?;
/// assert_eq!(span.to_string(), "PT725m29.9999965s");
/// assert_eq!(span.to_string(), "PT725M29.9999965S");
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
Expand Down
13 changes: 7 additions & 6 deletions src/fmt/friendly/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ format when using the `std::fmt::Display` trait implementation:
use jiff::{SignedDuration, ToSpan};
let span = 2.months().days(35).hours(2).minutes(30);
assert_eq!(format!("{span}"), "P2m35dT2h30m"); // ISO 8601
assert_eq!(format!("{span}"), "P2M35DT2H30M"); // ISO 8601
assert_eq!(format!("{span:#}"), "2mo 35d 2h 30m"); // "friendly"
let sdur = SignedDuration::new(2 * 60 * 60 + 30 * 60, 123_456_789);
assert_eq!(format!("{sdur}"), "PT2h30m0.123456789s"); // ISO 8601
assert_eq!(format!("{sdur}"), "PT2H30M0.123456789S"); // ISO 8601
assert_eq!(format!("{sdur:#}"), "2h 30m 123ms 456µs 789ns"); // "friendly"
```
Expand Down Expand Up @@ -467,10 +467,11 @@ P1Y2M3DT4H59M1.1S
P1y2m3dT4h59m1.1S
```
When all of the unit designators are capital letters in particular, everything
runs together and it's hard for the eye to distinguish where digits stop and
letters begin. Using lowercase letters for unit designators helps somewhat,
but this is an extension to ISO 8601 that isn't broadly supported.
When all of the unit designators are capital letters in particular (which
is the default), everything runs together and it's hard for the eye to
distinguish where digits stop and letters begin. Using lowercase letters for
unit designators helps somewhat, but this is an extension to ISO 8601 that
isn't broadly supported.
The "friendly" format resolves both of these problems by permitting sub-second
components and allowing the use of whitespace and longer unit designator labels
Expand Down
214 changes: 107 additions & 107 deletions src/fmt/friendly/parser.rs

Large diffs are not rendered by default.

45 changes: 36 additions & 9 deletions src/fmt/temporal/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,11 @@ But there are some details not easily captured by a simple regular expression:
* At least one unit must be specified. To write a zero span, specify `0` for
any unit. For example, `P0d` and `PT0s` are equivalent.
* The format is case insensitive. The printer will by default capitalize the
`P` and `T` designators, but lowercase the unit designators.
* The format is case insensitive. The printer will by default capitalize all
designators, but the unit designators can be configured to use lowercase with
[`SpanPrinter::lowercase`]. For example, `P3y1m10dT5h` instead of
`P3Y1M10DT5H`. You might prefer lowercase since you may find it easier to read.
However, it is an extension to ISO 8601 and isn't as broadly supported.
* Hours, minutes or seconds may be fractional. And the only units that may be
fractional are the lowest units.
* A span like `P99999999999y` is invalid because it exceeds the allowable range
Expand Down Expand Up @@ -1566,7 +1569,7 @@ impl SpanParser {
/// let mut buf = vec![];
/// // Printing to a `Vec<u8>` can never fail.
/// PRINTER.print_span(&span, &mut buf).unwrap();
/// assert_eq!(buf, "PT48m".as_bytes());
/// assert_eq!(buf, "PT48M".as_bytes());
/// ```
///
/// # Example: using adapters with `std::io::Write` and `std::fmt::Write`
Expand Down Expand Up @@ -1605,6 +1608,30 @@ impl SpanPrinter {
SpanPrinter { p: printer::SpanPrinter::new() }
}

/// Use lowercase for unit designator labels.
///
/// By default, unit designator labels are written in uppercase.
///
/// # Example
///
/// This shows the difference between the default (uppercase) and enabling
/// lowercase. Lowercase unit designator labels tend to be easier to read
/// (in this author's opinion), but they aren't as broadly supported since
/// they are an extension to ISO 8601.
///
/// ```
/// use jiff::{fmt::temporal::SpanPrinter, ToSpan};
///
/// let span = 5.years().days(10).hours(1);
/// let printer = SpanPrinter::new();
/// assert_eq!(printer.span_to_string(&span), "P5Y10DT1H");
/// assert_eq!(printer.lowercase(true).span_to_string(&span), "P5y10dT1h");
/// ```
#[inline]
pub const fn lowercase(self, yes: bool) -> SpanPrinter {
SpanPrinter { p: self.p.lowercase(yes) }
}

/// Format a `Span` into a string.
///
/// This is a convenience routine for [`SpanPrinter::print_span`] with
Expand All @@ -1618,7 +1645,7 @@ impl SpanPrinter {
/// const PRINTER: SpanPrinter = SpanPrinter::new();
///
/// let span = 3.years().months(5);
/// assert_eq!(PRINTER.span_to_string(&span), "P3y5m");
/// assert_eq!(PRINTER.span_to_string(&span), "P3Y5M");
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
Expand Down Expand Up @@ -1646,8 +1673,8 @@ impl SpanPrinter {
/// const PRINTER: SpanPrinter = SpanPrinter::new();
///
/// let dur = SignedDuration::new(86_525, 123_000_789);
/// assert_eq!(PRINTER.duration_to_string(&dur), "PT24h2m5.123000789s");
/// assert_eq!(PRINTER.duration_to_string(&-dur), "-PT24h2m5.123000789s");
/// assert_eq!(PRINTER.duration_to_string(&dur), "PT24H2M5.123000789S");
/// assert_eq!(PRINTER.duration_to_string(&-dur), "-PT24H2M5.123000789S");
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
Expand Down Expand Up @@ -1683,7 +1710,7 @@ impl SpanPrinter {
/// let mut buf = String::new();
/// // Printing to a `String` can never fail.
/// PRINTER.print_span(&span, &mut buf).unwrap();
/// assert_eq!(buf, "P3y5m");
/// assert_eq!(buf, "P3Y5M");
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
Expand Down Expand Up @@ -1719,12 +1746,12 @@ impl SpanPrinter {
/// let mut buf = String::new();
/// // Printing to a `String` can never fail.
/// PRINTER.print_duration(&dur, &mut buf).unwrap();
/// assert_eq!(buf, "PT24h2m5.123000789s");
/// assert_eq!(buf, "PT24H2M5.123000789S");
///
/// // Negative durations are supported.
/// buf.clear();
/// PRINTER.print_duration(&-dur, &mut buf).unwrap();
/// assert_eq!(buf, "-PT24h2m5.123000789s");
/// assert_eq!(buf, "-PT24H2M5.123000789S");
///
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
Expand Down
Loading

0 comments on commit d100b15

Please sign in to comment.