How do I handle dates and times in Rust?

Walkthrough

Chrono is the de-facto standard library for date and time handling in Rust. It provides types for timezones, durations, parsing, formatting, and arithmetic. Chrono distinguishes between naive types (no timezone) and aware types (with timezone), preventing common datetime bugs at compile time.

Core types:

  1. NaiveDate — date without timezone
  2. NaiveTime — time without timezone
  3. NaiveDateTime — combined date and time without timezone
  4. DateTime<Tz> — date and time with timezone (e.g., DateTime<Utc>, DateTime<Local>)
  5. Duration — span of time for arithmetic

Chrono integrates well with serde for serialization and provides extensive parsing/formatting options.

Code Example

# Cargo.toml
[dependencies]
chrono = "0.4"
use chrono::{DateTime, Duration, Local, NaiveDate, NaiveDateTime, NaiveTime, Utc};
 
fn main() {
    // ===== Current Date and Time =====
    
    let now = Local::now();
    println!("Local now: {}", now);
    
    let utc_now = Utc::now();
    println!("UTC now: {}", utc_now);
    
    // ===== Creating Specific Dates/Times =====
    
    let date = NaiveDate::from_ymd_opt(2024, 3, 15).unwrap();
    println!("Date: {}", date);
    
    let time = NaiveTime::from_hms_opt(14, 30, 0).unwrap();
    println!("Time: {}", time);
    
    let datetime = NaiveDateTime::new(date, time);
    println!("DateTime: {}", datetime);
    
    // With timezone
    let utc_datetime = datetime.and_utc();
    println!("UTC DateTime: {}", utc_datetime);
    
    // ===== Parsing from Strings =====
    
    let parsed_date = NaiveDate::parse_from_str("2024-03-15", "%Y-%m-%d").unwrap();
    println!("Parsed date: {}", parsed_date);
    
    let parsed_datetime = NaiveDateTime::parse_from_str(
        "2024-03-15 14:30:00",
        "%Y-%m-%d %H:%M:%S"
    ).unwrap();
    println!("Parsed datetime: {}", parsed_datetime);
    
    // Parse directly to DateTime<Utc>
    let parsed_utc: DateTime<Utc> = "2024-03-15T14:30:00Z".parse().unwrap();
    println!("Parsed UTC: {}", parsed_utc);
}

Formatting and Display

use chrono::{DateTime, Local, NaiveDate, TimeZone, Utc};
 
fn main() {
    let dt = Utc::now();
    
    // RFC 3339 format (ISO 8601)
    println!("RFC 3339: {}", dt.to_rfc3339());
    
    // Custom format
    println!("Custom: {}", dt.format("%Y-%m-%d %H:%M:%S"));
    println!("Readable: {}", dt.format("%A, %B %d, %Y at %I:%M %p"));
    
    // Common format specifiers
    let now = Local::now();
    println!("Year: {}", now.format("%Y"));       // 2024
    println!("Month: {}", now.format("%m"));      // 03
    println!("Day: {}", now.format("%d"));        // 15
    println!("Hour (24h): {}", now.format("%H")); // 14
    println!("Hour (12h): {}", now.format("%I")); // 02
    println!("Minute: {}", now.format("%M"));    // 30
    println!("Second: {}", now.format("%S"));    // 45
    println!("Weekday: {}", now.format("%A"));   // Friday
    println!("Month name: {}", now.format("%B")); // March
}

Duration and Arithmetic

use chrono::{Duration, NaiveDate, NaiveDateTime, NaiveTime, Utc};
 
fn main() {
    // ===== Creating Durations =====
    
    let five_days = Duration::days(5);
    let three_hours = Duration::hours(3);
    let thirty_minutes = Duration::minutes(30);
    let thousand_millis = Duration::milliseconds(1000);
    
    // Duration arithmetic
    let combined = five_days + three_hours;
    println!("Combined: {} days, {} hours", combined.num_days(), combined.num_hours());
    
    // ===== Date Arithmetic =====
    
    let date = NaiveDate::from_ymd_opt(2024, 3, 15).unwrap();
    
    let later = date + Duration::days(7);
    println!("One week later: {}", later);
    
    let earlier = date - Duration::days(7);
    println!("One week earlier: {}", earlier);
    
    // ===== DateTime Arithmetic =====
    
    let dt = NaiveDate::from_ymd_opt(2024, 3, 15).unwrap()
        .and_hms_opt(10, 0, 0).unwrap();
    
    let after = dt + Duration::hours(2) + Duration::minutes(30);
    println!("After 2h 30m: {}", after);
    
    // ===== Duration Between Dates =====
    
    let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
    let end = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
    
    let duration = end.signed_duration_since(start);
    println!("Days in year: {}", duration.num_days());
    
    // Compare dates
    if end > start {
        println!("End is after start");
    }
    
    // ===== Datetime Differences =====
    
    let dt1 = Utc::now();
    let dt2 = dt1 + Duration::hours(5);
    
    let diff = dt2.signed_duration_since(dt1);
    println!("Difference: {} minutes", diff.num_minutes());
}

