Crate jiff

Jiff is a datetime library for Rust that encourages you to jump into the pit of success. The focus of this library is providing high level datetime primitives that are difficult to misuse and have reasonable performance.

Jiff takes enormous inspiration from Temporal, which is a TC39 proposal to improve datetime handling in JavaScript.

Here is a quick example that shows how to parse a typical RFC 3339 instant, convert it to a zone aware datetime, add a span of time and losslessly print it:

use jiff::{Timestamp, ToSpan};

let time: Timestamp = "2024-07-11T01:14:00Z".parse()?;
let zoned = time.in_tz("America/New_York")?.checked_add(1.month().hours(2))?;
assert_eq!(zoned.to_string(), "2024-08-10T23:14:00-04:00[America/New_York]");
// Or, if you want an RFC3339 formatted string:
assert_eq!(zoned.timestamp().to_string(), "2024-08-11T03:14:00Z");

# Ok::<(), Box<dyn std::error::Error>>(())

Overview

The primary type in this crate is Zoned. A Zoned value is a datetime that corresponds to a precise instant in time in a particular geographic region. Users of this crate may find it helpful to think of a Zoned as a triple of the following components:

All three of these components are used to provide convenient high level operations on Zoned such as computing durations, adding durations and rounding.

A Span is this crate's primary duration type. It mixes calendar and clock units into a single type. Jiff also provides SignedDuration, which is like std::time::Duration, but signed. Users should default to a Span for representing durations when using Jiff.

The remainder of this documentation is organized as follows:

Also, the _documentation sub-module serves to provide longer form documentation:

Features

Here is a non-exhaustive list of the things that Jiff supports:

Here is also a list of things that Jiff doesn't currently support, along with a link to a relevant issue (if one exists).

At present, it is recommended to use the icu crate via jiff-icu for localization and non-Gregorian use cases.

Please file an issue if you can think of more (substantial) things to add to the above list.

Usage

Jiff is on crates.io and can be used by adding jiff to your dependencies in your project's Cargo.toml. Or more simply, just run cargo add jiff.

Here is a complete example that creates a new Rust project, adds a dependency on jiff, creates the source code for a simple datetime program and then runs it.

First, create the project in a new directory:

$ cargo new jiff-example
$ cd jiff-example

Second, add a dependency on jiff:

$ cargo add jiff

Third, edit src/main.rs. Delete what's there and replace it with this:

use jiff::{Unit, Zoned};

fn main() -> Result<(), jiff::Error> {
    let now = Zoned::now().round(Unit::Second)?;
    println!("{now}");
    Ok(())
}

Fourth, run it with cargo run:

$ cargo run
   Compiling jiff v0.2.0 (/home/andrew/rust/jiff)
   Compiling jiff-play v0.2.0 (/home/andrew/tmp/scratch/rust/jiff-play)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.37s
     Running `target/debug/jiff-play`
2024-07-10T19:54:20-04:00[America/New_York]

The first time you run the program will show more output like above. But subsequent runs shouldn't have to re-compile the dependencies.

Examples

Get the current time in your system's time zone

The Zoned::now returns your system's time and also attempts to automatically find your system's default time zone via [tz::TimeZone::system]:

use jiff::Zoned;

let now = Zoned::now();
println!("{now}");
// Output: 2024-07-10T17:09:28.168146054-04:00[America/New_York]

Print the current time rounded to the nearest second

This uses the Zoned::round API to round a zoned datetime to the nearest second. This is useful, for example, if you don't care about fractional seconds:

use jiff::{Unit, Zoned};

let now = Zoned::now().round(Unit::Second)?;
println!("{now}");
// Output: 2024-07-10T17:09:28-04:00[America/New_York]
# Ok::<(), Box<dyn std::error::Error>>(())

Print today's date at a specific time

Let's say you want to get the current date at 2pm. Here's one way of doing it that makes use of [Zoned::with]:

use jiff::Zoned;

