How do I work with Chrono for Date and Time Handling in Rust?

Walkthrough

Chrono is a comprehensive date and time library for Rust. It provides robust support for parsing, formatting, and manipulating dates, times, and time zones. Chrono is designed to be correct, handling edge cases like leap seconds, time zone transitions, and calendar arithmetic properly.

Key concepts:

  • DateTime — A date and time with timezone information
  • NaiveDateTime — A date and time without timezone
  • NaiveDate — A date without timezone
  • NaiveTime — A time without timezone
  • Duration — A span of time
  • TimeZone — Time zone handling (UTC, FixedOffset, Local)
  • Format stringsstrftime-like format specifiers

When to use Chrono:

  • Date/time arithmetic and calculations
  • Parsing and formatting dates
  • Time zone conversions
  • Scheduling and deadlines
  • Logging with timestamps
  • Calendar applications

When NOT to use Chrono:

  • Simple timing (use std::time::Instant)
  • Performance-critical inner loops
  • When you need only Unix timestamps (use std::time::SystemTime)

Code Examples

Current Date and Time

use chrono::{DateTime, Utc, Local, TimeZone};
 
fn main() {
    // Current UTC time
    let now_utc: DateTime<Utc> = Utc::now();
    println!("UTC: {}", now_utc);
    
    // Current local time
    let now_local: DateTime<Local> = Local::now();
    println!("Local: {}", now_local);
    
    // Individual components
    println!("Year: {}", now_utc.year());
    println!("Month: {}", now_utc.month());
    println!("Day: {}", now_utc.day());
    println!("Hour: {}", now_utc.hour());
    println!("Minute: {}", now_utc.minute());
    println!("Second: {}", now_utc.second());
}

Creating Specific Dates and Times

use chrono::{NaiveDate, NaiveTime, NaiveDateTime, Utc, TimeZone};
 
fn main() {
    // Create a date
    let date = NaiveDate::from_ymd_opt(2024, 1, 15).unwrap();
    println!("Date: {}", date);
    
    // Create a time
    let time = NaiveTime::from_hms_opt(14, 30, 0).unwrap();
    println!("Time: {}", time);
    
    // Combine date and time (naive - no timezone)
    let naive_dt = NaiveDateTime::new(date, time);
    println!("Naive DateTime: {}", naive_dt);
    
    // Add timezone (UTC)
    let dt_utc = Utc.from_utc_datetime(&naive_dt);
    println!("DateTime UTC: {}", dt_utc);
}

Date Arithmetic

use chrono::{Duration, NaiveDate, Utc, DateTime};
 
fn main() {
    let now = Utc::now();
    
    // Add/subtract duration
    let tomorrow = now + Duration::days(1);
    let yesterday = now - Duration::days(1);
    let next_week = now + Duration::weeks(1);
    let in_2_hours = now + Duration::hours(2);
    
    println!("Now: {}", now);
    println!("Tomorrow: {}", tomorrow);
    println!("Yesterday: {}", yesterday);
    
    // Duration between two dates
    let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
    let end = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
    let duration = end - start;
    
    println!("Days in year: {}", duration.num_days());
}

Formatting Dates

use chrono::{DateTime, Utc, Local};
 
fn main() {
    let now = Utc::now();
    
    // Default format
    println!("Default: {}", now);
    
    // RFC 2822 format
    println!("RFC 2822: {}", now.to_rfc2822());
    
    // RFC 3339 format (ISO 8601)
    println!("RFC 3339: {}", now.to_rfc3339());
    
    // Custom format
    println!("Custom: {}", now.format("%Y-%m-%d %H:%M:%S"));
    println!("Pretty: {}", now.format("%A, %B %d, %Y at %I:%M %p"));
}

Parsing Dates

use chrono::{NaiveDate, NaiveDateTime, DateTime, Utc, TimeZone};
 
fn main() {
    // Parse date
    let date = NaiveDate::parse_from_str("2024-01-15", "%Y-%m-%d").unwrap();
    println!("Date: {}", date);
    
    // Parse datetime
    let dt = NaiveDateTime::parse_from_str("2024-01-15 14:30:00", "%Y-%m-%d %H:%M:%S").unwrap();
    println!("DateTime: {}", dt);
    
    // Parse from RFC 3339
    let dt_rfc = DateTime::parse_from_rfc3339("2024-01-15T14:30:00Z").unwrap();
    println!("RFC 3339: {}", dt_rfc);
    
    // Parse from RFC 2822
    let dt_2822 = DateTime::parse_from_rfc2822("Mon, 15 Jan 2024 14:30:00 +0000").unwrap();
    println!("RFC 2822: {}", dt_2822);
}

Time Zones

use chrono::{DateTime, Utc, Local, FixedOffset, TimeZone};
 
fn main() {
    let now_utc: DateTime<Utc> = Utc::now();
    
    // Convert to local timezone
    let now_local: DateTime<Local> = now_utc.with_timezone(&Local);
    println!("Local: {}", now_local);
    
    // Fixed offset timezone (UTC+8)
    let offset = FixedOffset::east_opt(8 * 3600).unwrap();  // 8 hours in seconds
    let now_singapore: DateTime<FixedOffset> = now_utc.with_timezone(&offset);
    println!("Singapore: {}", now_singapore);
    
    // UTC-5 (Eastern Time without DST)
    let est = FixedOffset::west_opt(5 * 3600).unwrap();
    let now_est: DateTime<FixedOffset> = now_utc.with_timezone(&est);
    println!("EST: {}", now_est);
}

Unix Timestamps

use chrono::{DateTime, Utc, TimeZone};
 
fn main() {
    // Current Unix timestamp
    let now = Utc::now();
    let timestamp = now.timestamp();
    println!("Unix timestamp: {}", timestamp);
    
    // From timestamp to DateTime
    let from_ts: DateTime<Utc> = Utc.timestamp_opt(1700000000, 0).unwrap();
    println!("From timestamp: {}", from_ts);
    
    // With milliseconds
    let timestamp_millis = now.timestamp_millis();
    println!("Timestamp (ms): {}", timestamp_millis);
    
    // With nanoseconds
    let timestamp_nanos = now.timestamp_nanos_opt().unwrap();
    println!("Timestamp (ns): {}", timestamp_nanos);
}

Weekday and Week Calculations

use chrono::{NaiveDate, Weekday, Datelike};
 
fn main() {
    let date = NaiveDate::from_ymd_opt(2024, 1, 15).unwrap();
    
    // Get weekday
    let weekday = date.weekday();
    println!("Weekday: {:?}", weekday);  // Monday
    
    // Week number (ISO week)
    let iso_week = date.iso_week();
    println!("ISO Week: {}-{:02}", iso_week.year(), iso_week.week());
    
    // Day of year
    let day_of_year = date.ordinal();
    println!("Day of year: {}", day_of_year);
    
    // Is weekend?
    let is_weekend = matches!(weekday, Weekday::Sat | Weekday::Sun);
    println!("Is weekend: {}", is_weekend);
}

Comparing Dates

use chrono::{NaiveDate, NaiveDateTime, Utc, DateTime};
 
fn main() {
    let date1 = NaiveDate::from_ymd_opt(2024, 1, 15).unwrap();
    let date2 = NaiveDate::from_ymd_opt(2024, 6, 20).unwrap();
    
    // Comparison
    println!("date1 < date2: {}", date1 < date2);
    println!("Equal: {}", date1 == date1);
    
    // Duration between dates
    let diff = date2 - date1;
    println!("Days between: {}", diff.num_days());
    
    // Check if date is between two dates
    let middle = NaiveDate::from_ymd_opt(2024, 3, 1).unwrap();
    let is_between = date1 < middle && middle < date2;
    println!("March 1st is between: {}", is_between);
}

Date Ranges and Iteration

use chrono::{NaiveDate, Duration};
 
fn main() {
    let start = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
    let end = NaiveDate::from_ymd_opt(2024, 1, 7).unwrap();
    
    // Iterate over dates
    let mut current = start;
    while current <= end {
        println!("{} - {:?}", current, current.weekday());
        current = current + Duration::days(1);
    }
}

Human-Readable Time Differences

use chrono::{DateTime, Utc, Duration};
 
fn humanize_duration(dt: DateTime<Utc>) -> String {
    let now = Utc::now();
    let diff = now - 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 {
        format!("{} days ago", diff.num_days())
    }
}
 
fn main() {
    let past = Utc::now() - Duration::minutes(30);
    println!("{}", humanize_duration(past));
    
    let past2 = Utc::now() - Duration::hours(5);
    println!("{}", humanize_duration(past2));
}