Timezones and Conversions

use chrono::{DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, TimeZone, Utc};
 
fn main() {
    // ===== UTC to Local =====
    
    let utc_time: DateTime<Utc> = Utc::now();
    let local_time: DateTime<Local> = utc_time.with_timezone(&Local);
    
    println!("UTC: {}", utc_time);
    println!("Local: {}", local_time);
    
    // ===== Specific Timezones =====
    
    let est = FixedOffset::west_opt(5 * 3600).unwrap(); // UTC-5
    let pst = FixedOffset::west_opt(8 * 3600).unwrap(); // UTC-8
    let ist = FixedOffset::east_opt(5 * 3600 + 1800).unwrap(); // UTC+5:30
    
    let utc_dt = Utc::now();
    let est_dt: DateTime<FixedOffset> = utc_dt.with_timezone(&est);
    let pst_dt: DateTime<FixedOffset> = utc_dt.with_timezone(&pst);
    
    println!("EST: {}", est_dt);
    println!("PST: {}", pst_dt);
    
    // ===== Create DateTime with Timezone =====
    
    let naive = NaiveDate::from_ymd_opt(2024, 3, 15).unwrap()
        .and_hms_opt(14, 30, 0).unwrap();
    
    let with_offset = est.from_utc_datetime(&naive);
    println!("EST DateTime: {}", with_offset);
    
    // ===== Parse with Timezone =====
    
    let dt: DateTime<FixedOffset> = "2024-03-15T14:30:00-05:00".parse().unwrap();
    println!("Parsed with offset: {}", dt);
    
    // Convert to UTC
    let utc: DateTime<Utc> = dt.with_timezone(&Utc);
    println!("As UTC: {}", utc);
}

Date Components and Iteration

use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, Timelike, Weekday};
 
fn main() {
    let dt = NaiveDate::from_ymd_opt(2024, 3, 15).unwrap()
        .and_hms_opt(14, 30, 45).unwrap();
    
    // ===== Date Components =====
    
    println!("Year: {}", dt.year());
    println!("Month: {}", dt.month());
    println!("Day: {}", dt.day());
    println!("Hour: {}", dt.hour());
    println!("Minute: {}", dt.minute());
    println!("Second: {}", dt.second());
    
    // Weekday
    let weekday = dt.weekday();
    println!("Weekday: {:?} ({})", weekday, weekday.num_days_from_monday());
    
    // Day of year
    println!("Day of year: {}", dt.ordinal());
    
    // ISO week
    let iso_week = dt.iso_week();
    println!("ISO week: {}-{}", iso_week.year(), iso_week.week());
    
    // ===== Modify Components =====
    
    let next_month = dt.with_month(4).unwrap();
    let next_year = dt.with_year(2025).unwrap();
    let midnight = dt.with_hour(0).unwrap().with_minute(0).unwrap().with_second(0).unwrap();
    
    println!("Next month: {}", next_month);
    println!("Next year: {}", next_year);
    println!("Midnight: {}", midnight);
    
    // ===== Date Iteration =====
    
    let start = NaiveDate::from_ymd_opt(2024, 3, 1).unwrap();
    let end = NaiveDate::from_ymd_opt(2024, 3, 7).unwrap();
    
    let mut current = start;
    while current <= end {
        println!("{} - {:?}", current, current.weekday());
        current = current + Duration::days(1);
    }
    
    // Find next Monday
    let date = NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(); // Friday
    let mut next_monday = date;
    while next_monday.weekday() != Weekday::Mon {
        next_monday = next_monday + Duration::days(1);
    }
    println!("Next Monday after {}: {}", date, next_monday);
}

Parsing and Serialization with Serde

# Cargo.toml
[dependencies]
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
use chrono::{DateTime, NaiveDate, Utc};
use serde::{Deserialize, Serialize};
 
