How do I work with dates and times in Rust?

Walkthrough

The chrono crate is the de facto standard for date and time handling in Rust. It provides types for dates, times, datetimes, durations, and timezones. Chrono focuses on correctness, ergonomics, and interoperability with the standard library. It handles parsing, formatting, arithmetic, timezone conversions, and more.

Key types:

  1. DateTime<Utc> — datetime with UTC timezone
  2. DateTime<Local> — datetime in local timezone
  3. NaiveDateTime — datetime without timezone
  4. NaiveDate — date without timezone
  5. NaiveTime — time without timezone
  6. Duration — span of time

Chrono is feature-rich and well-tested for real-world datetime handling.

Code Example

# Cargo.toml
[dependencies]
chrono = "0.4"
use chrono::{DateTime, Utc, Local, NaiveDate, NaiveTime, NaiveDateTime, Duration};
 
fn main() {
    // Current time
    let now_utc: DateTime<Utc> = Utc::now();
    let now_local: DateTime<Local> = Local::now();
    
    println!("UTC: {}", now_utc);
    println!("Local: {}", now_local);
    
    // Create specific date and time
    let date = NaiveDate::from_ymd_opt(2024, 3, 15).unwrap();
    let time = NaiveTime::from_hms_opt(14, 30, 0).unwrap();
    let datetime = NaiveDateTime::new(date, time);
    
    println!("DateTime: {}", datetime);
}

Creating Dates and Times

use chrono::{NaiveDate, NaiveTime, NaiveDateTime, DateTime, Utc, Local, TimeZone};
 
fn main() {
    // Create a date
    let date1 = NaiveDate::from_ymd_opt(2024, 3, 15).unwrap();
    println!("Date: {}", date1);
    
    // Create a date from ordinal (year, day of year)
    let date2 = NaiveDate::from_yo_opt(2024, 75).unwrap();  // 75th day of 2024
    println!("From ordinal: {}", date2);
    
    // Create a date from ISO week
    let date3 = NaiveDate::from_isoywd_opt(2024, 10, chrono::Weekday::Mon).unwrap();
    println!("From ISO week: {}", date3);
    
    // Parse a date from string
    let date4 = NaiveDate::parse_from_str("2024-03-15", "%Y-%m-%d").unwrap();
    println!("Parsed date: {}", date4);
    
    // Create a time
    let time1 = NaiveTime::from_hms_opt(14, 30, 45).unwrap();
    println!("Time: {}", time1);
    
    let time2 = NaiveTime::from_hms_milli_opt(14, 30, 45, 500).unwrap();
    println!("Time with millis: {}", time2);
    
    let time3 = NaiveTime::from_hms_micro_opt(14, 30, 45, 500_000).unwrap();
    println!("Time with micros: {}", time3);
    
    // Parse time from string
    let time4 = NaiveTime::parse_from_str("14:30:45", "%H:%M:%S").unwrap();
    println!("Parsed time: {}", time4);
    
    // Combine date and time
    let datetime = NaiveDateTime::new(date1, time1);
    println!("DateTime: {}", datetime);
    
    // Create DateTime with timezone
    let utc_dt = Utc.with_ymd_and_hms(2024, 3, 15, 14, 30, 45).unwrap();
    println!("UTC DateTime: {}", utc_dt);
    
    let local_dt = Local.with_ymd_and_hms(2024, 3, 15, 14, 30, 45).unwrap();
    println!("Local DateTime: {}", local_dt);
}

Current Time and Timezones

use chrono::{DateTime, Utc, Local, FixedOffset, TimeZone};
 
fn main() {
    // Current time in different timezones
    let utc_now: DateTime<Utc> = Utc::now();
    let local_now: DateTime<Local> = Local::now();
    
    println!("UTC now: {}", utc_now);
    println!("Local now: {}", local_now);
    println!("Local timezone: {}", local_now.timezone());
    
    // Create a fixed offset timezone (UTC+8)
    let east8 = FixedOffset::east_opt(8 * 3600).unwrap();
    let dt_east8 = east8.from_utc_datetime(&utc_now.naive_utc());
    println!("UTC+8: {}", dt_east8);
    
    // Create a fixed offset timezone (UTC-5)
    let west5 = FixedOffset::west_opt(5 * 3600).unwrap();
    let dt_west5 = west5.from_utc_datetime(&utc_now.naive_utc());
    println!("UTC-5: {}", dt_west5);
    
    // Convert between timezones
    let utc_dt = Utc.with_ymd_and_hms(2024, 3, 15, 14, 30, 0).unwrap();
    let local_dt: DateTime<Local> = utc_dt.into();
    println!("\nConverted UTC to local: {}", local_dt);
    
    // Get timezone offset
    let offset = local_now.offset();
    println!("Local offset: {}", offset);
    
    // Unix timestamp
    let timestamp = utc_now.timestamp();
    println!("\nUnix timestamp: {}", timestamp);
    
    // From timestamp
    let from_timestamp = Utc.timestamp_opt(timestamp, 0).unwrap();
    println!("From timestamp: {}", from_timestamp);
}

Formatting and Parsing

use chrono::{DateTime, Utc, NaiveDate, NaiveDateTime};
 
fn main() {
    let dt = Utc.with_ymd_and_hms(2024, 3, 15, 14, 30, 45).unwrap();
    
    // Common formats
    println!("Default: {}", dt);
    println!("RFC 2822: {}", dt.to_rfc2822());
    println!("RFC 3339: {}", dt.to_rfc3339());
    
    // Custom formatting
    println!("\nCustom formats:");
    println!("Date: {}", dt.format("%Y-%m-%d"));
    println!("Time: {}", dt.format("%H:%M:%S"));
    println!("DateTime: {}", dt.format("%Y-%m-%d %H:%M:%S"));
    println!("US format: {}", dt.format("%m/%d/%Y %I:%M %p"));
    println!("European: {}", dt.format("%d.%m.%Y %H:%M"));
    println!("Verbose: {}", dt.format("%A, %B %d, %Y at %I:%M %p"));
    
    // Format specifiers
    println!("\nFormat specifiers:");
    println!("Year: {}", dt.format("%Y"));      // 2024
    println!("Year (short): {}", dt.format("%y")); // 24
    println!("Month: {}", dt.format("%m"));     // 03
    println!("Month name: {}", dt.format("%B")); // March
    println!("Month (short): {}", dt.format("%b")); // Mar
    println!("Day: {}", dt.format("%d"));       // 15
    println!("Weekday: {}", dt.format("%A"));   // Friday
    println!("Weekday (short): {}", dt.format("%a")); // Fri
    println!("Hour (24): {}", dt.format("%H")); // 14
    println!("Hour (12): {}", dt.format("%I")); // 02
    println!("Minute: {}", dt.format("%M"));    // 30
    println!("Second: {}", dt.format("%S"));    // 45
    println!("AM/PM: {}", dt.format("%p"));     // PM
    
    // Parsing
    println!("\nParsing:");
    let parsed1 = NaiveDate::parse_from_str("2024-03-15", "%Y-%m-%d").unwrap();
    println!("Parsed date: {}", parsed1);
    
    let parsed2 = NaiveDateTime::parse_from_str(
        "2024-03-15 14:30:45",
        "%Y-%m-%d %H:%M:%S"
    ).unwrap();
    println!("Parsed datetime: {}", parsed2);
    
    let parsed3 = DateTime::parse_from_rfc3339("2024-03-15T14:30:45Z").unwrap();
    println!("Parsed RFC 3339: {}", parsed3);
    
    let parsed4 = DateTime::parse_from_rfc2822("Fri, 15 Mar 2024 14:30:45 +0000").unwrap();
    println!("Parsed RFC 2822: {}", parsed4);
    
    // Parse with timezone
    let parsed5 = DateTime::parse_from_str(
        "2024-03-15 14:30:45 +0800",
        "%Y-%m-%d %H:%M:%S %z"
    ).unwrap();
    println!("Parsed with TZ: {}", parsed5);
}

Date and Time Arithmetic

use chrono::{Duration, NaiveDate, NaiveDateTime, DateTime, Utc, Days, Months};
 
fn main() {
    let dt = Utc.with_ymd_and_hms(2024, 3, 15, 14, 30, 0).unwrap();
    println!("Original: {}", dt);
    
    // Add/subtract durations
    let tomorrow = dt + Duration::days(1);
    let yesterday = dt - Duration::days(1);
    let next_week = dt + Duration::weeks(1);
    let last_hour = dt - Duration::hours(1);
    let in_30_min = dt + Duration::minutes(30);
    let in_5_sec = dt + Duration::seconds(5);
    
    println!("\nDuration arithmetic:");
    println!("+1 day: {}", tomorrow);
    println!("-1 day: {}", yesterday);
    println!("+1 week: {}", next_week);
    println!("-1 hour: {}", last_hour);
    println!("+30 min: {}", in_30_min);
    println!("+5 sec: {}", in_5_sec);
    
    // Duration from components
    let duration = Duration::new(3600, 500_000_000);  // 3600 seconds + 500ms
    println!("\nCustom duration: {} seconds", duration.num_seconds());
    
    // Difference between dates
    let date1 = NaiveDate::from_ymd_opt(2024, 3, 15).unwrap();
    let date2 = NaiveDate::from_ymd_opt(2024, 3, 20).unwrap();
    let diff = date2 - date1;
    println!("\nDifference: {} days", diff.num_days());
    
    // Compare dates
    println!("date1 < date2: {}", date1 < date2);
    
    // Add days (checked)
    let date = NaiveDate::from_ymd_opt(2024, 3, 15).unwrap();
    let future_date = date.checked_add_days(Days::new(10)).unwrap();
    println!("\n+10 days: {}", future_date);
    
    // Add months (handles month boundaries)
    let jan_31 = NaiveDate::from_ymd_opt(2024, 1, 31).unwrap();
    let feb = jan_31.checked_add_months(Months::new(1)).unwrap();
    println!("Jan 31 + 1 month: {}", feb);  // Feb 29 (leap year)
    
    // Calculate age
    let birth_date = NaiveDate::from_ymd_opt(1990, 6, 15).unwrap();
    let today = Utc::now().date_naive();
    let age = today.years_since(birth_date).unwrap();
    println!("\nAge: {} years", age);
}

Date Components and Queries

use chrono::{DateTime, Utc, Datelike, Timelike, Weekday};
 
fn main() {
    let dt = Utc.with_ymd_and_hms(2024, 3, 15, 14, 30, 45).unwrap();
    
    // Date components
    println!("Date components:");
    println!("  Year: {}", dt.year());
    println!("  Month: {}", dt.month());
    println!("  Day: {}", dt.day());
    println!("  Ordinal (day of year): {}", dt.ordinal());
    println!("  Weekday: {}", dt.weekday());
    
    // Time components
    println!("\nTime components:");
    println!("  Hour: {}", dt.hour());
    println!("  Minute: {}", dt.minute());
    println!("  Second: {}", dt.second());
    println!("  Nanosecond: {}", dt.nanosecond());
    
    // Weekday checks
    println!("\nWeekday:");
    println!("  Is weekday: {}", !matches!(dt.weekday(), Weekday::Sat | Weekday::Sun));
    println!("  Is weekend: {}", matches!(dt.weekday(), Weekday::Sat | Weekday::Sun));
    println!("  Is Friday: {}", dt.weekday() == Weekday::Fri);
    
    // Week of year
    let iso_week = dt.iso_week();
    println!("\nISO Week:");
    println!("  Week number: {}", iso_week.week());
    println!("  Year: {}", iso_week.year());
    
    // Quarter
    let quarter = (dt.month() - 1) / 3 + 1;
    println!("\nQuarter: Q{}", quarter);
    
    // Check if leap year
    let year = dt.year();
    let is_leap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
    println!("Is leap year: {}", is_leap);
    
    // Days in month
    let days_in_month = dt.with_day(1).unwrap()
        .with_month(dt.month() % 12 + 1).unwrap()
        .pred_opt().unwrap()
        .day();
    println!("Days in month: {}", days_in_month);
    
    // Time of day checks
    let hour = dt.hour();
    if hour < 12 {
        println!("\nGood morning!");
    } else if hour < 17 {
        println!("\nGood afternoon!");
    } else {
        println!("\nGood evening!");
    }
}