Working with Months and Years

use chrono::{NaiveDate, Datelike};
 
fn main() {
    let date = NaiveDate::from_ymd_opt(2024, 1, 31).unwrap();
    
    // Add months (handles overflow)
    // Note: Chrono doesn't have month arithmetic directly
    // Use with_month or checked_add_months
    
    // Last day of next month
    let next_month = if date.month() == 12 {
        NaiveDate::from_ymd_opt(date.year() + 1, 1, 31)
    } else {
        NaiveDate::from_ymd_opt(date.year(), date.month() + 1, 31)
    };
    
    // Note: January 31 + 1 month doesn't have Feb 31
    // Chrono handles this with checked arithmetic
    
    // Get number of days in month
    let days_in_month = NaiveDate::from_ymd_opt(2024, 2, 1).unwrap()
        .with_day(28).unwrap()  // Try 28 first (always valid)
        .with_day(29).unwrap(); // Try 29 (leap year check)
    
    println!("Feb 2024 has 29 days (leap year)");
    
    // Check leap year
    let is_leap = 2024 % 4 == 0 && (2024 % 100 != 0 || 2024 % 400 == 0);
    println!("2024 is leap year: {}", is_leap);
}

Parsing from Multiple Formats

use chrono::{NaiveDate, NaiveDateTime};
 
fn parse_flexible(input: &str) -> Option<NaiveDateTime> {
    // Try multiple formats
    let formats = [
        "%Y-%m-%d %H:%M:%S",
        "%Y-%m-%dT%H:%M:%S",
        "%Y/%m/%d %H:%M:%S",
        "%d-%m-%Y %H:%M:%S",
        "%Y-%m-%d",
    ];
    
    for fmt in formats {
        if let Ok(dt) = NaiveDateTime::parse_from_str(input, fmt) {
            return Some(dt);
        }
        // Try as date only
        if let Ok(date) = NaiveDate::parse_from_str(input, fmt) {
            return Some(date.and_hms_opt(0, 0, 0).unwrap());
        }
    }
    None
}
 
fn main() {
    let inputs = [
        "2024-01-15 14:30:00",
        "2024-01-15T14:30:00",
        "2024/01/15 14:30:00",
        "2024-01-15",
    ];
    
    for input in inputs {
        if let Some(dt) = parse_flexible(input) {
            println!("Parsed '{}': {}", input, dt);
        }
    }
}

Date Validation

use chrono::NaiveDate;
 
fn is_valid_date(year: i32, month: u32, day: u32) -> bool {
    NaiveDate::from_ymd_opt(year, month, day).is_some()
}
 
fn main() {
    println!("2024-01-15: {}", is_valid_date(2024, 1, 15));  // true
    println!("2024-02-30: {}", is_valid_date(2024, 2, 30)); // false
    println!("2023-02-29: {}", is_valid_date(2023, 2, 29)); // false (not leap year)
    println!("2024-02-29: {}", is_valid_date(2024, 2, 29)); // true (leap year)
    println!("2024-13-01: {}", is_valid_date(2024, 13, 1)); // false
}

Serde Integration

use chrono::{DateTime, Utc, Serde};
use serde::{Serialize, Deserialize};
 
#[derive(Serialize, Deserialize, Debug)]
struct Event {
    name: String,
    #[serde(with = "Serde")]
    timestamp: DateTime<Utc>,
}
 
fn main() {
    let event = Event {
        name: "Meeting".to_string(),
        timestamp: Utc::now(),
    };
    
    let json = serde_json::to_string(&event).unwrap();
    println!("JSON: {}", json);
    
    let parsed: Event = serde_json::from_str(&json).unwrap();
    println!("Parsed: {:?}", parsed);
}

Countdown Timer

use chrono::{DateTime, Utc, Duration};
 
fn time_until(target: DateTime<Utc>) -> (i64, i64, i64) {
    let now = Utc::now();
    let diff = target - now;
    
    let total_seconds = diff.num_seconds();
    let hours = total_seconds / 3600;
    let minutes = (total_seconds % 3600) / 60;
    let seconds = total_seconds % 60;
    
    (hours, minutes, seconds)
}
 
fn main() {
    // Target: 24 hours from now
    let target = Utc::now() + Duration::hours(24);
    
    let (h, m, s) = time_until(target);
    println!("Time remaining: {}h {}m {}s", h, m, s);
}

