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"]
TypeTimezoneWhen to use
NaiveDateNoCalendar dates, birthdays, deadlines
NaiveTimeNoTime without timezone context
NaiveDateTimeNoTimestamps in databases, local logs
DateTime<Utc>UTCServer timestamps, API responses
DateTime<Local>System localDisplay to users
DateTime<FixedOffset>Fixed offsetParsing 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:

SymbolMeaningExample output
%Y4-digit year2024
%m2-digit month08
%d2-digit day24
%HHour (00–23)14
%MMinute30
%SSecond45
%3fMillisecond123
%AFull weekday nameSaturday
%BFull month nameAugust
%aShort weekday nameSat
%bShort month nameAug
%ZTimezone nameUTC
%zTimezone 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 like Asia/Jakarta, America/New_York, or Europe/London, add the chrono-tz crate to Cargo.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::Instant for measuring durations — monotonic, never goes backward, ideal for benchmarking and timeouts. Not for dates or formatting.
  • std::time::SystemTime for Unix timestamps — convertible to seconds since the epoch via duration_since(UNIX_EPOCH). Needs error handling because it can fail due to NTP.
  • chrono for all complete datetime needs — dates, times, timezones, formatting, and parsing.
  • Naive vs timezone-awareNaiveDate/NaiveDateTime for 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 _opt APIs for validationNaiveDate::from_ymd_opt(2024, 2, 30) returns None (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-tz for a full timezone database — a separate crate bundling all IANA timezone names (Asia/Jakarta, etc.) at compile time.
  • chrono’s Duration differs from std::time::Duration — chrono’s Duration can be negative (for differences), std::time::Duration is always non-negative.

← Previous: Map   Next: Regex →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact