Four Thousand Weeks in Rust

Calculating my age in weeks.

First published on September 18, 2024. Last revised on September 18, 2024.

Four Thousand Weeks is a book written by Oliver Burkeman on the topic of time management. The premise of the title is that our average lifespan is around 4000 weeks, so let’s make them count.

“Missing out on something – indeed, on almost everything – is basically guaranteed. Which isn’t actually a problem anyway, it turns out, because ‘missing out’ is what makes our choices meaningful in the first place.” – 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 Wednesday, September 18, 2024 at 9:06 PM (-06:00).

Nathan was born on Tuesday, April 5, 1977 at 11:58 AM (-08:00).
He has been alive for 2476 weeks, 1 days, 7 hours and 8 minutes.

The Code

This is my first Rust program. I kept it simple, with imperative code and constants instead of config. The entire program is 70 lines of code, including tests. It depends on the Chrono crate for parsing and formatting dates.

// Calculate my age in weeks
// Inspired by Four Thousand Weeks by Oliver Burkeman.
use chrono::prelude::*;

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";

The use keyword imports the chrono::prelude, which allows the DateTime struct and other types to be used without prefixing them with chrono::.

I also define some 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 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. Local::now() 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()
}

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 the function is a test. It may seem redundant, but the tests module could also contain helper functions.

#[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 with 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);
}

My first Rust implementation of the age calculation expected the current time as a DateTime<Local>, which is what Local::now() returns. That really doesn’t work 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! There was probably a way to do it, but keep in mind that I’m still new to Rust.

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.

Nathan Youngman

Software Developer and Author