let zdt = Zoned::now().with()
    .hour(14)
    .minute(0)
    .second(0)
    .subsec_nanosecond(0)
    .build()?;
println!("{zdt}");
// Output: 2024-07-12T14:00:00-04:00[America/New_York]
# Ok::<(), Box<dyn std::error::Error>>(())

Or, if the time is known to be valid, you can use the infallibe civil::time convenience constructor:

use jiff::{civil::time, Zoned};

let zdt = Zoned::now().with().time(time(14, 0, 0, 0)).build()?;
println!("{zdt}");
// Output: 2024-07-12T14:00:00-04:00[America/New_York]
# Ok::<(), Box<dyn std::error::Error>>(())

You can eliminate the possibility of a panic at runtime by using time in a const block:

use jiff::{civil::time, Zoned};

let zdt = Zoned::now().with().time(const { time(14, 0, 0, 0) }).build()?;
println!("{zdt}");
// Output: 2024-07-12T14:00:00-04:00[America/New_York]
# Ok::<(), Box<dyn std::error::Error>>(())

Print the current Unix timestamp

This prints a Unix timestamp as the number of seconds since the Unix epoch via [Timestamp::now]:

use jiff::Timestamp;

let now = Timestamp::now();
println!("{}", now.as_second());
// Output: 1720646365

Or print the current timestamp to nanosecond precision (which is the maximum supported by Jiff):

use jiff::Timestamp;

let now = Timestamp::now();
println!("{}", now.as_nanosecond());
// Output: 1720646414218901664

Print the datetime for a timestamp

This example shows how to convert a Unix timestamp, in milliseconds, to a zoned datetime in the system's current time zone. This utilizes the Timestamp::from_millisecond constructor, [tz::TimeZone::system] to get the default time zone and the Timestamp::to_zoned routine to convert a timestamp to a zoned datetime.

use jiff::{tz::TimeZone, Timestamp};

let ts = Timestamp::from_millisecond(1_720_646_365_567)?;
let zdt = ts.to_zoned(TimeZone::system());
println!("{zdt}");
// Output: 2024-07-10T17:19:25.567-04:00[America/New_York]
// Or if you just want the RFC 3339 time without bothering with time zones:
assert_eq!(ts.to_string(), "2024-07-10T21:19:25.567Z");

# Ok::<(), Box<dyn std::error::Error>>(())

Create a zoned datetime from civil time

This example demonstrates the convenience constructor, civil::date, for a civil::Date. And use the civil::Date::at method to create a civil::DateTime. Once we have a civil datetime, we can use civil::DateTime::in_tz to do a time zone lookup and convert it to a precise instant in time:

use jiff::civil::date;

let zdt = date(2023, 12, 31).at(18, 30, 0, 0).in_tz("America/New_York")?;
assert_eq!(zdt.to_string(), "2023-12-31T18:30:00-05:00[America/New_York]");

# Ok::<(), Box<dyn std::error::Error>>(())

Note that civil::date should only be used for inputs that are known to be correct since it panics for an invalid date. If your date isn't known to be valid, then use the fallible civil::Date::new constructor.

Change an instant from one time zone to another

This shows how to find the civil time, in New York, when World War 1 ended:

use jiff::civil::date;

let zdt1 = date(1918, 11, 11).at(11, 0, 0, 0).in_tz("Europe/Paris")?;
let zdt2 = zdt1.in_tz("America/New_York")?;
assert_eq!(
    zdt2.to_string(),
    "1918-11-11T06:00:00-05:00[America/New_York]",
);

# Ok::<(), Box<dyn std::error::Error>>(())

Find the duration between two zoned datetimes

This shows how to compute a span between two zoned datetimes. This utilizes a Zoned's implementation for Sub, permitting one to subtract two zoned datetimes via the - operator:

use jiff::civil::date;

let zdt1 = date(2020, 8, 26).at(6, 27, 0, 0).in_tz("America/New_York")?;
let zdt2 = date(2023, 12, 31).at(18, 30, 0, 0).in_tz("America/New_York")?;
let span = &zdt2 - &zdt1;
assert_eq!(format!("{span:#}"), "29341h 3m");

