diff --git a/rust/src/python/helpers.rs b/rust/src/python/helpers.rs index f1ba0806..4f0cce8f 100644 --- a/rust/src/python/helpers.rs +++ b/rust/src/python/helpers.rs @@ -1,5 +1,4 @@ #![allow(clippy::useless_conversion)] -use std::cmp::Ordering; use pyo3::{ prelude::*, @@ -308,6 +307,9 @@ pub fn precise_diff<'py>( hour_diff -= 1; } + // The time of day of dt2 is earlier than that of dt1 + let time_borrow = hour_diff < 0; + if hour_diff < 0 { hour_diff += 24; day_diff -= 1; @@ -332,26 +334,17 @@ pub fn precise_diff<'py>( let days_in_month = DAYS_PER_MONTHS[usize::from(helpers::is_leap(dtinfo2.year))][dtinfo2.month as usize]; - match day_diff.cmp(&(days_in_month - days_in_last_month)) { - Ordering::Less => { - // We don't have a full month, we calculate days - if days_in_last_month < dtinfo1.day { - day_diff += dtinfo1.day; - } else { - day_diff += days_in_last_month; - } - } - Ordering::Equal => { - // 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 => { - // We have a full month - day_diff += days_in_last_month; - } + if dtinfo2.day == days_in_month && !time_borrow { + // dt2 is the last day of its month and dt1's day does not + // exist in it, so adding the months to dt1 lands exactly + // on dt2 (e.g. Jan 31 + 1 month = Feb 28): a full month + day_diff = 0; + month_diff += 1; + } else if days_in_last_month < dtinfo1.day { + // We don't have a full month, we calculate days + day_diff += dtinfo1.day; + } else { + day_diff += days_in_last_month; } month_diff -= 1; diff --git a/src/pendulum/_helpers.py b/src/pendulum/_helpers.py index bb989625..ca5742df 100644 --- a/src/pendulum/_helpers.py +++ b/src/pendulum/_helpers.py @@ -253,6 +253,8 @@ def precise_diff( hour_diff += 24 d_diff -= 1 + # A negative d_diff here means the time of day of d2 is earlier than d1's + time_borrow = d_diff < 0 y_diff = d2.year - d1.year m_diff = d2.month - d1.month d_diff += d2.day - d1.day @@ -272,20 +274,16 @@ def precise_diff( days_in_last_month = DAYS_PER_MONTHS[leap][month] days_in_month = DAYS_PER_MONTHS[int(is_leap(d2.year))][d2.month] - if d_diff < days_in_month - days_in_last_month: - # We don't have a full month, we calculate days - if days_in_last_month < d1.day: - d_diff += d1.day - else: - d_diff += days_in_last_month - elif d_diff == days_in_month - days_in_last_month: - # We have exactly a full month - # We remove the days difference - # and add one to the months difference + if d2.day == days_in_month and not time_borrow: + # d2 is the last day of its month and d1's day does not + # exist in it, so adding the months to d1 lands exactly + # on d2 (e.g. Jan 31 + 1 month = Feb 28): a full month d_diff = 0 m_diff += 1 + elif days_in_last_month < d1.day: + # We don't have a full month, we calculate days + d_diff += d1.day else: - # We have a full month d_diff += days_in_last_month m_diff -= 1 diff --git a/tests/date/test_diff.py b/tests/date/test_diff.py index cfececfc..1133965e 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.local(2017, 1, 31)): 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.local(2017, 2, 28)): 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 a9660495..ed732235 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -86,6 +86,33 @@ def test_precise_diff() -> None: assert_diff(diff, days=6, hours=0) +def test_precise_diff_partial_month() -> None: + # May 12 to June 11 is 30 days, not a full month + diff = precise_diff(datetime(2020, 5, 12, 4), datetime(2020, 6, 11, 10)) + assert_diff(diff, days=30, hours=6) + + diff = precise_diff(datetime(2020, 6, 11, 10), datetime(2020, 5, 12, 4)) + assert_diff(diff, days=-30, hours=-6) + + # https://github.com/python-pendulum/pendulum/issues/790 + diff = precise_diff(datetime(2024, 1, 3), datetime(2024, 2, 1)) + assert_diff(diff, days=29) + + +def test_precise_diff_end_of_month() -> None: + # Jan 31 + 1 month is clamped to Feb 28, so this is a full month + diff = precise_diff(datetime(2023, 1, 31), datetime(2023, 2, 28)) + assert_diff(diff, months=1) + + # https://github.com/python-pendulum/pendulum/issues/402 + diff = precise_diff(datetime(2019, 8, 30), datetime(2020, 2, 29)) + assert_diff(diff, months=6) + + # Not a full month if the time of day has not been reached yet + diff = precise_diff(datetime(2023, 1, 31, 10), datetime(2023, 2, 28, 9)) + assert_diff(diff, days=27, hours=23) + + def test_precise_diff_timezone() -> None: paris = pendulum.timezone("Europe/Paris") toronto = pendulum.timezone("America/Toronto")