Rust

std has no calendar — jiff’s Timestamp, Zoned, and civil types, with chrono field notes.

Rust’s standard library deliberately ships no calendar: std::time offers SystemTime (an absolute point in time for wall-clock stamps), Instant (a monotonic reading for measuring elapsed time — unrelated to calendar instants despite the name), and Duration (an exact span). There is no date type, no zone handling, no formatting. For real date-and-time work you pick a crate; this guide teaches jiff, which transplants the design of JavaScript’s Temporal — the same type-per-meaning model this guide recommends everywhere.

jiff’s inventory maps cleanly onto the guide’s vocabulary: Timestamp is the absolute instant (“aware UTC”); Zoned is an instant paired with an IANA zone so calendar arithmetic can respect DST; civil::DateTime, civil::Date and civil::Time are the naive values — wall-clock readings that don’t identify a moment until you attach a zone; and Span is a mixed calendar/exact duration. The time-zone database comes from the system, with a bundled copy available — no extra crate.

A civil::Date names a period — a whole calendar day — while a Timestamp names a point: different kinds of value, not two precisions of one idea. See Points vs periods.

The safe default for “now” is Timestamp::now(); store it in RFC 3339 UTC. Reach for Zoned when a reader or calendar arithmetic enters the picture, and for civil types when the value genuinely has no zone (a birthday, a recurring 09:00).

Civil values are not moments

jiff enforces the boundary in its types: converting civil::DateTime to anything absolute requires a zone (civil.in_tz("America/New_York")?), and DST ambiguity is a first-class concept — the default resolves gaps and folds like Temporal’s 'compatible' mode, and to_ambiguous_zoned(...).unambiguous() lets you reject instead of guess.

Pitfall: A civil::DateTime (or chrono’s NaiveDateTime) does not identify a moment in time. Comparing two that were meant for different zones, or assuming one is UTC, gives results off by an offset that shifts across DST transitions. Attach the zone explicitly before comparing or subtracting.

Pitfall: Doing calendar math with std::time alone — SystemTime + Duration::from_secs(86_400) — advances exact time, not civil time: across a DST transition “86 400 seconds later” is not “the same wall-clock time tomorrow”, and no Duration of seconds can express “one month”. Use Zoned arithmetic with a Span (zoned.checked_add(1.day())?) for calendar steps.

See also: Durations and Instant vs civil time.

Go deeper: conversions, Span arithmetic, and chrono in the wild

The core types and conversions:

use jiff::{Timestamp, Zoned};

let now: Timestamp = Timestamp::now();                 // an absolute instant
let paris: Zoned = now.in_tz("Europe/Paris")?;         // instant + zone, for reading
let civil = paris.datetime();                          // the wall-clock reading
let back: Zoned = civil.in_tz("Europe/Paris")?;        // wall clock -> instant, zone named
let today = paris.date();                              // a calendar day (a period)

Parsing is type-directed — each type parses its own textual form, so the value you ask for is the value you get:

let t: jiff::Timestamp = "2026-06-05T14:00:00Z".parse()?;  // absolute; offset required
let d: jiff::civil::Date = "2026-06-05".parse()?;          // a date, never an instant
let z: jiff::Zoned = "2026-06-05T15:00[America/New_York]".parse()?; // RFC 9557

Exact vs calendar arithmetic — the distinction behind pitfall #2:

use jiff::ToSpan;

let start = "2026-03-29T01:30[Europe/London]".parse::<jiff::Zoned>()?;
let exact = &start + 24.hours();          // exactly 24h later
let next_day = start.checked_add(1.day())?; // "same time tomorrow", DST-aware
// Across the spring-forward night these land on different instants.

chrono, in the wild. Most existing Rust code uses chrono, and its model maps onto the same vocabulary: DateTime<Utc> is the aware instant, NaiveDateTime/NaiveDate are the civil values (the name says it), and DateTime<FixedOffset> carries an offset, not a zone. Real IANA zones need the companion crate chrono-tz. chrono surfaces DST ambiguity too: tz.from_local_datetime(&naive) returns a LocalResult that can be None (gap) or Ambiguous (fold) — handle both rather than .unwrap(). What chrono lacks is jiff’s calendar Span arithmetic and bundled tzdb; what it has is over a decade of ecosystem integrations. Both are honest about the instant/civil line — the pitfalls above apply verbatim.

std interop: SystemTime converts to and from Timestamp (Timestamp::try_from(system_time)?), so jiff composes with std-speaking APIs at the boundary.


← Back to all topics