Flecks of Rust #13: Environment variables and dates in Rust

Working with environment variables in Rust is easy using the std::env module.

use std::env;

fn main() {
    let key = "HOME";
    let value = env::var(key);
    if !value.is_ok() {
        eprintln!("{} not found", key);
        return;
    }

    let value = value.unwrap();  // we know it's there
    println!("The value of {} is {}", key, value);
}

If you create a new project using Cargo, and replace the default source code in main.rs with the above, you should be able to run the program and see something like this:

% cargo run --quiet envvar
The value of HOME is /Users/me

Let's define an environment variable that holds a date in standard format, and work with it.

Handling dates with the Chrono crate

The de facto date and time library in Rust is Chrono. It handles just about anything that you might reasonably want, from storing dates, times or full-blown timestamps, in local time or with a timezone. It also handles formatting and parsing.

Let's make a Rust program that can show the user how old they are in days, and congratulate them if it happens to be their birthday.

Create a new app with cargo new birthday. Add the Chrono crate to the Cargo.toml file:

[dependencies]
chrono = "0.4.38"

Here is the full text of main.rs, with some explanation to follow:

use std::env;
use chrono::{NaiveDate, Local, Datelike};

fn main() {
    let key = "BIRTHDATE";
    let value = env::var(key);
    // If the environment variable was not found,
    // we exit early to avoid excessive nesting.
    if !value.is_ok() {
        return;
    }

    let value = value.unwrap();  // we know it's there

    // Try to parse the BIRTHDATE variable as an ISO 8601 date
    // into a NaiveDate. If that succeeds, compare the month and
    // day components of the birthdate and the NaiveDate of today.
    // If they match, congratulate the user.
    match NaiveDate::parse_from_str(&value, "%F") {
        Ok(birthdate) => {
            let today: NaiveDate = Local::now().date_naive();
            if birthdate.month() == today.month() && birthdate.day() == today.day() {
                print!("Happy birthday! ");
            }

            // Compute the difference between the birthdate and today.
            // If the result is positive, show the user's age in days
            // (if zero, don't bother). If the result is negative,
            // be amazed.
            let diff = today.signed_duration_since(birthdate);
            let day_count = diff.num_days();
            if day_count >= 0 {
                if day_count != 0 {
                    print!("You are {} days old.", day_count);
                    if day_count % 1000 == 0 {
                        print!(" That's a nice round number!");
                    }
                }
            } else {
                print!("Looks like you're not born yet!");
            }
            println!();
        },
        Err(_) => {
            eprintln!("Error in the '{}' environment variable: \
                '{}' is not a valid date.", key, value);
        }
    }
}

If you run the program with Cargo, you will not see any output, because the BIRTHDATE environment is not set. So, first set its value to a valid ISO 8601 date, and then run the program again. You should see something like this:

% BIRTHDATE=1989-11-30 cargo run --quiet
You are 12735 days old.

The program was run on 2024-10-12. When you set the BIRTHDATE environment variable to a date where the month and day match those of today, you get a congratulation:

% BIRTHDATE=1989-10-12 cargo run --quiet
Happy birthday! You are 12784 days old.

There are some checks for corner cases. For example, a bogus BIRTHDATE environment variable value means that NaiveDate::parse_from_str fails:

% BIRTHDATE=YYYY-MM-DD cargo run --quiet
Error in the 'BIRTHDATE' environment variable: 'YYYY-MM-DD' is not a valid date.

If the value of the BIRTHDATE environment variable is a valid date but it has not passed yet, it will be questioned:

% BIRTHDATE=2038-01-19 cargo run --quiet
Looks like you're not born yet!

Finally, if it is your birthday but you are a newborn baby, you will be congratulated, but not reminded that you are zero days old:

% BIRTHDATE=2024-10-12 cargo run --quiet
Happy birthday!

This logic uses the signed_duration_since function of NaiveDate, which returns a TimeDelta value that you can convert into days using its num_days function. If the user's age in days is an even thousand, it will be noted.

If you want a constant reminder of the passing of time, and handy reminders to celebrate your thousands of days, then build the app, put it somewhere in your path, and run it in your login shell. Be sure to have a look at the Chrono crate documentation to see what other goodies can be found there.