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:
NaiveDate— date without timezoneNaiveTime— time without timezoneNaiveDateTime— combined date and time without timezoneDateTime<Tz>— date and time with timezone (e.g.,DateTime<Utc>,DateTime<Local>)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)ordt.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
_optmethods for safe construction (returns Option instead of panicking)