# Ok::<(), Box<dyn std::error::Error>>(())

The above returns no units bigger than hours because it makes the operation reversible in all cases. But if you don't need reversibility (i.e., adding the span returned to zdt1 gives you zdt2), then you can ask for bigger units via Zoned::until to make the span more comprehensible:

use jiff::{civil::date, Unit};

let zdt1 = date(2020, 8, 26).at(6, 27, 0, 0).in_tz("America/New_York")?;
let zdt2 = date(2023, 12, 31).at(18, 30, 0, 0).in_tz("America/New_York")?;
let span = zdt1.until((Unit::Year, &zdt2))?;
assert_eq!(format!("{span:#}"), "3y 4mo 5d 12h 3m");

# Ok::<(), Box<dyn std::error::Error>>(())

Add a duration to a zoned datetime

This example shows how one can add a Span to a Zoned via Zoned::checked_add to get a new Zoned value. We utilize the ToSpan trait for convenience construction of Span values.

use jiff::{civil::date, ToSpan};

let zdt1 = date(2020, 8, 26).at(6, 27, 0, 0).in_tz("America/New_York")?;
let span = 3.years().months(4).days(5).hours(12).minutes(3);
let zdt2 = zdt1.checked_add(span)?;
assert_eq!(zdt2.to_string(), "2023-12-31T18:30:00-05:00[America/New_York]");

# Ok::<(), Box<dyn std::error::Error>>(())

As with civil::date, the ToSpan trait should only be used with inputs that are known to be valid. If you aren't sure whether the inputs are valid, then use Span::new and its fallible mutators like Span::try_years.

Dealing with ambiguity

In some cases, civil datetimes either don't exist in a particular time zone or are repeated. By default, Jiff automatically uses the tz::Disambiguation::Compatible strategy for choosing an instant in all cases:

use jiff::civil::date;

// 2:30 on 2024-03-10 in New York didn't exist. It's a "gap."
// The compatible strategy selects the datetime after the gap.
let zdt = date(2024, 3, 10).at(2, 30, 0, 0).in_tz("America/New_York")?;
assert_eq!(zdt.to_string(), "2024-03-10T03:30:00-04:00[America/New_York]");

// 1:30 on 2024-11-03 in New York appeared twice. It's a "fold."
// The compatible strategy selects the datetime before the fold.
let zdt = date(2024, 11, 3).at(1, 30, 0, 0).in_tz("America/New_York")?;
assert_eq!(zdt.to_string(), "2024-11-03T01:30:00-04:00[America/New_York]");

# Ok::<(), Box<dyn std::error::Error>>(())

For more control over disambiguation, see tz::TimeZone::to_ambiguous_zoned. Or fmt::temporal::DateTimeParser::disambiguation if you're parsing zoned datetimes.

Parsing a span

Jiff supports parsing ISO 8601 duration strings:

use jiff::Span;

let span: Span = "P5y1w10dT5h59m".parse()?;
let expected = Span::new().years(5).weeks(1).days(10).hours(5).minutes(59);
assert_eq!(span, expected.fieldwise());

# Ok::<(), Box<dyn std::error::Error>>(())

The same format is used for serializing and deserializing Span values when the serde feature is enabled.

Jiff also supports a bespoke "friendly" format as well:

use jiff::Span;

let expected = Span::new().years(5).weeks(1).days(10).hours(5).minutes(59);
let span: Span = "5 years, 1 week, 10 days, 5 hours, 59 minutes".parse()?;
assert_eq!(span, expected.fieldwise());
let span: Span = "5yrs 1wk 10d 5hrs 59mins".parse()?;
assert_eq!(span, expected.fieldwise());
let span: Span = "5y 1w 10d 5h 59m".parse()?;
assert_eq!(span, expected.fieldwise());

# Ok::<(), Box<dyn std::error::Error>>(())

Parsing an RFC 2822 datetime string

