Date & Time #
Date and time handling in Rust is split into two levels: the standard library provides std::time for simple needs like measuring durations and getting Unix timestamps, but it doesn’t know the concept of “date” or “timezone”. For more complete needs — parsing dates, local formatting, timezone conversion, computing “30 days from now” — you need the chrono crate. This article covers both: std::time for use without external dependencies, and chrono for all real datetime needs in production applications.
std::time — DateTime Without Dependencies
#
The standard library provides two main types for time:
flowchart LR
ST["std::time"]
ST --> I["Instant\nMonotonic time\nFor measuring duration\nCannot be compared\nto real-world time"]
ST --> S["SystemTime\nSystem time (wall clock)\nConvertible to Unix timestamp\nCan go forward or backward (NTP)"]Instant — Measuring Durations
#
Instant is monotonic time — it can only move forward, never backward (unlike the system clock which can be changed by NTP or an admin). Use it to measure how long something takes:
use std::time::{Instant, Duration};
use std::thread;
fn hitung_fibonacci(n: u64) -> u64 {
match n {
0 => 0,
1 => 1,
n => hitung_fibonacci(n - 1) + hitung_fibonacci(n - 2),
}
}
fn main() {
// Record the start time
let mulai = Instant::now();
let hasil = hitung_fibonacci(35);
// Compute the elapsed time
let durasi = mulai.elapsed();
println!("fib(35) = {}", hasil);
println!("Computation time: {:?}", durasi);
println!("In milliseconds: {} ms", durasi.as_millis());
println!("In microseconds: {} µs", durasi.as_micros());
// Simulate an operation with sleep
let t = Instant::now();
thread::sleep(Duration::from_millis(100));
println!("Slept 100ms, actual: {:?}", t.elapsed());
// Compare two Instants
let awal = Instant::now();
thread::sleep(Duration::from_millis(10));
let akhir = Instant::now();
println!("akhir > awal: {}", akhir > awal);
println!("Difference: {:?}", akhir.duration_since(awal));
}
SystemTime and Unix Timestamps
#
SystemTime represents system time — it can be converted to a Unix timestamp (seconds since 1 January 1970 UTC):
use std::time::{SystemTime, UNIX_EPOCH, Duration};
fn main() {
// Current time
let sekarang = SystemTime::now();
// Convert to a Unix timestamp
match sekarang.duration_since(UNIX_EPOCH) {
Ok(durasi) => {
println!("Unix timestamp: {} seconds", durasi.as_secs());
println!("Unix timestamp: {} ms", durasi.as_millis());
}
Err(e) => println!("Error: {}", e),
}
// Create a SystemTime from a Unix timestamp
let timestamp = UNIX_EPOCH + Duration::from_secs(1_700_000_000);
println!("From timestamp 1700000000: {:?}", timestamp);
// Difference between two SystemTimes
let t1 = SystemTime::now();
std::thread::sleep(Duration::from_millis(50));
let t2 = SystemTime::now();
match t2.duration_since(t1) {
Ok(d) => println!("Difference: {:?}", d),
Err(_) => println!("t2 is earlier than t1"), // can happen due to NTP adjustment
}
}
Duration — Representing a Time Interval
#
The standard library’s Duration represents a non-negative time interval:
use std::time::Duration;
fn main() {
// Creating a Duration
let satu_detik = Duration::from_secs(1);
let setengah_detik = Duration::from_millis(500);
let satu_menit = Duration::from_secs(60);
let satu_jam = Duration::from_secs(3600);
let presisi_tinggi = Duration::from_nanos(1_500_000); // 1.5 ms
// Arithmetic operations
let dua_detik = satu_detik + satu_detik;
let nol_koma_lima = satu_detik / 2;
println!("2 seconds: {:?}", dua_detik);
println!("0.5 seconds: {:?}", nol_koma_lima);
// Unit conversion
let durasi = Duration::from_secs(3723); // 1 hour 2 minutes 3 seconds
println!("Seconds: {}", durasi.as_secs());
println!("Milliseconds: {}", durasi.as_millis());
println!("Hours (rounded): {}", durasi.as_secs() / 3600);
println!("Remaining minutes: {}", (durasi.as_secs() % 3600) / 60);
// Comparing Durations
println!("1 minute > 30 seconds: {}", satu_menit > setengah_detik);
println!("Largest: {:?}", satu_jam.max(satu_menit));
}
The chrono Crate — Complete Dates and Times
#
For needs beyond mere timestamps and durations — parsing dates from strings, formatting to ISO 8601, computing “how many days until the deadline”, timezone conversion — use the chrono crate.
Add it to Cargo.toml:
[dependencies]
chrono = "0.4"
The Type Map in chrono
#
flowchart TD
C["chrono types"]
C --> N["Naive (without timezone)"]
C --> TZ["Timezone-aware"]
N --> ND["NaiveDate\nDate only\n2024-08-24"]
N --> NT["NaiveTime\nTime only\n14:30:00"]
N --> NDT["NaiveDateTime\nDate + time\nwithout timezone"]
TZ --> DU["DateTime<Utc>\nUTC time"]
TZ --> DL["DateTime<Local>\nSystem local time"]
TZ --> DF["DateTime<FixedOffset>\nFixed offset\n+07:00, -05:00"]| Type | Timezone | When to use |
|---|---|---|
NaiveDate | No | Calendar dates, birthdays, deadlines |
NaiveTime | No | Time without timezone context |
NaiveDateTime | No | Timestamps in databases, local logs |
DateTime<Utc> | UTC | Server timestamps, API responses |
DateTime<Local> | System local | Display to users |
DateTime<FixedOffset> | Fixed offset | Parsing strings with an offset |
Getting the Current Time #
use chrono::prelude::*;
fn main() {
// Current UTC time
let utc_now: DateTime<Utc> = Utc::now();
println!("UTC: {}", utc_now);
println!("UTC RFC3339: {}", utc_now.to_rfc3339());
// Current local time (system timezone)
let lokal_now: DateTime<Local> = Local::now();
println!("Local: {}", lokal_now);
// Today's date only
let hari_ini: NaiveDate = Local::now().date_naive();
println!("Today: {}", hari_ini);
// Current time only
let waktu_kini: NaiveTime = Local::now().time();
println!("Current time: {}", waktu_kini.format("%H:%M:%S"));
// Unix timestamp from chrono
let ts = utc_now.timestamp();
let ts_ms = utc_now.timestamp_millis();
println!("Unix timestamp: {}", ts);
println!("Unix timestamp ms: {}", ts_ms);
}
Creating a DateTime from Specific Values #
The original article used an old API that’s now deprecated. Here’s the correct way for the latest chrono 0.4:
use chrono::prelude::*;
fn main() {
// NaiveDate — date only
let tanggal = NaiveDate::from_ymd_opt(2024, 8, 24)
.expect("Invalid date");
println!("Date: {}", tanggal);
// NaiveTime — time only
let waktu = NaiveTime::from_hms_opt(14, 30, 45)
.expect("Invalid time");
println!("Time: {}", waktu);
// NaiveDateTime — date and time combined
let naive_dt = NaiveDateTime::new(tanggal, waktu);
println!("NaiveDateTime: {}", naive_dt);
// DateTime<Utc> from a NaiveDateTime
let utc_dt = naive_dt.and_utc();
println!("UTC: {}", utc_dt);
// DateTime<FixedOffset> — with WIB offset (+07:00)
let wib = FixedOffset::east_opt(7 * 3600).unwrap();
let wib_dt = wib.from_local_datetime(&naive_dt)
.single()
.expect("Ambiguous time");
println!("WIB: {}", wib_dt);
// From a Unix timestamp
let dari_ts = DateTime::from_timestamp(1_700_000_000, 0)
.expect("Invalid timestamp");
println!("From timestamp: {}", dari_ts);
// Date validation — from_ymd_opt returns None for invalid dates
let tidak_valid = NaiveDate::from_ymd_opt(2024, 2, 30);
println!("30 Feb 2024: {:?}", tidak_valid); // None
let valid = NaiveDate::from_ymd_opt(2024, 2, 29); // 2024 is a leap year
println!("29 Feb 2024: {:?}", valid); // Some(2024-02-29)
}
Date and Time Arithmetic #
use chrono::prelude::*;
use chrono::Duration;
fn main() {
let sekarang = Utc::now();
// Add and subtract Durations
let besok = sekarang + Duration::days(1);
let seminggu_lalu = sekarang - Duration::weeks(1);
let dua_jam_lagi = sekarang + Duration::hours(2);
let tiga_puluh_menit_lalu = sekarang - Duration::minutes(30);
println!("Now: {}", sekarang.format("%Y-%m-%d %H:%M:%S"));
println!("Tomorrow: {}", besok.format("%Y-%m-%d %H:%M:%S"));
println!("A week ago: {}", seminggu_lalu.format("%Y-%m-%d"));
println!("In 2 hours: {}", dua_jam_lagi.format("%H:%M:%S"));
// Operations on NaiveDate — adding months and years
let hari_ini = NaiveDate::from_ymd_opt(2024, 8, 24).unwrap();
// checked_add_months — safe for month ends
use chrono::Months;
let bulan_depan = hari_ini.checked_add_months(Months::new(1))
.expect("Overflow");
println!("Next month: {}", bulan_depan);
let tahun_depan = hari_ini.checked_add_months(Months::new(12))
.expect("Overflow");
println!("Next year: {}", tahun_depan);
// Difference between two DateTimes — chrono::Duration
let deadline = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
let sisa_hari = deadline.signed_duration_since(hari_ini).num_days();
println!("Days left until year end: {} days", sisa_hari);
// Difference between two DateTime<Utc>
let t1 = Utc::now();
std::thread::sleep(std::time::Duration::from_millis(50));
let t2 = Utc::now();
let selisih = t2.signed_duration_since(t1);
println!("Difference: {} ms", selisih.num_milliseconds());
// Comparing DateTimes
let d1 = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
let d2 = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
println!("d1 < d2: {}", d1 < d2);
println!("d2 > d1: {}", d2 > d1);
println!("Latest: {}", d1.max(d2));
}
Formatting and Parsing #
Formatting to a String #
chrono uses format strings similar to strftime in C. Here are the most frequently used symbols:
| Symbol | Meaning | Example output |
|---|---|---|
%Y | 4-digit year | 2024 |
%m | 2-digit month | 08 |
%d | 2-digit day | 24 |
%H | Hour (00–23) | 14 |
%M | Minute | 30 |
%S | Second | 45 |
%3f | Millisecond | 123 |
%A | Full weekday name | Saturday |
%B | Full month name | August |
%a | Short weekday name | Sat |
%b | Short month name | Aug |
%Z | Timezone name | UTC |
%z | Timezone offset | +0700 |
use chrono::prelude::*;
fn main() {
let dt = Utc::now();
// Common formats
println!("{}", dt.format("%Y-%m-%d")); // 2024-08-24
println!("{}", dt.format("%d/%m/%Y")); // 24/08/2024
println!("{}", dt.format("%Y-%m-%d %H:%M:%S")); // 2024-08-24 14:30:45
println!("{}", dt.format("%H:%M")); // 14:30
println!("{}", dt.format("%A, %d %B %Y")); // Saturday, 24 August 2024
// Standard international formats
println!("{}", dt.to_rfc3339()); // 2024-08-24T14:30:45.123456789Z
println!("{}", dt.to_rfc2822()); // Sat, 24 Aug 2024 14:30:45 +0000
// To a string
let sebagai_string = dt.format("%Y-%m-%d").to_string();
println!("String type: {}", sebagai_string);
}
Parsing from a String #
use chrono::prelude::*;
fn main() {
// Parse a NaiveDateTime (without timezone)
let s = "2024-08-24 14:30:45";
let dt = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S")
.expect("Format mismatch");
println!("Parsed: {}", dt);
// Parse a NaiveDate only
let tgl_str = "24/08/2024";
let tgl = NaiveDate::parse_from_str(tgl_str, "%d/%m/%Y")
.expect("Date format mismatch");
println!("Date: {}", tgl);
// Parse a DateTime with timezone (RFC3339 / ISO 8601)
let rfc_str = "2024-08-24T14:30:45+07:00";
let dt_tz = DateTime::parse_from_rfc3339(rfc_str)
.expect("Not an RFC3339 format");
println!("RFC3339: {}", dt_tz);
// Parse with proper error handling
let input = "not-a-date";
match NaiveDate::parse_from_str(input, "%Y-%m-%d") {
Ok(d) => println!("Success: {}", d),
Err(e) => println!("Failed to parse '{}': {}", input, e),
}
// Parse and convert to UTC
let lokal_str = "2024-08-24T14:30:45+07:00";
let dt_wib = DateTime::parse_from_rfc3339(lokal_str).unwrap();
let dt_utc: DateTime<Utc> = dt_wib.into();
println!("WIB: {}", dt_wib);
println!("UTC: {}", dt_utc);
}
Timezone Conversion #
use chrono::prelude::*;
fn main() {
let utc_now: DateTime<Utc> = Utc::now();
// Convert to fixed offsets
let wib = FixedOffset::east_opt(7 * 3600).unwrap(); // UTC+7
let wita = FixedOffset::east_opt(8 * 3600).unwrap(); // UTC+8
let wit = FixedOffset::east_opt(9 * 3600).unwrap(); // UTC+9
let est = FixedOffset::west_opt(5 * 3600).unwrap(); // UTC-5
let wib_now = utc_now.with_timezone(&wib);
let wita_now = utc_now.with_timezone(&wita);
let wit_now = utc_now.with_timezone(&wit);
let est_now = utc_now.with_timezone(&est);
println!("UTC : {}", utc_now.format("%Y-%m-%d %H:%M:%S %Z"));
println!("WIB : {}", wib_now.format("%Y-%m-%d %H:%M:%S %z"));
println!("WITA : {}", wita_now.format("%Y-%m-%d %H:%M:%S %z"));
println!("WIT : {}", wit_now.format("%Y-%m-%d %H:%M:%S %z"));
println!("EST : {}", est_now.format("%Y-%m-%d %H:%M:%S %z"));
// For a full timezone database (Asia/Jakarta, America/New_York, etc.)
// use the chrono-tz crate:
// [dependencies]
// chrono = "0.4"
// chrono-tz = "0.9"
//
// use chrono_tz::Asia::Jakarta;
// let jakarta_now = utc_now.with_timezone(&Jakarta);
// Store time as UTC, display in local
let simpan_di_db: DateTime<Utc> = Utc::now();
let tampil_ke_user = simpan_di_db.with_timezone(&wib);
println!("\nStored (UTC): {}", simpan_di_db.to_rfc3339());
println!("Displayed (WIB): {}", tampil_ke_user.format("%d/%m/%Y %H:%M"));
}
For timezone conversion with full IANA names likeAsia/Jakarta,America/New_York, orEurope/London, add thechrono-tzcrate toCargo.toml. This crate bundles a complete timezone database at compile time without system dependencies.
Real-World Use Cases #
Computing Age from a Birth Date #
use chrono::prelude::*;
fn hitung_usia(tanggal_lahir: NaiveDate) -> u32 {
let hari_ini = Local::now().date_naive();
let tahun_penuh = hari_ini.year() - tanggal_lahir.year();
// Check whether this year's birthday has passed
let sudah_ulang_tahun = (hari_ini.month(), hari_ini.day())
>= (tanggal_lahir.month(), tanggal_lahir.day());
if sudah_ulang_tahun {
tahun_penuh as u32
} else {
(tahun_penuh - 1) as u32
}
}
fn main() {
let lahir = NaiveDate::from_ymd_opt(1995, 8, 24).unwrap();
println!("Age: {} years", hitung_usia(lahir));
}
Timestamps for Logs and Audit Trails #
use chrono::prelude::*;
struct CatatanAudit {
aksi: String,
pengguna: String,
waktu: DateTime<Utc>,
}
impl CatatanAudit {
fn baru(aksi: &str, pengguna: &str) -> Self {
CatatanAudit {
aksi: aksi.to_string(),
pengguna: pengguna.to_string(),
waktu: Utc::now(),
}
}
fn tampilkan(&self) {
println!(
"[{}] {} by {}",
self.waktu.format("%Y-%m-%dT%H:%M:%S%.3fZ"),
self.aksi,
self.pengguna
);
}
}
fn main() {
let log = vec![
CatatanAudit::baru("LOGIN", "[email protected]"),
CatatanAudit::baru("BUAT_DOKUMEN", "[email protected]"),
CatatanAudit::baru("HAPUS_DATA", "[email protected]"),
];
for catatan in &log {
catatan.tampilkan();
}
}
Summary #
std::time::Instantfor measuring durations — monotonic, never goes backward, ideal for benchmarking and timeouts. Not for dates or formatting.std::time::SystemTimefor Unix timestamps — convertible to seconds since the epoch viaduration_since(UNIX_EPOCH). Needs error handling because it can fail due to NTP.chronofor all complete datetime needs — dates, times, timezones, formatting, and parsing.- Naive vs timezone-aware —
NaiveDate/NaiveDateTimefor data without timezone context (calendars, local databases);DateTime<Utc>for server timestamps;DateTime<Local>for display to users.- Always store as UTC, display in local — the standard convention to avoid timezone bugs in multi-region applications.
- Use the
_optAPIs for validation —NaiveDate::from_ymd_opt(2024, 2, 30)returnsNone(invalid date), not a panic like the old deprecated APIs.to_rfc3339()for serialization — ISO 8601 format with timezone is the standard for APIs and databases.chrono-tzfor a full timezone database — a separate crate bundling all IANA timezone names (Asia/Jakarta, etc.) at compile time.chrono’sDurationdiffers fromstd::time::Duration— chrono’sDurationcan be negative (for differences),std::time::Durationis always non-negative.