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

  1. Print the date for four timestamps a year apart and check the leap day.
  2. Turn the day-of-year into a date by walking the month lengths.
  3. 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.

  1. How many seconds are in a day?

    Answer

    86 400

    Twenty-four hours of sixty minutes of sixty seconds.

  2. Why is div_euclid used 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.

  3. Which weekday was 1 January 1970?

    Answer

    Thursday

    The Unix epoch is a Thursday, which is the zero of the weekday table.

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

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

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

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

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

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

  10. 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 day in this scope

    The 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