While you probably shouldn't pick RFC 2822 as a format for new things, it is sometimes necessary to use it when something else requires it (like HTTP or email). Parsing and printing of RFC 2822 datetimes is done via the fmt::rfc2822 module:

use jiff::fmt::rfc2822;

let zdt1 = rfc2822::parse("Thu, 29 Feb 2024 05:34 -0500")?;
let zdt2 = zdt1.in_tz("Australia/Tasmania")?;
assert_eq!(rfc2822::to_string(&zdt2)?, "Thu, 29 Feb 2024 21:34:00 +1100");
let zdt3 = zdt1.in_tz("Asia/Kolkata")?;
assert_eq!(rfc2822::to_string(&zdt3)?, "Thu, 29 Feb 2024 16:04:00 +0530");

# Ok::<(), Box<dyn std::error::Error>>(())

Using strftime and strptime for formatting and parsing

Jiff has support for the C style strftime and strptime functions for formatting and parsing datetime types. All of Jiff's datetime types having a strptime constructor for parsing, and a strftime method for formatting. For example, this shows how to use Zoned::strptime to parsed a string in a "odd" custom format into a zoned datetime:

use jiff::Zoned;

let zdt = Zoned::strptime(
    "%A, %B %d, %Y at %I:%M%p %Q",
    "Monday, July 15, 2024 at 5:30pm US/Eastern",
)?;
assert_eq!(zdt.to_string(), "2024-07-15T17:30:00-04:00[US/Eastern]");

# Ok::<(), Box<dyn std::error::Error>>(())

And this shows how to use Zoned::strftime to format a zoned datetime. Note the use of %Z, which will print a time zone abbreviation (when one is available) instead of an offset (%Z can't be used for parsing):

use jiff::civil::date;

let zdt = date(2024, 7, 15).at(17, 30, 59, 0).in_tz("Australia/Tasmania")?;
// %-I instead of %I means no padding.
let string = zdt.strftime("%A, %B %d, %Y at %-I:%M%P %Z").to_string();
assert_eq!(string, "Monday, July 15, 2024 at 5:30pm AEST");

# Ok::<(), Box<dyn std::error::Error>>(())

However, time zone abbreviations aren't parsable because they are ambiguous. For example, CST can stand for Central Standard Time, Cuba Standard Time or China Standard Time. Instead, it is recommended to use %Q to format an IANA time zone identifier (which can be parsed, as shown above):

use jiff::civil::date;

let zdt = date(2024, 7, 15).at(17, 30, 59, 0).in_tz("Australia/Tasmania")?;
// %-I instead of %I means no padding.
let string = zdt.strftime("%A, %B %d, %Y at %-I:%M%P %Q").to_string();
assert_eq!(string, "Monday, July 15, 2024 at 5:30pm Australia/Tasmania");

# Ok::<(), Box<dyn std::error::Error>>(())

See the fmt::strtime module documentation for supported conversion specifiers and other APIs.

Serializing and deserializing integer timestamps with Serde

Sometimes you need to interact with external services that use integer timestamps instead of something more civilized like RFC 3339. Since Timestamp's Serde integration uses RFC 3339, you'll need to override the default. While you could hand-write this, Jiff provides convenience routines that do this for you. But you do need to wire it up via Serde's with attribute:

use jiff::Timestamp;

#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct Record {
    #[serde(with = "jiff::fmt::serde::timestamp::second::required")]
    timestamp: Timestamp,
}

let json = r#"{"timestamp":1517644800}"#;
let got: Record = serde_json::from_str(&json)?;
assert_eq!(got.timestamp, Timestamp::from_second(1517644800)?);
assert_eq!(serde_json::to_string(&got)?, json);

# Ok::<(), Box<dyn std::error::Error>>(())

If you need to support optional timestamps via Option<Timestamp>, then use jiff::fmt::serde::timestamp::second::optional instead.

For more, see the fmt::serde sub-module. (This requires enabling Jiff's serde crate feature.)

Crate features

Ecosystem features

Time zone features

Modules