parse_datetime: Implement GNU style overflow/underflow

In GNU if using date with relative Months with the -d flag and the target Month does not have the source day (for example 31) GNU overflows the diff in days.
This commit is contained in:
cerdelen
2026-01-19 10:38:35 +01:00
committed by Sylvestre Ledru
parent 6306843888
commit 7e528ad609
2 changed files with 87 additions and 34 deletions
+12 -22
View File
@@ -280,30 +280,20 @@ impl DateTimeBuilder {
// 4d. Apply relative adjustments.
for rel in self.relative {
dt = dt.checked_add::<Span>(if let relative::Relative::Months(x) = rel {
let mut temp = dt.clone();
let mut days: i32 = 0;
if x < 0 {
for _ in 0..x.unsigned_abs() as usize {
// going backwards we first change the month to last month as last months
// days are important to calculate the day delta from now to now -1 month
temp = temp.checked_sub(1.month())?;
days += temp.date().last_of_month().day() as i32;
}
days *= -1;
} else {
for _ in 0..x {
// going forward we take the days of this month and then change the month to
// the next month as going forward the current month is important for the
// day delta from now to +1 month
days += temp.date().last_of_month().day() as i32;
temp = temp.checked_add(1.month())?;
}
let desired_day = dt.day();
dt = if let relative::Relative::Months(x) = rel {
// GNU way of calculating relative Months
// GNU changes the Month and then checks if the target Month has
// this day. If this day does not exist in the target month it overflows
// the difference
dt = dt.checked_add::<Span>(Span::new().try_months(x)?)?;
if desired_day != dt.day() {
dt = dt.checked_add((desired_day - dt.day()).days())?;
}
Span::new().try_days(days)?
dt
} else {
rel.try_into()?
})?;
dt.checked_add::<Span>(rel.try_into()?)?
};
}
// 4e. Apply final fixed offset.
+75 -12
View File
@@ -1,6 +1,8 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use core::panic;
use jiff::{civil::DateTime, tz::TimeZone, Zoned};
use parse_datetime::parse_datetime_at_date;
use rstest::rstest;
@@ -130,6 +132,8 @@ fn test_time_invalid(#[case] input: &str) {
}
#[rstest]
#[case::months_ago_0("2026-01-12 0 Months ago", "2026-01-12")]
#[case::months_ago_negative_1("2026-01-12 -1 Months ago", "2026-02-12")]
#[case::months_ago_1("2026-01-12 1 Months ago", "2025-12-12")]
#[case::months_ago_2("2026-01-12 2 Months ago", "2025-11-12")]
#[case::months_ago_3("2026-01-12 3 Months ago", "2025-10-12")]
@@ -142,6 +146,10 @@ fn test_time_invalid(#[case] input: &str) {
#[case::months_ago_10("2026-01-12 10 Months ago", "2025-03-12")]
#[case::months_ago_11("2026-01-12 11 Months ago", "2025-02-12")]
#[case::months_ago_12("2026-01-12 12 Months ago", "2025-01-12")]
#[case::months_ago_24("2026-01-12 24 Months ago", "2024-01-12")]
#[case::months_ago_36("2026-01-12 36 Months ago", "2023-01-12")]
#[case::months_ago_120("2026-01-12 120 Months ago", "2016-01-12")]
#[case::months_ago_240("2026-01-12 240 Months ago", "2006-01-12")]
fn test_time_months_ago(#[case] input: &str, #[case] expected: &str) {
let now = "2026-01-12"
.parse::<DateTime>()
@@ -152,18 +160,73 @@ fn test_time_months_ago(#[case] input: &str, #[case] expected: &str) {
}
#[rstest]
#[case::months_in_1_months("2026-01-12 1 Months", "2026-02-12")]
#[case::months_in_2_months("2026-01-12 2 Months", "2026-03-12")]
#[case::months_in_3_months("2026-01-12 3 Months", "2026-04-12")]
#[case::months_in_4_months("2026-01-12 4 Months", "2026-05-12")]
#[case::months_in_5_months("2026-01-12 5 Months", "2026-06-12")]
#[case::months_in_6_months("2026-01-12 6 Months", "2026-07-12")]
#[case::months_in_7_months("2026-01-12 7 Months", "2026-08-12")]
#[case::months_in_8_months("2026-01-12 8 Months", "2026-09-12")]
#[case::months_in_9_months("2026-01-12 9 Months", "2026-10-12")]
#[case::months_in_10_months("2026-01-12 10 Months", "2026-11-12")]
#[case::months_in_11_months("2026-01-12 11 Months", "2026-12-12")]
#[case::months_in_12_months("2026-01-12 12 Months", "2027-01-12")]
#[case::from_29_feb_in_1_month("2026-02-28 1 months", "2026-03-28")]
#[case::from_29_feb_1_month_ago("2026-02-28 1 months ago", "2026-01-28")]
#[case::from_31_jan_in_1_month("2026-01-31 1 months", "2026-03-03")]
#[case::from_31_march_1_month_ago("2026-03-31 1 months ago", "2026-03-03")]
#[case::from_15_march_1_month_ago("2026-03-15 1 months ago", "2026-02-15")]
#[case::from_15_jan_in_1_month("2026-01-15 1 months", "2026-02-15")]
fn test_relative_month_time_non_leap_year(#[case] input: &str, #[case] expected: &str) {
let now = "2026-01-12"
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
check_time(input, expected, "%Y-%m-%d", Some(now));
}
#[rstest]
#[case::from_31_dec_1_month_ago("2026-12-31 1 months ago", "2026-12-01")]
#[case::from_31_dec_3_month_ago("2026-12-31 3 months ago", "2026-10-01")]
#[case::from_31_mar_in_1_month("2026-03-31 1 months", "2026-05-01")]
#[case::from_31_mar_in_3_month("2026-03-31 3 months", "2026-07-01")]
fn test_relative_month_time_dest_month_does_not_have_the_day(
#[case] input: &str,
#[case] expected: &str,
) {
let now = "2026-01-12"
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
check_time(input, expected, "%Y-%m-%d", Some(now));
}
#[rstest]
#[case::from_29_feb_in_1_month("2024-02-29 1 months", "2024-03-29")]
#[case::from_29_feb_1_month_ago("2024-02-29 1 months ago", "2024-01-29")]
#[case::from_31_jan_in_1_month("2024-01-31 1 months", "2024-03-02")]
#[case::from_31_march_1_month_ago("2024-03-31 1 months ago", "2024-03-02")]
#[case::from_15_march_1_month_ago("2024-03-15 1 months ago", "2024-02-15")]
#[case::from_15_jan_in_1_month("2024-01-15 1 months", "2024-02-15")]
fn test_relative_month_time_leap_year(#[case] input: &str, #[case] expected: &str) {
let now = "2026-01-12"
.parse::<DateTime>()
.unwrap()
.to_zoned(TimeZone::UTC)
.unwrap();
check_time(input, expected, "%Y-%m-%d", Some(now));
}
#[rstest]
#[case::months_in_0_months("2026-01-12 0 months", "2026-01-12")]
#[case::months_in_negative_1_months("2026-01-12 -1 months", "2025-12-12")]
#[case::months_in_1_months("2026-01-12 1 months", "2026-02-12")]
#[case::months_in_2_months("2026-01-12 2 months", "2026-03-12")]
#[case::months_in_3_months("2026-01-12 3 months", "2026-04-12")]
#[case::months_in_4_months("2026-01-12 4 months", "2026-05-12")]
#[case::months_in_5_months("2026-01-12 5 months", "2026-06-12")]
#[case::months_in_6_months("2026-01-12 6 months", "2026-07-12")]
#[case::months_in_7_months("2026-01-12 7 months", "2026-08-12")]
#[case::months_in_8_months("2026-01-12 8 months", "2026-09-12")]
#[case::months_in_9_months("2026-01-12 9 months", "2026-10-12")]
#[case::months_in_10_months("2026-01-12 10 months", "2026-11-12")]
#[case::months_in_11_months("2026-01-12 11 months", "2026-12-12")]
#[case::months_in_12_months("2026-01-12 12 months", "2027-01-12")]
#[case::months_in_24_months("2026-01-12 24 months", "2028-01-12")]
#[case::months_in_36_months("2026-01-12 36 months", "2029-01-12")]
#[case::months_in_120_months("2026-01-12 120 months", "2036-01-12")]
#[case::months_in_240_months("2026-01-12 240 months", "2046-01-12")]
fn test_time_in_months(#[case] input: &str, #[case] expected: &str) {
let now = "2026-01-12"
.parse::<DateTime>()