[Pre-RFC] More convenient Duration formatting


#1

Summary

This RFC proposes a more convenient method of formatting std::time::Duration methods to strings.

Motivation

The std::time API is currently pretty bare-bones, compared to time libraries in other languages. This is because date/time processing is a hairy area, and it is preferred that a std-worthy API be iterated outside of std. Examples of available 3rd-party libraries are time and chrono.

Still, for some applications, e.g. measuring time elapsed while a piece of code runs, std::time is perfectly usable, except that printing the results in a non-Debug manner is tedious. This RFC tries to help in this area, namely adding a method of formatting Duration values with some degree of customization.

Detailed design

This RFC does not propose to implement Display on Duration. This might be used later for a standards conforming exact representation (see “Alternatives” below). Instead, a new method called display() is added, similar to that on Path, returning a Display struct with a std::fmt::Display impl.

This provides the opportunity of adding arguments to display(), which is useful since a Duration can be expressed in different units, and the string formatting language is not capable of expressing this on its own. The proposed signature is:

fn display<U: Into<Option<Unit>>>(&self, unit: U) -> Display

with

enum Unit { Secs, Millis, Micros, Nanos }

The std::fmt::Display impl for Display can then format the duration according to the given unit, using the number format given in the format string specification. None selects the unit so that the number is above 1. The unit is added to the result unless “alternate” format is requested with #.

Examples:

let d = Duration::new(0, 500_000_000);
format!("{}", d.display(None))           // -> "500 ms"
format!("{}", d.display(Unit::Secs))     // -> "0.5 s"
format!("{:.3}", d.display(Unit::Secs))  // -> "0.500 s"
format!("{}", d.display(Unit::Micros))   // -> "500000 us"
format!("{:#}", d.display(Unit::Micros)) // -> "500000"

How We Teach This

This is a new API with entry in the library reference.

In the book and Rust by Example, there is currently no example using Duration other than as an argument to thread::sleep.

Drawbacks

  • When stabilized, will be around forever, might not be useful enough for all cases.

Alternatives

The time crate implements Display on its Duration, which uses ISO 8601 format (PxDTy.yyyS). Standard conforming, but not very readable. The chrono crate just reuses time::Duration.

Unresolved questions

  • More units? Years, months, days, hours, minutes?
  • Can Unit be otherwise useful later?
  • Instead of Option<Unit>, add Unit::Auto?
  • Swap meaning of alternate/normal format?
  • Support exponential notation?

#2

I like what you propose for sub-second durations, but for longer durations, a human-readable notation in days, hours, minutes, and seconds would be nice. Python’s datetime.timedelta does this:

>>> print(timedelta(minutes=20, seconds=42.12))
0:20:42.120000
>>> print(timedelta(hours=2, minutes=0, seconds=42))
2:00:42
>>> print(timedelta(days=6, hours=2, minutes=20, seconds=42.12))
6 days, 2:00:42.120000
>>> print(timedelta(days=367, minutes=20))
367 days, 0:20:00

That’s using the __str__ conversion on a timedelta object, which would correspond to Display in Rust. Note that unlike ISO 8601 duration notation, the hours, minutes, and seconds fields are never omitted even if they are zero, and it doesn’t try to do years, months, or weeks. I’m not a huge fan of the automatic switching between %d and %.6f for the seconds field.


#3

An alternative would be to introduce Duration and Time format traits but that would take quite a bit of design work.


#4

I certainly would prefer Unit::Auto over the Option<Unit> interface.

I agree with @zackw that support for longer times would be important, particularly in the likely scenario that the Unit enum cannot be extended to support additional units. When using longer durations (and in particular non-decimal units), I think the user should be able to print with either combined units or decimal notation. e.g. 1.25 hours in some contexts is easier to read than 1:15, particularly when taken to extremes (1.23421 years is easier to read and compare with other durations (also in years) than the same value expressed in years, weeks, days, hours, and seconds.

It might be good to add a complementary method

fn in_units(u: Unit) -> f64

which would just do the unit conversion.

Another approach to this task would be to forget all this, and simply define constants (of type Duration) for the different units, and then implemnt the Div trait between two durations.

impl std::ops::Div for Duration {
    type Output = f64;
    fn div(self, rhs: Self) -> f64 ...
}

Then we could achieve all the same formatting except the auto-formatting by

format!("{} s", d/Duration::second); // -> "0.5 s"

etc. Autoformatting could be omitted, or it could be decided that autoformatting is the Display method for durations.


#5

As an input argument, it probably shouldn’t be an enum for exactly this reason. Instead, probably constants:

pub struct Unit(Inner);

impl Unit {
    pub const SECONDS: Unit = Unit(Inner::Seconds);
    pub const MILLIS: Unit = Unit(Inner::Millis);
    // etc
}

enum Inner {
    Seconds,
    Millis,
    Nanos,
    // etc
}

Usage as:

format!("{}", d.display(Unit::MILLIS));

With the inner enum private, new variants can be easily added, and new consts aren’t a breaking change at all.


#6

I would add that I see no reason for Unit to be a distinct type. What is wrong with these constants being of type Duration? That describes their type perfectly well.

To answer my own question, I guess the advantage of a separate type is if you are excited about having the display method include the units. This seems to me very little value to be gained at the cost of introducing an entire new type into the crate, with its whole slew of constants.


#7

Maybe with RFC https://github.com/rust-lang/rfcs/pull/2008 that concern could be ammeliorated…?