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