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 strings —
strftime-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
Utcfor storage,Localfor display Naive*types have no timezone infoDurationfor time spans- Use
checked_add_signedfor safe arithmetic - Serde integration available with
#[serde(with = "Serde")] from_ymd_optreturnsOptionfor validation- Use
with_timezoneto convert between time zones