#[derive(Debug, Serialize, Deserialize)]
struct Event {
    name: String,
    #[serde(with = "chrono::serde::ts_seconds")]
    timestamp: DateTime<Utc>,
    #[serde(with = "chrono::serde::ts_milliseconds")]
    created_at: DateTime<Utc>,
    date: NaiveDate,
}
 
fn main() {
    let event = Event {
        name: "Meeting".to_string(),
        timestamp: Utc::now(),
        created_at: Utc::now(),
        date: NaiveDate::from_ymd_opt(2024, 3, 15).unwrap(),
    };
    
    // Serialize to JSON
    let json = serde_json::to_string(&event).unwrap();
    println!("JSON: {}", json);
    
    // Deserialize from JSON
    let parsed: Event = serde_json::from_str(&json).unwrap();
    println!("Parsed: {:?}", parsed);
}

Practical Examples

use chrono::{DateTime, Datelike, Duration, Local, NaiveDate, NaiveDateTime, TimeZone, Utc};
 
// Age calculation
fn calculate_age(birth_date: NaiveDate) -> i32 {
    let today = Local::now().date_naive();
    let mut age = today.year() - birth_date.year();
    
    if today.month() < birth_date.month() ||
       (today.month() == birth_date.month() && today.day() < birth_date.day()) {
        age -= 1;
    }
    
    age
}
 
// Time ago formatter
fn time_ago(dt: DateTime<Utc>) -> String {
    let now = Utc::now();
    let diff = now.signed_duration_since(dt);
    
    if diff.num_minutes() < 1 {
        "just now".to_string()
    } else if diff.num_minutes() < 60 {
        format!("{} minutes ago", diff.num_minutes())
    } else if diff.num_hours() < 24 {
        format!("{} hours ago", diff.num_hours())
    } else if diff.num_days() < 7 {
        format!("{} days ago", diff.num_days())
    } else {
        format!("{} weeks ago", diff.num_weeks())
    }
}
 
// Get start and end of day
fn start_of_day(dt: DateTime<Utc>) -> DateTime<Utc> {
    dt.with_hour(0).unwrap()
        .with_minute(0).unwrap()
        .with_second(0).unwrap()
        .with_nanosecond(0).unwrap()
}
 
fn end_of_day(dt: DateTime<Utc>) -> DateTime<Utc> {
    dt.with_hour(23).unwrap()
        .with_minute(59).unwrap()
        .with_second(59).unwrap()
        .with_nanosecond(999_999_999).unwrap()
}
 
// Check if date is weekend
fn is_weekend(date: NaiveDate) -> bool {
    matches!(date.weekday(), chrono::Weekday::Sat | chrono::Weekday::Sun)
}
 
fn main() {
    // Age calculation
    let birth = NaiveDate::from_ymd_opt(1990, 6, 15).unwrap();
    println!("Age: {}", calculate_age(birth));
    
    // Time ago
    let past = Utc::now() - Duration::hours(3);
    println!("Time ago: {}", time_ago(past));
    
    // Start/end of day
    let now = Utc::now();
    println!("Start of day: {}", start_of_day(now));
    println!("End of day: {}", end_of_day(now));
    
    // Weekend check
    let saturday = NaiveDate::from_ymd_opt(2024, 3, 16).unwrap();
    println!("Is weekend: {}", is_weekend(saturday));
}

Summary

  • Use Local::now() for current local time, Utc::now() for UTC
  • Create dates with NaiveDate::from_ymd_opt(year, month, day) (safe, returns Option)
  • Create times with NaiveTime::from_hms_opt(hour, min, sec)
  • Combine with NaiveDateTime::new(date, time) then add timezone with .and_utc()
  • Parse with parse_from_str(str, format) using strftime specifiers
  • Format with .format("%Y-%m-%d %H:%M:%S") — see strftime documentation
  • Use Duration::days(), Duration::hours(), etc. for time spans
  • Add/subtract durations: date + Duration::days(7)
  • Get difference: dt2.signed_duration_since(dt1)
  • Access components via .year(), .month(), .day(), .hour(), .weekday()
  • Modify components: .with_year(), .with_month(), .with_hour(), etc.
  • Convert timezones: dt.with_timezone(&Local) or dt.with_timezone(&FixedOffset)
  • Use serde feature for JSON serialization: chrono = { version = "0.4", features = ["serde"] }
  • Naive types have no timezone; DateTime types are timezone-aware
  • Always use _opt methods for safe construction (returns Option instead of panicking)