From 492f908ba4f403725020c61ddc79761b5fd7a86c Mon Sep 17 00:00:00 2001 From: Sri Charan Date: Sun, 11 Oct 2026 20:42:12 +0530 Subject: [PATCH] Fix partial intervals matching shorter month lengths --- rust/src/python/helpers.rs | 6 ++++-- src/pendulum/_helpers.py | 6 +++++- tests/date/test_diff.py | 4 ++-- tests/test_helpers.py | 38 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/rust/src/python/helpers.rs b/rust/src/python/helpers.rs index f1ba0806e..84a0f8a5c 100644 --- a/rust/src/python/helpers.rs +++ b/rust/src/python/helpers.rs @@ -341,14 +341,16 @@ pub fn precise_diff<'py>( day_diff += days_in_last_month; } } - Ordering::Equal => { + Ordering::Equal + if dtinfo1.day == days_in_last_month && dtinfo2.day == days_in_month => + { // We have exactly a full month // We remove the days difference // and add one to the months difference day_diff = 0; month_diff += 1; } - Ordering::Greater => { + Ordering::Equal | Ordering::Greater => { // We have a full month day_diff += days_in_last_month; } diff --git a/src/pendulum/_helpers.py b/src/pendulum/_helpers.py index bb9896252..7dab7a71f 100644 --- a/src/pendulum/_helpers.py +++ b/src/pendulum/_helpers.py @@ -278,7 +278,11 @@ def precise_diff( d_diff += d1.day else: d_diff += days_in_last_month - elif d_diff == days_in_month - days_in_last_month: + elif ( + d_diff == days_in_month - days_in_last_month + and d1.day == days_in_last_month + and d2.day == days_in_month + ): # We have exactly a full month # We remove the days difference # and add one to the months difference diff --git a/tests/date/test_diff.py b/tests/date/test_diff.py index cfececfce..d96cf9c84 100644 --- a/tests/date/test_diff.py +++ b/tests/date/test_diff.py @@ -261,7 +261,7 @@ def test_diff_for_humans_other_and_month(): assert today.diff_for_humans(today.add(months=1)) == "1 month before" - with pendulum.travel_to(pendulum.datetime(2017, 1, 31)): + with pendulum.travel_to(pendulum.datetime(2017, 1, 31, tz="America/Toronto")): today = pendulum.today().date() assert today.diff_for_humans(today.add(weeks=4)) == "1 month before" @@ -314,7 +314,7 @@ def test_diff_for_humans_other_and_future_month(): assert today.diff_for_humans(today.subtract(weeks=4)) == "4 weeks after" assert today.diff_for_humans(today.subtract(months=1)) == "1 month after" - with pendulum.travel_to(pendulum.datetime(2017, 2, 28)): + with pendulum.travel_to(pendulum.datetime(2017, 2, 28, tz="America/Toronto")): today = pendulum.today().date() assert today.diff_for_humans(today.subtract(weeks=4)) == "1 month after" diff --git a/tests/test_helpers.py b/tests/test_helpers.py index a96604954..2ca4abb68 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -126,6 +126,44 @@ def test_precise_diff_timezone() -> None: assert diff.total_days == 0 +@pytest.mark.parametrize( + "year, start_day, expected_days", [(2023, 4, 28), (2024, 3, 29)] +) +@pytest.mark.parametrize("reverse", [False, True]) +def test_precise_diff_partial_month( + year: int, start_day: int, expected_days: int, reverse: bool +) -> None: + start = datetime(year, 1, start_day) + end = datetime(year, 2, 1) + sign = -1 if reverse else 1 + diff = precise_diff(end, start) if reverse else precise_diff(start, end) + + assert_diff(diff, days=sign * expected_days) + + start = pendulum.datetime(year, 1, start_day, tz="America/Toronto") + end = pendulum.datetime(year, 2, 1, tz="America/Toronto") + interval = start - end if reverse else end - start + + assert interval.in_months() == 0 + assert interval.in_days() == sign * expected_days + + +@pytest.mark.parametrize("year, end_day", [(2023, 28), (2024, 29)]) +def test_precise_diff_clipped_full_month(year: int, end_day: int) -> None: + start = datetime(year, 1, 31) + end = datetime(year, 2, end_day) + + assert_diff(precise_diff(start, end), months=1) + assert_diff(precise_diff(end, start), months=-1) + + +def test_precise_diff_partial_month_with_borrowed_day() -> None: + start = datetime(2024, 1, 30, 1) + end = datetime(2024, 2, 29) + + assert_diff(precise_diff(start, end), days=29, hours=23) + + def test_week_day() -> None: assert week_day(2017, 6, 2) == 5 assert week_day(2017, 1, 1) == 7