Four Thousand Weeks in Rust
Four Thousand Weeks is a book written by Oliver Burkeman on the topic of time management. The premise is that the average human lifespan is about 4,000 weeks – so let’s make the most of our time.
“Missing out… isnβt actually a problem… because ‘missing out’ is what makes our choices meaningful.”
– Four Thousand Weeks, Oliver Burkeman
After listening to the audio book in 2022, I decided to build a week calculator. Its entire purpose is to remind me of how old I am. π
The output looks like this:
The current time is Tuesday, April 15, 2025 at 10:12 AM (-06:00).
Nathan was born on Tuesday, April 5, 1977 at 11:58 AM (-08:00).
He has been alive for 2505 weeks, 6 days, 20 hours and 14 minutes.
The Code
This is my first Rust program so I kept it simple. The entire program is 70 lines, including tests. It depends on the Chrono crate (a Rust library) for parsing and formatting dates.
Starting from the top, the use keyword imports the chrono::prelude
, which allows the DateTime
struct and other types to be used without prefixing them with chrono::
.
// Calculate my age in weeks
// Inspired by Four Thousand Weeks by Oliver Burkeman.
use chrono::prelude::*;
Then I define constants for my name and birthdate, as well as formats for parsing and displaying times. Time zone abbreviations may be ambiguous, such as CST
.1 That’s why I use offsets like -08:00 instead.
const NAME: &str = "Nathan";
const PRONOUN: &str = "He";
// NOTE: -08:00 is PST. Daylight Saving Time started in B.C. on Sunday, April 24, 1977.
const BIRTHDATE: &str = "1977-04-05 11:58 -08:00";
const PARSE_FORMAT: &str = "%Y-%m-%d %H:%M %:z";
const TIME_FORMAT: &str = "%A, %B %-d, %Y at %-I:%M %p (%:z)";
The main function calculates my age in weeks and uses the println!
macro to output the result. The age
function returns a tuple that is assigned to the variables weeks
, days
, hours
, and minutes
.
fn main() {
let now = now();
let birthdate = parse_date_time(BIRTHDATE);
let (weeks, days, hours, minutes) = age(birthdate, now);
println!("The current time is {}.\n", now.format(TIME_FORMAT));
println!("{} was born on {}.", NAME, birthdate.format(TIME_FORMAT));
println!(
"{} has been alive for {} weeks, {} days, {} hours and {} minutes.",
PRONOUN, weeks, days, hours, minutes
);
}
The now
and parse_date_time
functions are small wrappers around Chrono. The Local::now()
function returns a DateTime<Local>
, but I convert it to a FixedOffset to make testing easier. More on that later.
fn now() -> DateTime<FixedOffset> {
Local::now().fixed_offset()
}
fn parse_date_time(s: &str) -> DateTime<FixedOffset> {
DateTime::parse_from_str(s, PARSE_FORMAT).unwrap()
}
Note: unwrap
will cause a panic if the date cannot be parsed, which is fine for a small script like this.
The real work is in the age
function, which calculates the difference between two dates and returns the number of weeks, days, hours and minutes.
fn age(birthdate: DateTime<FixedOffset>, now: DateTime<FixedOffset>) -> (i64, i64, i64, i64) {
let local_birthdate = birthdate.with_timezone(&now.timezone());
let duration = now - local_birthdate;
let weeks = duration.num_weeks();
let days = duration.num_days() - (duration.num_weeks() * 7);
let hours = duration.num_hours() - (duration.num_days() * 24);
let minutes = duration.num_minutes() - (duration.num_hours() * 60);
(weeks, days, hours, minutes)
}
The tests are written in the same file, but they are only compiled into the binary when running cargo test
, thanks to the #[cfg(test)] attribute. With use super::*
, the tests module can access functions in the parent module.
#[cfg(test)]
mod tests {
use super::*;
// ... tests go here
}
GitHub Copilot wrote this test for me. The test failed initially, but rather than alter the test, I switched the timezone format to one that Chrono can both parse and format. The #[test]
attribute indicates that this function is a test.
#[test]
fn test_parse_date_time() {
let s = "2024-09-18 19:27 -06:00";
let dt = parse_date_time(s);
assert_eq!(dt.format(PARSE_FORMAT).to_string(), s);
}
To test the age function, I parse two dates and assert the result. I’ve verified the result against an earlier Go implementation and an online calculator.
#[test]
fn test_age() {
// -06:00 is MDT.
let now = parse_date_time("2024-09-18 19:27 -06:00");
let birthdate = parse_date_time("1977-04-05 11:58 -08:00");
let (weeks, days, hours, minutes) = age(birthdate, now);
assert_eq!(weeks, 2476);
assert_eq!(days, 1);
assert_eq!(hours, 5);
assert_eq!(minutes, 29);
}
In my first Rust implementation, the age calculation expected the current time as a DateTime<Local>
, which is what Local::now()
returns. That doesn’t work very well for testing though, as I needed a specific time in a specific timezone to stand-in for “now”. I was completely stumped and just went without tests for quite some time! π
Three months later, the fixed_offset()
method was added to Chrono, which makes it trivial to test with a specific time for “now”.
And that’s the entire program. The full source code is available on GitHub.