Is there an API in the standard library to find the period between two dates

Hi,

I was going through datetime - How to get the difference between two chrono dates? - Stack Overflow and it is absolutely correct up to the following line:

let days = diff.num_days();

but after this line, the author has assumed a year of 365 days and a month of 30 days which is not correct because the months are not of equal length and a year may be a leap year as well.

The standard library in Java has a class, Period, using which one can find the period between two dates as follows:

System.out.println(Period.between(LocalDate.of(1970, 12, 23), LocalDate.now()));

and it outputs P50Y10M23D which is 50 years 10 months and 23 days.

Is there any such API in Rust?

1 Like

I am sorry, but this reply wlll not answer your question directly. The reason for that is, that as I see it, this question cannot be answered in an unambiguous manner. Or it can, but you will have to make other compromises.

What is the difference between August 30 and October 1? 1 month and a day? But what is then the difference between August 31 and October 1? Or July 31 and September 1?

2 Likes

Thanks for your response. Date-Time is a complex topic and your questions are valid. When it comes to doing something we have to make some assumptions and there should be some API based on assumptions. At the moment, I am clueless about how to find the years, months and days between two dates in Rust.

Rust has Duration which is equivalent of Duration in Java but I have not been able to find an equivalent of Period.

Just to illustrate, yes, these Periods are all the same in Java: Scastie - An interactive playground for Scala.

1 Like

Yes, but not necessarily in the standard library. As there's no obviously correct way to calculate this, it feels more suited to a third-party crate.

3 Likes

Since Rust has a Duration in the standard library, it makes sense to have a Period as well in the standard library but it is not so important. What is important is there should be something like Period to help create applications that need to display years, months, days as a difference between two dates.

Perhaps

But not that, because the result is badly defined. Even when you make the assumptions that Java's Period class does, you can only use the resulting period to calculate an end date when you have a given start date. Including the year. You cannot even calculate a start date back from an end date and a Period, see @mpol 's example.

Given this nebulous definition of what "months" mean, this is definitely not something that should be included in the standard library. Perhaps it would be useful to create a third-party create that can do this so one can please their manager when generating a report, but it would not be very useful in any kind calculation on dates.

1 Like

Here's an example of how one could approach it:

use chrono::naive::NaiveDate as ND;

/// Return whether year `y` in the Gregorian calendar is a leap year
fn is_leap_year(y: u32) -> bool
{
    y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)
}