Duration Calculations

use chrono::{DateTime, Utc, Duration, NaiveDate};
 
fn main() {
    // Create Duration from various units
    let d1 = Duration::seconds(90);
    let d2 = Duration::minutes(5);
    let d3 = Duration::hours(2);
    let d4 = Duration::days(7);
    let d5 = Duration::weeks(2);
    
    println!("Duration from units:");
    println!("  90 seconds: {} seconds", d1.num_seconds());
    println!("  5 minutes: {} seconds", d2.num_seconds());
    println!("  2 hours: {} minutes", d3.num_minutes());
    println!("  7 days: {} hours", d4.num_hours());
    println!("  2 weeks: {} days", d5.num_days());
    
    // Duration arithmetic
    let combined = Duration::hours(1) + Duration::minutes(30);
    println!("\n1h + 30m = {} minutes", combined.num_minutes());
    
    let doubled = Duration::hours(2) * 2;
    println!("2h * 2 = {} hours", doubled.num_hours());
    
    // Difference between DateTimes
    let start = Utc.with_ymd_and_hms(2024, 3, 15, 9, 0, 0).unwrap();
    let end = Utc.with_ymd_and_hms(2024, 3, 15, 17, 30, 0).unwrap();
    let elapsed = end - start;
    println!("\nWork day: {} hours {} minutes", 
             elapsed.num_hours(), 
             elapsed.num_minutes() % 60);
    
    // Calculate time until event
    let now = Utc::now();
    let event = Utc.with_ymd_and_hms(2024, 12, 25, 0, 0, 0).unwrap();
    let until = event - now;
    println!("\nUntil Christmas: {} days", until.num_days());
    
    // Age calculation
    let birth = NaiveDate::from_ymd_opt(1990, 6, 15).unwrap();
    let today = Utc::now().date_naive();
    let age_duration = today - birth;
    println!("\nAge in days: {}", age_duration.num_days());
    println!("Age in years (approx): {}", age_duration.num_days() / 365);
    
    // Countdown timer
    let deadline = Utc::now() + Duration::hours(24);
    let remaining = deadline - Utc::now();
    println!("\nDeadline in {} hours", remaining.num_hours());
}

Iterating Over Date Ranges

use chrono::{NaiveDate, Duration, Days};
 
