In this topic, I will try to review the current landscape of Rust libraries for accounting dates and times. In particular, I will touch on the thorny issues concerning support for time standards and UTC leap seconds.
The problem space
There are several different quantities used by software to represent dates and times, each with their own properties and complexities.
A timestamp measures the linear count of seconds since an agreed-upon moment, usually the Unix epoch (1970-01-01T00:00:00Z). For efficiency, arithmetics involving durations in uniform time units are often performed over timestamps rather than broken-down calendar time values.
A date and time in its simplest form refers to a date and a time of day. A date-time value has real-world meaning when it is associated with a calendar and a relationship to a standard time scale usually formulated as a fixed offset. This article will only consider the proleptic Gregorian calendar as it is the calendar that most software applications solely need to deal with, as well as the only calendar designated for worldwide communication by ISO 8601 and various Internet standards. Similarly, most applications deal with times offset from UTC, but there are complications with this time standard which are detailed below.
A civil date and time is date and time designated by authorities governing time zones. A time zone is normally defined by the principal offset from UTC and possibly a daylight saving time (DST) offset applied on a certain schedule within each year. The history of changes in the time zone definition needs to be considered for local calendar times that refer to the past. Crucially, this is the legal definition of what a local date and time e.g. "February 24, 12:34 in Helsinki" refers to.
Time standards and leap seconds
There are several internationally adopted standards for measurement of time, of which most practical interest is in UTC and TAI.
International Atomic Time (TAI) is a standard realized by tracking atomic clocks at metrological institutions around the world. It is a continuous time scale not connected to Earth's rotation (though realized in Earth's reference frame, if you're into relativistic effects).
Universal Coordinated Time (UTC) is derived from TAI to more accurately correspond to solar days. UTC is kept at an offset by a whole number of seconds from TAI. Due to irregularities in Earth's rotation, leap seconds are occasionally introduced by the IERS to increment the offset by one second (or decrement it, but negative leap seconds have not been needed yet) by extending the minute 23:59 of a chosen calendar day to last 61 seconds (or shortening it to 59 seconds, in case of a negative leap second).
The existence of leap seconds means that the mapping between UTC and timestamps on a continuous, physically representative time scale cannot be computed with simple logico-arithmetical formulas and must involve a table of the published leap seconds. Furthermore, this mapping changes as information about newly introduced leap seconds is disseminated. As a consequence of this, when Alice and Bob must agree on which timestamp a future UTC time refers to or vice versa, their tables of leap seconds must be synchronized.
To round up this introduction, it's worth mentioning that it has been decided at the General Conference on Weights and Measures to sunset leap seconds (pardon my astronomical joke here) by 2035.
State of the art in software outside Rust
Unsurprisingly, the kind of intricacies needed to properly address the UTC discontinuity events that have only occurred 27 times since 1972 meets pushback from the software industry. Most widely used programming environments do not support leap seconds, performing calculations over calendar time as if the leap seconds did not and will not exist at all (by implication, the solution to a negative leap second is to insert a notional missing second). This permits simple date and time computations on the proleptic Gregorian calendar, with units up to the week being uniform multiples of the second.
Java's JSR-310 API is the closest one I've come across to provide some acknowlegement to leap seconds and admit their data model cannot fully represent UTC. It stops short of mandating much specific behavior, though:
Implementations of the Java time-scale using the JSR-310 API are not required to provide any clock that is sub-second accurate, or that progresses monotonically or smoothly. Implementations are therefore not required to actually perform the UTC-SLS slew or to otherwise be aware of leap seconds. JSR-310 does, however, require that implementations must document the approach they use when defining a clock representing the current instant.
ECMAScript's in-progress TC39 proposal on Temporal
has this note:
Although Temporal does not deal with leap seconds, dates coming from other software may have a second value of 60. In the default
'constrain'
mode and when parsing an ISO 8601 string, this will be converted to 59. In'reject'
mode, this function will throw, so if you have to interoperate with times that may contain leap seconds, don't usereject
.
Among protocols used to exchange time data, text formats such as RFC 3339 tolerate leap seconds, or, in fact, any time specification referring to 23:59:60 in UTC, with only vague language about possible validation. On the other hand, specifications of some binary formats, notably the Timestamp
well-known message type for Protobuf, expressly forbid accounting of leap seconds in their interpretation.
Unix real-time clock: is worse really better?
POSIX timestamps represent the number of seconds since the Unix epoch. Unix standard library functions converting between timestamps and broken-down calendar time ignore leap seconds.
For a variety of mostly historical reasons, leap seconds have been solved on Unix systems by setting the wall-clock time back by a second. The wall clock is generally allowed by POSIX to make discontinuous jumps in either direction, so this is not a new concern; Unix programs using system wall-clock timestamps to measure elapsed time should be prepared to gracefully handle the non-monotonic clock. However, if an application expects the clock to be synchronized via NTP, leap seconds become "dangerous time" when the system clock abruptly deviates from UTC by up to a second, yet there is no way to get information about this through standard APIs. Linux provides the OS-specific adjtimex
system call, which works if the time synchronization daemon supports the feature and is able to get the information on the upcoming leap second in time.
NTP and leap second smear
NTP, the time synchronization protocol predominantly used on the internet, supports information on leap seconds. However, due to the issues with the system clock APIs described above, companies maintaining large computing infrastructures - Google, Amazon, Meta among them - have implemented various leap second smear schemes, where a leap second is absorbed by gradual clock adjustments over some time interval around the discontinuity in actual UTC, leveling back with it at the end of the smear interval. For systems using smeared NTP sources (which all must agree on a particular scheme), the clock will run slow by the order of 10-100 ppm during that interval, which is within accuracy tolerance of hardware clocks used in most computing devices and NTP's acceptable slew rate.
Date and time APIs in Rust
std::time
The standard library provides only minimal and opaque facilities to quantify time and access system-provided clocks. This is quite on purpose, as the details of implementation vary with the target operating system, while the subject matter of various time scales and calendar manipulation appears too complex and opinionated to have a single standard API that would suit all users. std::time::Instant
provides a monotonic (but not necessarily steady) clock, while std::time::SystemTime
exposes system's real-time clock and allows to obtain the time difference elapsed since another system clock time, including the value representing the Unix epoch. No details are provided on the accuracy of the clock, or on dealing with leap seconds besides specifying that SystemTime
does not count them.
Third-party crates
As is often the case, crates developed by the community step in to provide functionality missing in the standard library, albeit with their own shortcomings and peculiarities. For this review, I have looked at the following popular crates, listed here with TL;DR summaries of their functionality:
time
- simple facilities for date-time manipulation with numeric timezone offsets from UTC. No support for leap seconds, besides some allowances in input string formats.chrono
- more complex API on dates and times with extensible abstraction to support time zones, and quirky, over-permissive support for leap second inputs.hifitime
- precise measurement of time based on TAI, designed for scientific and engineering applications. UTC leap seconds are supported as fully as possible (with issues discussed below).
Solutions to specific problems
In this section, I will get into details of how the Rust crates implement specific tasks that occur with processing date and time information.
Precision
All of the libraries reviewed here use time and duration values with fixed precision down to nanoseconds.
Range of dates
time
can represent (with any valid timezone offset) dates in the ±9999 year range. Enabling the large-dates
feature extends the year range to ±999999 (changing behavior with compile-time features is a footgun, but let's not digress here).
chrono
supports the date range of about ±262000 years from the Common Epoch.
hifitime
can represent years in the range of about ±3276800 around the reference epoch of 1900-01-01 12:00 TAI. So if you code the software of a space probe in Rust using this library, it will still be ticking correctly when it reaches another star system.
Arithmetics with numeric durations
The simplest and commonly occurring kind of computations are finding the duration in seconds elapsed between two time references, and adding or subtracting a duration from a time reference to get another one in the same domain.
Duration arithmetics in time
operate on UTC timestamps ignoring leap seconds (as of version 0.3.34). This is not suitable for applications that require precise calculations on approximated physical time, but is sufficient for many other purposes. The calculations should be interoperable with the standard library's SystemTime
on most platforms. Instant
is also provided for dealing with monotonic clock readings and wraps std::time::Instant
.
Same kind of arithmetics are available in chrono
, but with a twist: the broken-down representation of time allows leap seconds. In fact, it allows constructing any time value with an interstitial second after 59, because it might be a leap second and the core library has no way to verify it. The motto of its unorthodox approach to leap second handling is "it allows for leap seconds but behaves as if there are no other leap seconds". What we get out of this is kinky math that is non-associative and, so long as there are binary operations that may operate on two notional leap seconds, just plain wrong:
use chrono::{DateTime, TimeDelta};
let t1 = DateTime::parse_from_rfc3339("2024-02-24T12:34:59.5Z").unwrap();
let t2 = DateTime::parse_from_rfc3339("2024-02-24T12:34:60.5Z").unwrap();
assert_ne!(t1, t2);
let delta = TimeDelta::milliseconds(500);
assert_eq!(t1 + delta, t2 + delta);
let t3 = DateTime::parse_from_rfc3339("2024-02-24T12:35:60.5Z").unwrap();
assert_eq!(t3 - t2, TimeDelta::seconds(61));
With this kind of laxity, my advice is to not use chrono
in applications that can't operate on the "garbage in, garbage out" principle, or at least apply external validation so that leap second values cannot occur. Another concern is performance: there is CPU overhead in checking for special cases that only legitimately occur in 27 seconds over the last 50+ years.
hifitime
operates on TAI timestamps, so this seems to be a good fit for scientific or engineering applications that need the continuous time scale. Be aware, though, that the machine's system clock may not be good enough as an input.
Mapping to UTC
To discuss the mathematical properties of how fixed-precision values represent the abstract UTC time scale in this section, let's consider the set of discrete time moments in UTC rounded to whole nanoseconds and bound by the supported date range of the library in question.
In time
, the mapping of OffsetDateTime
to UTC is not surjective: times within leap seconds do not have a representation.
In chrono
any valid UTC time can be represented, but, as shown above, any values of DateTime
within the second number 60 are permitted (internally represented as a supernumerary billion of nanoseconds added to second 59), not only valid leap seconds. The data domain is non-linear and does not have a well-defined mapping to UTC for all values.
hifitime
gets the bijective mapping to UTC right, with the proviso of how up to date the internally used list of leap seconds is. There is an extension trait to provide this list from an external source (thus shifting the responsibility, not solving the problem), but it is not used for most convenient conversions and string formatting. So to keep up with the latest leap seconds published by the IERS, applications are supposed to regularly update to the latest version of the crate.
Importing leap second times
Date and time references may be exchanged in a format permitting the second value of 60, such as RFC 3339 or RFC 2822.
time
accepts such formatted strings when parsing from these formats, with the restriction that the time in UTC must refer to the last minute of the last day of a calendar month (which follows the practice that has always been used by the IERS). The time is then converted to 23:59:59.999999999 in UTC, that is, the last nanosecond preceding the notional leap second.
chrono
accepts and internally represents times with second 60, regardless of any other components of the date-time value, as shown in the example above. Again, beware of using this library to work with untrusted inputs.
hifitime
correctly validates UTC leap second times for the hardcoded list of latest known leap seconds, and invalidates times with the second number of 60 that are not found on the list. See the previous section for how this can still be incorrect.
Time zone offsets
A simpler level of support for the world's time zones is to operate with numeric offsets from UTC. If the date-time value carries information on the offset, and the leap seconds are ignored, arithmetics can be performed in a simple way on the proleptic Gregorian calendar without changing the offset.
The OffsetDateTime
type in time
features the numeric offset from UTC in the range ±25:59:59. A fallible conversion to a different offset is provided.
Time zone support in chrono
's DateTime
type is realized generically, with in-crate implementations for UTC itself (at zero cost upon monomorphization) and fixed offsets from UTC. There are conversions to these well-known time zone parameterizations as well.
The developers of hifitime
have little concern for such earthly matters as local time, so there is only minimal support for UTC offsets in input or output.
Obtaining local time zone offset
The operating system provides information on the local time offset currently in effect, typically as a function retrieving the local time. On Unix, this is usually done with the localtime_r
standard C library function, but it's generally unsound in Rust due to unguarded use of the process' environment variables.
time
provides methods to obtain the system's UTC offset if the local-offset
feature is enabled. There is also an unsafe function, heavily discouraged against being used, to allow unsoundness in obtaining the local clock, which enables unconditional use of localtime_r
on Unix. Unfortunately, without the unsoundness the library gracefully fails to retrieve the local offset on most Unix platforms, including Linux. This is only avoided if the implementation detects that the process only runs the single thread, which on Linux is done by reading /proc/self/stat
. In earlier versions of the crate, the unsoundness was not opt-in, which earned it a security advisory.
chrono
used to take the same unsound approach. After getting slapped with a security advisory of its own, this was changed to a pure Rust implementation reading the TZ
environment variable and working with data from the system timezone database.
hifitime
has no support for local time, period.
Civil time zones
Time zones are the trickiest subject in date and time manipulation. First, the definitions of time zones are subject to change; IANA maintains a not rarely updated database on the world's time zones, which is integrated by software platform vendors. Second, civil date and time computations have more surprising failure cases: a periodic job that is scheduled to run every night at 03:01 Helsinki time will not run on March 31, 2024 due to the DST transition.
time
does not currently support time zone information. Jacob Pratt, the principal developer, has stated his intent to add support for tzdata at some future date. A complementary time-tz
crate provides time zone conversions and compiled-in time zone data, deriving some of API and the problem of keeping up to date from chrono
and chrono-tz
.
chrono
has both generic API for time zones and fallible methods for civil date manipulation, such as adding a number of days to a DateTime
. chrono-tz
is a companion crate to chrono
that provides civil time zone data from the IANA database as compiled-in constants. Updates in the time zones are meant to be applied by updating the crate dependency to the newest release and rebuilding the application, which can be considered an unsatisfactory method of keeping up with legislative activities of more than a hundred governing bodies around the world. How do you even semver that? The tzfile
crate provides integration of timezone database data from /usr/share/zoneinfo
, but this functionality is only available on some Unix platforms, most importantly Linux and MacOS.
The developers of hifitime
, quite possibly, scoff at the messy humanity at large, seeing how we are unable to divide our geoid into regularly shaped time zones and how we resort to playing tricks with daylight time in a pathetic attempt to improve our productivity and energy conservation.
Conclusion
From this review, it should be evident that none of the crates cover all common use cases without significant unresolved issues. time
is sufficient for numeric date-time data based on UTC (sans the leap seconds) and validation of formatted inputs, but its support for reading local time without the unsoundness hack leaves a lot to be desired. chrono
deserves praise for its implementations of local time and time zone manipulation, but its data domain is too loose for applications conscious about correctness and security. hifitime
is the best fit for scientific and engineering applications which need a precise and continuous time scale, but is not designed for dealing with local time.