Business Day Calculations

use chrono::{NaiveDate, Weekday, Duration, Datelike};
 
fn is_business_day(date: NaiveDate) -> bool {
    !matches!(date.weekday(), Weekday::Sat | Weekday::Sun)
}
 
fn add_business_days(start: NaiveDate, days: u32) -> NaiveDate {
    let mut current = start;
    let mut added = 0;
    
    while added < days {
        current = current + Duration::days(1);
        if is_business_day(current) {
            added += 1;
        }
    }
    
    current
}
 
fn main() {
    let start = NaiveDate::from_ymd_opt(2024, 1, 15).unwrap();  // Monday
    
    let five_business_days = add_business_days(start, 5);
    println!("5 business days from Monday: {} ({:?})", 
             five_business_days, five_business_days.weekday());
}

Age Calculator

use chrono::{NaiveDate, Local, Datelike};
 
fn calculate_age(birthday: NaiveDate) -> u32 {
    let today = Local::now().date_naive();
    
    let mut age = today.year() - birthday.year();
    
    // Adjust if birthday hasn't occurred this year
    let birthday_this_year = birthday
        .with_year(today.year())
        .unwrap();
    
    if birthday_this_year > today {
        age -= 1;
    }
    
    age as u32
}
 
fn main() {
    let birthday = NaiveDate::from_ymd_opt(1990, 6, 15).unwrap();
    println!("Age: {}", calculate_age(birthday));
}

Cron-Like Scheduling

use chrono::{DateTime, Utc, Datelike, Timelike};
 
fn next_hourly() -> DateTime<Utc> {
    let now = Utc::now();
    now.with_minute(0).unwrap()
        .with_second(0).unwrap()
        .with_nanosecond(0).unwrap()
        + chrono::Duration::hours(1)
}
 
fn next_daily(hour: u32) -> DateTime<Utc> {
    let now = Utc::now();
    let next = now
        .with_hour(hour).unwrap()
        .with_minute(0).unwrap()
        .with_second(0).unwrap()
        .with_nanosecond(0).unwrap();
    
    if next > now {
        next
    } else {
        next + chrono::Duration::days(1)
    }
}
 
fn main() {
    println!("Next hourly: {}", next_hourly());
    println!("Next daily at 9am: {}", next_daily(9));
}

Performance-Safe Duration

use chrono::{Duration, NaiveDate};
 
fn main() {
    let date = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
    
    // Checked arithmetic
    match date.checked_add_signed(Duration::days(365)) {
        Some(new_date) => println!("One year later: {}", new_date),
        None => println!("Overflow occurred"),
    }
    
    // Also checked_sub_signed, checked_add_days, etc.
    let earlier = date.checked_sub_days(chrono::Days::new(1));
    println!("Previous day: {:?}", earlier);
}

Summary

Chrono Key Imports:

use chrono::{DateTime, Utc, Local, NaiveDate, NaiveTime, NaiveDateTime, Duration, TimeZone};

Core Types:

Type Description
DateTime<Tz> Date and time with timezone
NaiveDateTime Date and time without timezone
NaiveDate Date without timezone
NaiveTime Time without timezone
Duration Span of time

Time Zones:

Type Description
Utc UTC timezone
Local System local timezone
FixedOffset Fixed UTC offset

Format Specifiers:

Specifier Description
%Y 4-digit year
%m Month (01-12)
%d Day (01-31)
%H Hour (00-23)
%M Minute (00-59)
%S Second (00-59)
%A Weekday name
%B Month name
%I Hour (01-12)
%p AM/PM

Common Operations:

// Current time
let now = Utc::now();
 
// Create date
let date = NaiveDate::from_ymd_opt(2024, 1, 15).unwrap();
 
// Add duration
let tomorrow = now + Duration::days(1);
 
// Format
let s = now.format("%Y-%m-%d %H:%M:%S").to_string();
 
// Parse
let dt = NaiveDateTime::parse_from_str("2024-01-15 14:30:00", "%Y-%m-%d %H:%M:%S").unwrap();

Key Points:

  • Use Utc for storage, Local for display
  • Naive* types have no timezone info
  • Duration for time spans
  • Use checked_add_signed for safe arithmetic
  • Serde integration available with #[serde(with = "Serde")]
  • from_ymd_opt returns Option for validation
  • Use with_timezone to convert between time zones