How can I remove 3 years from a date in Rust?

89 Views Asked by At

Now I'm using

use chrono::{DateTime, Utc};

pub fn today_three_years_earlier() -> DateTime<Utc> {
    let now = Utc::now();
    
    // this is February 29 and it doesn't work of course
    // let now = DateTime::from_timestamp(1_709_228_325, 0).unwrap();
    
    let from = now.with_year(now.year() - 3).unwrap();

    from
}

but if now (today) is February 29 it doesn't work of course because I'm simply removing 3 years from today's year.

How can I subtract exactly 3 years from now's date?

It would also be enough for me to remove 365 days * 3 years (without considering leap years).

REPRODUCTION: https://www.rustexplorer.com/b/wkqutm

2

There are 2 best solutions below

0
Schwern On BEST ANSWER

tl;dr: You could subtract 36 Months instead. The result will be Feb 28th.

The result will be clamped to valid days in the resulting month, see checked_add_months for details.

now - Months::new(3 * 12);

However, this has the result of mapping two days (Feb 28th and Feb 29th) to one day (Feb 28th). Depending on your application this might have negative consequences, like scheduling software piling two days worth of work into one day. In that case you probably want to catch the None result and ask the user what to do (see @cdhowie's answer).

In general, you must consider that calendar calculations might result in a date which does not exist.


Calendars are hard. Our calendar is full of exceptions, gaps, and cycles which do not line up, but we try to force it anyway.

"What is Feb 29th of last year?" is unanswerable. A human might arbitrarily decide to say Feb 28th or March 1st, but Rust won't decide this for you, it returns None to inform you it doesn't exist. This is documented in with_year.

Returns None if: The resulting date does not exist (February 29 in a non-leap year).

There is no way around this. You're trying to map 366 days into 365; it cannot neatly map 1-to-1.

Other date changes have similar problems. If you change Dec 31st to April. If you ask 2:30am on Sunday, March 10, 2024 in most of the United States (2am to 3am doesn't exist because of daylight savings). If you ask UTC for Sunday, November 3, 2024, 1:30:00 am in the US (because of daylight savings 1:30am happens twice). Etc.

Subtracting (or adding) 12 months works because the maintainers of chrono have decided that if you ask to subtract 12 months in February you want a date in February.

The result will be clamped to valid days in the resulting month, see DateTime::checked_sub_months for details.

0
cdhowie On

The documentation for with_year clearly states that this operation can return None when "the resulting date does not exist (February 29 in a non-leap year)." There are several other cases where None is returned as well.

Quite simply, you've asked for a date that isn't on the calendar, and there is no single way to respond to this request that is more correct than any other. For example, you could argue that the function should just use March 1st instead, but there is no particular reason to choose March 1st over February 28th.

If you want March 1st then you could simply retry the operation with now advanced by one day in the case that the date is February 29th:

pub fn today_three_years_earlier() -> DateTime<Utc> {
    let now = DateTime::from_timestamp(1_709_228_325, 0).unwrap();

    let from = now
        .with_year(now.year() - 3)
        .unwrap_or_else(|| match (now.month(), now.day()) {
            (2, 29) => (now + Days::new(1)).with_year(now.year() - 3).unwrap(),
            _ => panic!("Cannot subtract 3 years from {now}"),
        });

    from
}

(Playground)

If you want February 28th then just subtract Days::new(1) instead of adding it.