/// Return the number of days in the month `m` in year `y` in the Gregorian calendar. Note that
/// The month number is zero based, i.e. `m=0` corresponds to January, `m=1` to February, etc.
fn days_per_month(y: u32, m: u32) -> u32
{
    const DAYS_PER_MONTH: [u32; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
    let nd = DAYS_PER_MONTH[m as usize];
    nd + (m == 1 && is_leap_year(y)) as u32
}

/// Return the number of years, months and days, as well as the sign of the time difference,
/// between dates `t0` and `t1`. When `t1` is before t0, the last value in the return tuple
/// is `true`, otherwise `false`. This function aims to mimic the behaviour of Java's Period
/// class, which saturates the day number at the end of the month (i.e., both the difference
/// between Aug 30 and Oct 1, and Aug 31 and Oct 1, return 1 month plus 1 day).
fn datediff<T>(date0: T, date1: T) -> (u32, u32, u32, bool)
where T: chrono::Datelike + PartialOrd
{

    if date1 < date0
    {
        let (ny, nm, nd, _) = datediff(date1, date0);
        (ny, nm, nd, true)
    }
    else
    {
        let (y0, m0, mut d0) = (date0.year() as u32, date0.month0(), date0.day0());
        let (mut y1, mut m1, mut d1) = (date1.year() as u32, date1.month0(), date1.day0());

        if d0 > d1
        {
            let (py1, pm1) = if m1 == 0 { (y1-1, 11) } else { (y1, m1-1) };
            let pnd = days_per_month(py1, pm1);
            d0 = d0.min(pnd-1);
            if d0 > d1
            {
                y1 = py1;
                m1 = pm1;
                d1 += pnd;
            }
        }
        if m0 > m1
        {
            y1 -= 1;
            m1 += 12;
        }

        (y1 - y0, m1 - m0, d1 - d0, false)
    }
}

fn main()
{
    // Simple difference: 1 month
    println!("{:?}", datediff(ND::from_ymd(2021, 1, 1), ND::from_ymd(2021, 2, 1)));
    // Day number of end greater than that of start: 1 month, 14 days
    println!("{:?}", datediff(ND::from_ymd(2021, 1, 1), ND::from_ymd(2021, 2, 15)));
    // Day number of end less than that of start: 17 days
    println!("{:?}", datediff(ND::from_ymd(2021, 1, 15), ND::from_ymd(2021, 2, 1)));
    // Same as last, but add extra month: 1 month, 14 days
    println!("{:?}", datediff(ND::from_ymd(2021, 1, 15), ND::from_ymd(2021, 3, 1)));
    // Same in a leap year: 1 month, 15 days
    println!("{:?}", datediff(ND::from_ymd(2020, 1, 15), ND::from_ymd(2020, 3, 1)));
    // Simple difference, different years: 5 years, 2 months
    println!("{:?}", datediff(ND::from_ymd(2020, 9, 1), ND::from_ymd(2025, 11, 1)));
    // Different years, but end month less than start month: 4 years, 10 months
    println!("{:?}", datediff(ND::from_ymd(2020, 11, 1), ND::from_ymd(2025, 9, 1)));
    // Different years, but end month and day less than start: 4 years, 9 months, 17 days
    println!("{:?}", datediff(ND::from_ymd(2020, 11, 15), ND::from_ymd(2025, 9, 1)));
    // Same as last, dates reversed: last value in result is `false`
    println!("{:?}", datediff(ND::from_ymd(2025, 9, 1), ND::from_ymd(2020, 11, 15)));

    // Examples from post.
    // Returns 1 month, 1 day
    println!("{:?}", datediff(ND::from_ymd(2021, 8, 30), ND::from_ymd(2021, 10, 1)));
    // Returns 1 month, 1 days
    println!("{:?}", datediff(ND::from_ymd(2021, 8, 31), ND::from_ymd(2021, 10, 1)));
    // Returns 1 month, 1 day
    println!("{:?}", datediff(ND::from_ymd(2021, 7, 31), ND::from_ymd(2021, 9, 1)));

    // Also works around February: 1 month 1 day
    println!("{:?}", datediff(ND::from_ymd(2021, 1, 31), ND::from_ymd(2021, 3, 1)));
}

This is only lightly tested, and only works for positive years in a Gregorian calendar. Feel free to use it and/or extend it if you want. Personally I think Java's behaviour in this case is bat shit crazy, so I am not taking this any further.

FWIW, this behaviour is also different from GNU date:

> LC_ALL=C date -d "2021-08-30 +1month"
Thu Sep 30 00:00:00 CEST 2021
> LC_ALL=C date -d "2021-08-30 +1month +1day"
Fri Oct  1 00:00:00 CEST 2021
> LC_ALL=C date -d "2021-08-31 +1month"
Fri Oct  1 00:00:00 CEST 2021
> LC_ALL=C date -d "2021-08-31 +1month +1day"
Sat Oct  2 00:00:00 CEST 2021
2 Likes

My gut feeling is that nothing with such ill defined and ambiguous results should be included in std.

If there were some recognised and widely used standard for such a thing then maybe. It seems there is not.

3 Likes

Not in std, because calendars are evil.

You'll want to check out crates, perhaps one of these: #date // Lib.rs

3 Likes

Speaking as the maintainer of the time crate, I intend on implementing a Period struct at some point. As far as I know, no one has done this in Rust yet.

4 Likes

I hope it will be an Interval struct to measure time offset between two events. A Period struct needs to specify a repetition frequency or interval, since "period" derives from "periodic".

Pretty sure it's the other way around. Also, a single cycle of a recurring event is only one meaning of the "period"; it is certainly valid to use the word to describe a time interval.

From my background in the finance world, where time periods play an import roles (e.g. for calculating interest on loans), I can tell that this topic is indeed quite complex.

In finance, a time period (e.g. 3 months) makes only sense together with the definition of the day count method used (of which there are many). These day count conventions define precisely how to deal with leap days, end-of-months rules taking the varying months length into account, bank holidays, etc.

Even with specifying a day count convention, calculating the time period given two dates and a day count conventions is in some cases ambiguous. This is probably due the fact that this is rarely needed, in contrast to the inverse direction (given a date and a time period, find the end date).

If you are interested, some of these conventions and features are implemented in a hobby project of mine: finql

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.