fn main() {
    // Iterate over a range of dates
    let start = NaiveDate::from_ymd_opt(2024, 3, 1).unwrap();
    let end = NaiveDate::from_ymd_opt(2024, 3, 7).unwrap();
    
    println!("Week of March 2024:");
    let mut current = start;
    while current <= end {
        println!("  {} - {}", current.format("%Y-%m-%d"), current.weekday());
        current = current + Duration::days(1);
    }
    
    // Generate all days in a month
    println!("\nAll days in March 2024:");
    let march_1 = NaiveDate::from_ymd_opt(2024, 3, 1).unwrap();
    let april_1 = NaiveDate::from_ymd_opt(2024, 4, 1).unwrap();
    
    let days_in_march: Vec<NaiveDate> = (0..)
        .map(|i| march_1.checked_add_days(Days::new(i)).unwrap())
        .take_while(|&d| d < april_1)
        .collect();
    
    for day in &days_in_march {
        println!("  {}", day.format("%a %d"));
    }
    println!("Total days: {}", days_in_march.len());
    
    // Find all weekends in a month
    println!("\nWeekends in March 2024:");
    for day in &days_in_march {
        if matches!(day.weekday(), chrono::Weekday::Sat | chrono::Weekday::Sun) {
            println!("  {} - {}", day.format("%Y-%m-%d"), day.weekday());
        }
    }
    
    // Get all Mondays in a range
    println!("\nMondays in range:");
    for day in &days_in_march {
        if day.weekday() == chrono::Weekday::Mon {
            println!("  {}", day.format("%Y-%m-%d"));
        }
    }
}

Parsing Various Date Formats

use chrono::{NaiveDate, NaiveDateTime, NaiveTime, DateTime, Utc};
 
fn parse_date_flexible(input: &str) -> Option<NaiveDate> {
    // Try various common formats
    let formats = [
        "%Y-%m-%d",      // 2024-03-15
        "%d/%m/%Y",      // 15/03/2024
        "%m/%d/%Y",      // 03/15/2024
        "%Y%m%d",        // 20240315
        "%d-%b-%Y",      // 15-Mar-2024
        "%d %B %Y",      // 15 March 2024
        "%B %d, %Y",     // March 15, 2024
    ];
    
    for fmt in &formats {
        if let Ok(date) = NaiveDate::parse_from_str(input, fmt) {
            return Some(date);
        }
    }
    None
}
 
fn main() {
    // Parse various date formats
    let date_strings = [
        "2024-03-15",
        "15/03/2024",
        "03/15/2024",
        "20240315",
        "15-Mar-2024",
        "15 March 2024",
        "March 15, 2024",
    ];
    
    println!("Parsing dates:");
    for s in &date_strings {
        match parse_date_flexible(s) {
            Some(date) => println!("  '{}' -> {}", s, date),
            None => println!("  '{}' -> Failed to parse", s),
        }
    }
    
    // Parse datetime with timezone
    let inputs = [
        "2024-03-15T14:30:45Z",
        "2024-03-15T14:30:45+00:00",
        "2024-03-15T14:30:45-05:00",
        "2024-03-15 14:30:45",
    ];
    
    println!("\nParsing datetimes:");
    for input in &inputs {
        if let Ok(dt) = DateTime::parse_from_rfc3339(input) {
            println!("  '{}' -> {}", input, dt);
        } else if let Ok(dt) = NaiveDateTime::parse_from_str(input, "%Y-%m-%d %H:%M:%S") {
            println!("  '{}' -> {} (naive)", input, dt);
        } else {
            println!("  '{}' -> Failed", input);
        }
    }
}

Real-World Examples

use chrono::{DateTime, Utc, Local, NaiveDate, Duration, Datelike, TimeZone};
 
fn format_elapsed(seconds: i64) -> String {
    if seconds < 60 {
        format!("{} seconds ago", seconds)
    } else if seconds < 3600 {
        format!("{} minutes ago", seconds / 60)
    } else if seconds < 86400 {
        format!("{} hours ago", seconds / 3600)
    } else {
        format!("{} days ago", seconds / 86400)
    }
}
 
fn is_business_day(date: NaiveDate) -> bool {
    !matches!(date.weekday(), chrono::Weekday::Sat | chrono::Weekday::Sun)
}
 
fn next_business_day(date: NaiveDate) -> NaiveDate {
    let mut next = date + Duration::days(1);
    while !is_business_day(next) {
        next = next + Duration::days(1);
    }
    next
}
 
fn days_until_deadline(deadline: NaiveDate) -> i64 {
    let today = Utc::now().date_naive();
    (deadline - today).num_days()
}
 
