Skip to content

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.

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));
}
▶ Try it Yourself

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.

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.5s
90s
1500 ms
1.5 s
total: 91.5s
1 min 30 s
true
▶ Try it Yourself

You’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.

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());
}
▶ Try it Yourself

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.

Add the crate to your project (see Cargo & Crates if this is new):

Terminal window
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, like DateTime<Utc> or DateTime<Local>.

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-14
year 2025, month 3, day 14
2025-03-14 09:30:00
2025-03-14 17:45:10
None
▶ Try it Yourself

February 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().)

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}");
}
▶ Try it Yourself

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.

.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-14
14/03/2025
Friday, March 14, 2025
Fri 14 Mar
09:05
9:05 AM
report-20250314.txt
▶ Try it Yourself

The 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.

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 range
Could not parse "March 14": input contains invalid characters
Could not parse "2025-03": premature end of input
▶ Try it Yourself

For 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.

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-02
7 days earlier: 2025-01-24
1 month later: 2025-02-28
1 week later: 2025-02-07
2 hours later: 2025-02-01 01:30:00
▶ Try it Yourself

Look 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? true
358 days, or 51 full weeks
backwards: -358 days
hotel stay: 69 hours
▶ Try it Yourself

Weekday 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”.

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 2025
Pay rent 10 Jun due today
Send report 11 Jun due tomorrow
Renew passport 01 Jul 21 days left
Return library book 09 Jun 1 day overdue
Dentist bill 03 Jun 7 days overdue
▶ Try it Yourself

Try 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.

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 scope
help: 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-14
2025-03-14
▶ Try it Yourself

Timing 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::Instant measures elapsed time; Duration is a length of time; SystemTime is the wall clock (good for Unix timestamps).
  • For calendars, add chrono. Use NaiveDate / NaiveDateTime for dates without a time zone, and Utc::now() / Local::now() for the current moment.
  • from_ymd_opt returns an Option, and parse_from_str returns a Result, because not every input is a real date.
  • .format("%Y-%m-%d") turns a date into text using % codes. Remember: %m is month, %M is minute.
  • Add time with checked_add_days, checked_add_months, or + TimeDelta. Subtract two dates to get a TimeDelta, then call .num_days().
  • Pass “today” into your functions instead of reading the clock inside them.

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