Rust Dates and Times with chrono
Programs deal with time all the time. “How long did this take?” “What day is the invoice due?” “How many days are left until the trip?” Rust splits these questions into two kinds.
The standard library’s std::time module is a stopwatch: it measures how much time passes. It does not know about calendars, months, or weekdays. For calendar work, almost every Rust project uses the chrono crate. This page starts with the stopwatch, then moves on to the calendar.
Measuring elapsed time with Instant
Section titled “Measuring elapsed time with Instant”Instant::now() takes a reading from the computer’s clock. Later, .elapsed() tells you how much time has passed since that reading, as a Duration:
use std::time::{Duration, Instant};
fn main() { let start = Instant::now();
let mut total: u64 = 0; for i in 0..1_000_000 { total += i; }
let elapsed = start.elapsed(); println!("total = {total}"); println!("took {:?}", elapsed); println!("took {} ms", elapsed.as_millis()); println!("under 5 seconds? {}", elapsed < Duration::from_secs(5));}We don’t show the output here on purpose: the time is different on every computer and on every run. You will see something like took 6.862793ms. Try clicking the button twice and compare.
An Instant is only useful for measuring. You can’t print it as a date, and it only goes forward. That’s exactly what you want for timing code, because it is never affected by someone changing the system clock.
Durations
Section titled “Durations”A Duration is a length of time, like “90 seconds”. You create one from seconds or milliseconds, add durations together, and compare them:
use std::time::Duration;
fn main() { let short = Duration::from_millis(1500); let long = Duration::from_secs(90);
println!("{:?}", short); println!("{:?}", long); println!("{} ms", short.as_millis()); println!("{:.1} s", short.as_secs_f64());
let total = short + long; println!("total: {:?}", total); println!("{} min {} s", long.as_secs() / 60, long.as_secs() % 60); println!("{}", short < long);}Output:
1.5s90s1500 ms1.5 stotal: 91.5s1 min 30 strueYou’ll meet Duration in many places: std::thread::sleep(Duration::from_millis(100)) pauses a thread, and timeouts in network libraries take a Duration too.
Wall-clock time with SystemTime
Section titled “Wall-clock time with SystemTime”SystemTime is the time on the computer’s clock, the one you see in the corner of your screen. The usual thing to do with it is to ask how long it has been since the Unix epoch, 1 January 1970 (UTC). That number is called a Unix timestamp:
use std::time::{SystemTime, UNIX_EPOCH};
fn main() { let now = SystemTime::now(); let since_1970 = now .duration_since(UNIX_EPOCH) .expect("the clock is set before 1970"); println!("Seconds since 1 Jan 1970: {}", since_1970.as_secs());}The number depends on when you run it, so again there is no output shown. Why does duration_since return a Result? Because the system clock can be wrong or can be moved backwards. That’s also why you should use Instant, not SystemTime, to time your code.
That’s about all std::time does. It has no idea what “March” or “Friday” means. For that, you need chrono.
Adding chrono
Section titled “Adding chrono”Add the crate to your project (see Cargo & Crates if this is new):
cargo add chrono[dependencies]chrono = "0.4"The Rust Playground has chrono too, so every button below works. chrono has two families of types:
- Naive types (
NaiveDate,NaiveTime,NaiveDateTime) are dates and times with no time zone. “14 March 2025” or “09:30” written on a calendar. Most everyday code uses these. DateTime<Tz>is a moment in time with a time zone, likeDateTime<Utc>orDateTime<Local>.
Making dates from fixed values
Section titled “Making dates from fixed values”NaiveDate::from_ymd_opt(year, month, day) builds a date. It returns an Option, because not every combination is a real date:
use chrono::{Datelike, NaiveDate, NaiveTime};
fn main() { let date = NaiveDate::from_ymd_opt(2025, 3, 14).unwrap(); println!("{date}"); println!("year {}, month {}, day {}", date.year(), date.month(), date.day());
let time = NaiveTime::from_hms_opt(9, 30, 0).unwrap(); let meeting = date.and_time(time); println!("{meeting}");
let other = date.and_hms_opt(17, 45, 10).unwrap(); println!("{other}");
let impossible = NaiveDate::from_ymd_opt(2025, 2, 30); println!("{:?}", impossible);}Output:
2025-03-14year 2025, month 3, day 142025-03-14 09:30:002025-03-14 17:45:10NoneFebruary 30th doesn’t exist, so you get None instead of a broken date. With fixed values that you typed yourself, .unwrap() is fine. With values from a user, handle the None.
The use chrono::Datelike line brings in the methods .year(), .month(), and .day(). (Timelike does the same for .hour() and .minute().)
The current date and time
Section titled “The current date and time”Utc::now() gives the current moment in UTC. Local::now() gives it in the computer’s own time zone:
use chrono::{Local, Utc};
fn main() { let utc_now = Utc::now(); let local_now = Local::now();
println!("UTC: {}", utc_now); println!("Local: {}", local_now); println!("Today: {}", local_now.format("%Y-%m-%d"));
let today = local_now.date_naive(); println!("As a NaiveDate: {today}");}The output is today’s date, so it changes every day. The Playground’s servers use UTC, so both lines will show the same time there. .date_naive() drops the time zone and the time, leaving a plain NaiveDate. A good habit: get “now” once at the edge of your program, then pass the date into your functions. That keeps them easy to test, as you’ll see in the deadline example.
Formatting dates
Section titled “Formatting dates”.format() takes a pattern. Each % code is replaced by part of the date. %Y is the year, %m the month number, %d the day:
use chrono::NaiveDate;
fn main() { let date = NaiveDate::from_ymd_opt(2025, 3, 14).unwrap(); let dt = date.and_hms_opt(9, 5, 0).unwrap();
println!("{}", date.format("%Y-%m-%d")); println!("{}", date.format("%d/%m/%Y")); println!("{}", date.format("%A, %B %-d, %Y")); println!("{}", date.format("%a %d %b")); println!("{}", dt.format("%H:%M")); println!("{}", dt.format("%-I:%M %p"));
let file_name = date.format("report-%Y%m%d.txt").to_string(); println!("{file_name}");}Output:
2025-03-1414/03/2025Friday, March 14, 2025Fri 14 Mar09:059:05 AMreport-20250314.txtThe codes you’ll use most: %Y year, %m month (01–12), %d day (01–31), %H hour (00–23), %M minute, %S second, %A weekday name, %B month name. A - after the % removes the leading zero, so %-d prints 5 instead of 05. .format() returns something you can print. Call .to_string() when you need a String.
Parsing text into dates
Section titled “Parsing text into dates”NaiveDate::parse_from_str does the reverse: it reads text using the same % pattern. It returns a Result, because the text might not match:
use chrono::{NaiveDate, NaiveDateTime};
fn main() { let a = NaiveDate::parse_from_str("2025-03-14", "%Y-%m-%d"); println!("{:?}", a);
let b = NaiveDate::parse_from_str("14/03/2025", "%d/%m/%Y"); println!("{:?}", b);
let c: Result<NaiveDate, _> = "2025-03-14".parse(); println!("{:?}", c);
let d = NaiveDateTime::parse_from_str("2025-03-14 09:30", "%Y-%m-%d %H:%M"); println!("{:?}", d);
for text in ["2025-13-01", "March 14", "2025-03"] { match NaiveDate::parse_from_str(text, "%Y-%m-%d") { Ok(date) => println!("Parsed: {date}"), Err(e) => println!("Could not parse {text:?}: {e}"), } }}Output:
Ok(2025-03-14)Ok(2025-03-14)Ok(2025-03-14)Ok(2025-03-14T09:30:00)Could not parse "2025-13-01": input is out of rangeCould not parse "March 14": input contains invalid charactersCould not parse "2025-03": premature end of inputFor the standard YYYY-MM-DD form you can also just call .parse(). Month 13 doesn’t exist, so the first bad example is “out of range”. The error messages are short but clear enough to show to a user.
Date arithmetic
Section titled “Date arithmetic”Adding days to a date by hand is surprisingly hard: months have different lengths, and leap years exist. chrono does it for you. Use checked_add_days with Days, checked_add_months with Months, or add a TimeDelta:
use chrono::{Days, Months, NaiveDate, TimeDelta};
fn main() { let start = NaiveDate::from_ymd_opt(2025, 1, 31).unwrap();
let plus_30 = start.checked_add_days(Days::new(30)).unwrap(); println!("30 days later: {plus_30}");
let minus_7 = start.checked_sub_days(Days::new(7)).unwrap(); println!("7 days earlier: {minus_7}");
let next_month = start.checked_add_months(Months::new(1)).unwrap(); println!("1 month later: {next_month}");
let week_later = start + TimeDelta::weeks(1); println!("1 week later: {week_later}");
let dt = start.and_hms_opt(23, 30, 0).unwrap(); let later = dt + TimeDelta::hours(2); println!("2 hours later: {later}");}Output:
30 days later: 2025-03-027 days earlier: 2025-01-241 month later: 2025-02-281 week later: 2025-02-072 hours later: 2025-02-01 01:30:00Look at “1 month later”: 31 January plus one month is 28 February, because there is no 31 February. chrono picks the last valid day. The checked_ methods return an Option, which is None only if the result falls outside chrono’s (very large) range of years. Adding 2 hours to 23:30 correctly moves to the next day.
Weekdays and the difference between two dates
Section titled “Weekdays and the difference between two dates”.weekday() (from Datelike) tells you the day of the week. Subtracting one date from another gives a TimeDelta, and .num_days() turns it into a number:
use chrono::{Datelike, NaiveDate, Weekday};
fn main() { let date = NaiveDate::from_ymd_opt(2025, 3, 15).unwrap(); let day = date.weekday(); println!("{date} is a {day} ({})", date.format("%A"));
let weekend = matches!(day, Weekday::Sat | Weekday::Sun); println!("weekend? {weekend}");
let start = NaiveDate::from_ymd_opt(2025, 1, 1).unwrap(); let end = NaiveDate::from_ymd_opt(2025, 12, 25).unwrap(); let gap = end - start; println!("{} days, or {} full weeks", gap.num_days(), gap.num_weeks()); println!("backwards: {} days", (start - end).num_days());
let check_in = start.and_hms_opt(14, 0, 0).unwrap(); let check_out = NaiveDate::from_ymd_opt(2025, 1, 4).unwrap().and_hms_opt(11, 0, 0).unwrap(); println!("hotel stay: {} hours", (check_out - check_in).num_hours());}Output:
2025-03-15 is a Sat (Saturday)weekend? true358 days, or 51 full weeksbackwards: -358 dayshotel stay: 69 hoursWeekday is an enum with variants Mon to Sun, so you can match on it. Notice that the difference can be negative if the second date is later. That’s useful: a negative number of days left means “overdue”.
Example: days until a deadline
Section titled “Example: days until a deadline”Let’s put it together. The function takes “today” as a parameter instead of calling Local::now() inside. That way the output below is always the same, and the function is easy to test. In a real program you would pass in Local::now().date_naive().
use chrono::NaiveDate;
fn days_left(today: NaiveDate, deadline: NaiveDate) -> String { let days = (deadline - today).num_days(); match days { 0 => String::from("due today"), 1 => String::from("due tomorrow"), d if d > 1 => format!("{d} days left"), -1 => String::from("1 day overdue"), d => format!("{} days overdue", -d), }}
fn main() { let today = NaiveDate::from_ymd_opt(2025, 6, 10).unwrap();
let tasks = [ ("Pay rent", "2025-06-10"), ("Send report", "2025-06-11"), ("Renew passport", "2025-07-01"), ("Return library book", "2025-06-09"), ("Dentist bill", "2025-06-03"), ];
println!("Today is {}", today.format("%A, %-d %B %Y")); for (name, date_text) in tasks { let deadline = NaiveDate::parse_from_str(date_text, "%Y-%m-%d").unwrap(); println!("{:<20} {} {}", name, deadline.format("%d %b"), days_left(today, deadline)); }}Output:
Today is Tuesday, 10 June 2025Pay rent 10 Jun due todaySend report 11 Jun due tomorrowRenew passport 01 Jul 21 days leftReturn library book 09 Jun 1 day overdueDentist bill 03 Jun 7 days overdueTry changing today to a different date and run it again. The {:<20} pads the name to 20 characters so the columns line up (see Output).
chrono isn’t the only choice. The time crate is a smaller, popular alternative, and the newer jiff crate puts extra care into time zones and daylight saving time; chrono is used here because it is the most widely used and it’s in the Playground.
Common mistakes
Section titled “Common mistakes”Forgetting use chrono::Datelike. Without it, .year(), .month(), and .weekday() don’t work. The error says the method is “private”, which is confusing, but the help line tells you the real fix:
error[E0624]: method `year` is private --> src/main.rs:5:25 |5 | println!("{}", date.year()); | ^^^^ private method | = help: items from traits can only be used if the trait is in scopehelp: trait `Datelike` which provides `year` is implemented but not in scope; perhaps you want to import it |1 + use chrono::Datelike; |Mixing up %m and %M. %m is the month, %M is the minute. With a date and time, the wrong one doesn’t give an error. It quietly prints the minutes:
use chrono::NaiveDate;
fn main() { let meeting = NaiveDate::from_ymd_opt(2025, 3, 14).unwrap().and_hms_opt(9, 5, 0).unwrap(); println!("{}", meeting.format("%Y-%M-%d")); // wrong: minutes println!("{}", meeting.format("%Y-%m-%d")); // right: month}Output:
2025-05-142025-03-14Timing code with SystemTime. The system clock can jump (for example when it syncs with the internet). Use Instant for measuring, and SystemTime or chrono only when you need the actual date.
Calling Local::now() deep inside your logic. It makes results change every day and tests hard to write. Pass the date in as a parameter, like days_left does.
std::time::Instantmeasures elapsed time;Durationis a length of time;SystemTimeis the wall clock (good for Unix timestamps).- For calendars, add
chrono. UseNaiveDate/NaiveDateTimefor dates without a time zone, andUtc::now()/Local::now()for the current moment. from_ymd_optreturns anOption, andparse_from_strreturns aResult, because not every input is a real date..format("%Y-%m-%d")turns a date into text using%codes. Remember:%mis month,%Mis minute.- Add time with
checked_add_days,checked_add_months, or+ TimeDelta. Subtract two dates to get aTimeDelta, then call.num_days(). - Pass “today” into your functions instead of reading the clock inside them.
Quick check
Section titled “Quick check”Quick check
1. Which type should you use to measure how long a piece of code takes?
2. What does NaiveDate::from_ymd_opt(2025, 2, 30) return?
3. In a chrono format string, what does %M mean?
4. You subtract two NaiveDates: deadline - today. How do you get the number of days?
5. What is 31 January 2025 plus Months::new(1) in chrono?
Score: 0 / 5
Read more
Section titled “Read more”- std::time — Rust standard library —
Instant,Duration, andSystemTimewith more examples. - chrono documentation — docs.rs — the full guide to every chrono type and method.
- chrono format codes — docs.rs — the complete list of
%codes forformatandparse_from_str. - Date and Time — Rust Cookbook — short recipes for common date and time tasks.
- jiff documentation — docs.rs — a newer date and time crate with strong time zone support, if you want to compare.