fn format_datetime_friendly(dt: DateTime<Local>) -> String {
    let now = Local::now();
    let diff = now - dt;
    
    if diff.num_minutes() < 1 {
        "just now".to_string()
    } else if diff.num_hours() < 1 {
        format!("{} minutes ago", diff.num_minutes())
    } else if diff.num_days() < 1 {
        format!("{} hours ago", diff.num_hours())
    } else if diff.num_days() < 7 {
        format!("{} days ago", diff.num_days())
    } else {
        dt.format("%b %d, %Y").to_string()
    }
}
 
fn main() {
    // Time elapsed
    println!("Time elapsed:");
    println!("  30 seconds: {}", format_elapsed(30));
    println!("  5 minutes: {}", format_elapsed(300));
    println!("  2 hours: {}", format_elapsed(7200));
    println!("  3 days: {}", format_elapsed(259200));
    
    // Business days
    let friday = NaiveDate::from_ymd_opt(2024, 3, 15).unwrap();
    let saturday = NaiveDate::from_ymd_opt(2024, 3, 16).unwrap();
    
    println!("\nBusiness days:");
    println!("  {} is business day: {}", friday, is_business_day(friday));
    println!("  {} is business day: {}", saturday, is_business_day(saturday));
    println!("  Next business day after Fri: {}", next_business_day(friday));
    println!("  Next business day after Sat: {}", next_business_day(saturday));
    
    // Deadline countdown
    let deadline = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
    println!("\nDays until deadline: {}", days_until_deadline(deadline));
    
    // Friendly datetime
    let past = Local.with_ymd_and_hms(2024, 3, 15, 10, 0, 0).unwrap();
    println!("\nFriendly format: {}", format_datetime_friendly(past));
}

Serialization with Serde

# Cargo.toml
[dependencies]
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
use chrono::{DateTime, Utc, NaiveDate};
use serde::{Deserialize, Serialize};
 
#[derive(Debug, Serialize, Deserialize)]
struct Event {
    name: String,
    #[serde(with = "chrono::serde::ts_seconds")]
    timestamp: DateTime<Utc>,
    date: NaiveDate,
}
 
fn main() {
    let event = Event {
        name: "Conference".to_string(),
        timestamp: Utc.with_ymd_and_hms(2024, 6, 15, 9, 0, 0).unwrap(),
        date: NaiveDate::from_ymd_opt(2024, 6, 15).unwrap(),
    };
    
    // Serialize
    let json = serde_json::to_string(&event).unwrap();
    println!("JSON: {}", json);
    
    // Deserialize
    let decoded: Event = serde_json::from_str(&json).unwrap();
    println!("Decoded: {:?}", decoded);
}

Summary

  • Use chrono crate for date and time handling in Rust
  • Utc::now() and Local::now() for current datetime
  • NaiveDate::from_ymd_opt(year, month, day) to create dates
  • NaiveTime::from_hms_opt(hour, minute, second) to create times
  • DateTime<Utc> and DateTime<Local> for timezone-aware datetimes
  • Use .format("pattern") for custom formatting
  • Common patterns: %Y-%m-%d, %H:%M:%S, %Y-%m-%d %H:%M:%S
  • Parse with parse_from_str(input, pattern)
  • Use Duration for time spans: Duration::days(n), Duration::hours(n)
  • Add/subtract with + Duration and - Duration
  • Use Datelike trait for date components: .year(), .month(), .day()
  • Use Timelike trait for time components: .hour(), .minute(), .second()
  • .weekday() returns Weekday enum (Mon, Tue, Wed, Thu, Fri, Sat, Sun)
  • .timestamp() for Unix timestamp
  • Use serde feature for JSON serialization
  • Use .checked_add_months() and .checked_add_days() for safe arithmetic
  • Handle month/year boundaries carefully when adding months