Dates and formatting
The standard library gives a moment in time and a duration, and no calendar. A date is arithmetic on the number of days since 1970, and the arithmetic is short enough to write by hand: that is the point of this lesson. The chrono crate does the same work for time zones, parsing and formatting, and it is named at the end for the parts a manual version does not cover.
This lesson turns a timestamp into a year, a month and a day, finds the weekday, writes the date as text, handles February, and ends with the compiler refusing a signed number where a duration wants an unsigned one.
From seconds to a date
Whole days are seconds divided by 86 400, and the remainder is the time of day. The day count is turned into a date with the standard era algorithm: 400 years is 146 097 days, and the position inside the era gives the year, the month and the day.
The program converts the timestamp 1 700 000 000, which is a fixed moment in 2023, and prints the date in the ISO order.
fn civil_from_days(days: i64) -> (i64, u32, u32) {
let z = days + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = (z - era * 146_097) as u64;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let year = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let day = (doy - (153 * mp + 2) / 5 + 1) as u32;
let month = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
(if month <= 2 { year + 1 } else { year }, month, day)
}
fn main() {
let timestamp: i64 = 1_700_000_000;
let days = timestamp.div_euclid(86_400);
let (year, month, day) = civil_from_days(days);
println!("{year:04}-{month:02}-{day:02}");
}
rustc --edition 2024 civil.rs
./civil
2023-11-14
The division is div_euclid rather than /, so a timestamp before 1970 rounds towards minus infinity and the remainder stays positive. That single choice is what makes the same function work for 1969. The previous lesson produces the timestamp itself.
The weekday
The first of January 1970 was a Thursday, so the day count modulo seven names the weekday without any table of month lengths. rem_euclid keeps the index positive for dates before the epoch.
The program takes the same timestamp and prints the day of the week. The array of names starts at Thursday, which is index zero.
fn main() {
let timestamp: i64 = 1_700_000_000;
let days = timestamp.div_euclid(86_400);
let names = ["Thursday", "Friday", "Saturday", "Sunday", "Monday", "Tuesday", "Wednesday"];
let index = days.rem_euclid(7) as usize;
println!("{}", names[index]);
}
rustc --edition 2024 weekday.rs
./weekday
Tuesday
The index is a cast from a non-negative i64, which is why the array indexing cannot go out of bounds. The same trick gives the day of the year once the date is known, and a calendar view is a loop over it.
A date as text
Formatting is the part a program usually gets from a library, and the manual version is a table and a format!. The width and the zero fill come from the format specification of the formatting lesson.
The program prints the date in a written form and the time of day for the same timestamp. The seconds modulo a day give hours, minutes and seconds without a library.
fn main() {
let timestamp: i64 = 1_700_000_000;
let seconds = timestamp.rem_euclid(86_400);
let months = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December",
];
let month = months[10];
let day = 14;
let year = 2023;
println!("{day} {month} {year}");
println!("{:02}:{:02}:{:02}", seconds / 3600, seconds % 3600 / 60, seconds % 60);
}
rustc --edition 2024 format.rs
./format
14 November 2023
22:13:20
The date and the time are the same moment, one in the calendar and one on the clock. A real program passes the year, month and day from the conversion above instead of writing them out, and the chrono crate is the one to reach for when the format has to be parsed as well as printed, or when a time zone is involved.
A timestamp is always UTC, and a calendar date is not: the same second is a different day in Tokyo and in Lima. A program that prints a local date needs the offset as well as the timestamp, which is exactly the part chrono and its time zone database provide and this lesson leaves out.
Leap years and the length of a month
February is the only month whose length depends on the year, and the rule has three parts: divisible by four, not by a hundred unless also by four hundred. A match with a guard puts the rule in one place.
The program asks for February of 2023, 2024 and 1900. The answers are twenty-eight, twenty-nine and twenty-eight, because 1900 is divisible by a hundred and not by four hundred.
fn days_in_month(year: i64, month: u32) -> u32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) => 29,
2 => 28,
_ => 0,
}
}
fn main() {
println!("{} {} {}", days_in_month(2023, 2), days_in_month(2024, 2), days_in_month(1900, 2));
}
rustc --edition 2024 leap.rs
./leap
28 29 28
The guard on the second arm is evaluated only for February, and the plain 2 below it catches the other years. The lesson on patterns covers the guards, and the next lesson needs a date-free seed.
A signed timestamp
A timestamp is signed, because dates before 1970 are negative, and a duration is unsigned, because a length of time is never negative. Passing one to the other is a type error, and the help says how to convert with a check.
The program hands an i64 timestamp to Duration::from_secs. The compiler reports E0308 and the two types.
use std::time::Duration;
fn main() {
let timestamp: i64 = 1_700_000_000;
let duration = Duration::from_secs(timestamp);
println!("{duration:?}");
}
rustc --edition 2024 secs_err.rs
error[E0308]: mismatched types
--> secs_err.rs:5:40
|
5 | let duration = Duration::from_secs(timestamp);
| ------------------- ^^^^^^^^^ expected `u64`, found `i64`
| |
| arguments to this function are incorrect
|
note: associated function defined here
--> /rustc/31fca3adb283cc9dfd56b49cdee9a96eb9c96ffd/library/core/src/time.rs:224:17
|
help: you can convert an `i64` to a `u64` and panic if the converted value doesn't fit
|
5 | let duration = Duration::from_secs(timestamp.try_into().unwrap());
| ++++++++++++++++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0308`.
The help is the right fix for a timestamp a program has just validated, and a program that accepts dates before 1970 keeps the signed value and adds the epoch itself. The conversion in this lesson never needs a duration: the arithmetic is on whole days and their remainder.
Exercises
- Print the date for four timestamps a year apart and check the leap day.
- Turn the day-of-year into a date by walking the month lengths.
- Print the same date twice, once in ISO order and once as "day month year", and compare the two lines.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
How many seconds are in a day?
Answer
86 400
Twenty-four hours of sixty minutes of sixty seconds.
-
Why is
div_euclidused for the day count?Answer
It rounds towards minus infinity and keeps the remainder positive
A timestamp before 1970 needs the same code path, and the euclidean division gives it.
-
Which weekday was 1 January 1970?
Answer
Thursday
The Unix epoch is a Thursday, which is the zero of the weekday table.
-
Which year is a leap year?
Answer
2024, divisible by four and not by a hundred
The rule skips centuries unless they are divisible by four hundred, so 1900 and 2100 are not.
-
What does this program print?
fn main() { let timestamp: i64 = 86_400; let days = timestamp.div_euclid(86_400); let seconds = timestamp.rem_euclid(86_400); println!("{days} {seconds}"); }Answer
1 0
One whole day and no remainder seconds.
-
What does this program print?
fn main() { let timestamp: i64 = -1; let days = timestamp.div_euclid(86_400); let seconds = timestamp.rem_euclid(86_400); println!("{days} {seconds}"); }Answer
-1 86399
The euclidean division goes to the day before and leaves a positive remainder.
-
What does this program print?
fn main() { let days: i64 = 19_675; let names = ["Thursday", "Friday", "Saturday", "Sunday", "Monday", "Tuesday", "Wednesday"]; println!("{}", names[days.rem_euclid(7) as usize]); }Answer
Tuesday
The remainder of the day count names Tuesday for this date.
-
What does this program print?
fn days_in_month(year: i64, month: u32) -> u32 { match month { 2 if year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) => 29, 2 => 28, _ => 31, } } fn main() { println!("{} {}", days_in_month(2000, 2), days_in_month(2100, 2)); }Answer
29 28
Two thousand is divisible by four hundred and 2100 is not.
-
What is the first line of the compiler output for this program?
use std::time::Duration; fn main() { let timestamp: i64 = 1_700_000_000; let duration = Duration::from_secs(timestamp); println!("{duration:?}"); }Answer
error[E0308]: mismatched types
The function takes u64 and the timestamp is i64, so the types do not match.
-
What is the first line of the compiler output for this program?
fn main() { let year = 2023; let month = 11; println!("{year:04}-{month:02}"); println!("{year:04}-{month:02}-{day:02}"); }Answer
error[E0425]: cannot find value
dayin this scopeThe last format string names a variable that was never declared.
The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev