From b0244b5f7264ff9a9b952435f8193b9636b1a1d3 Mon Sep 17 00:00:00 2001 From: chuenchen309 <48723787+chuenchen309@users.noreply.github.com> Date: Sun, 19 Jul 2026 11:21:25 +0800 Subject: [PATCH] Make DateTime - Duration calendar-aware for days/weeks (match add/subtract) `DateTime + duration(days=n)` routes through `add(**delta._signature)`, so it is calendar-aware, but `DateTime - duration(days=n)` folded `days`/`weeks` into absolute seconds (`delta._total`). Across a DST transition the two disagree: `dt - duration(days=1)` shifts the wall-clock time by an hour while `dt.subtract(days=1)` and `dt + duration(days=-1)` do not, so the operator contradicts its own named method and `(dt - d) + d == dt` breaks. Mirror `_add_timedelta_` by passing `delta._signature` to `subtract`, so subtraction of a Duration is the exact negation of addition. Absolute units (hours/minutes/seconds) and naive datetimes are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: chuenchen309 <48723787+chuenchen309@users.noreply.github.com> --- src/pendulum/datetime.py | 4 +--- tests/datetime/test_sub.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/pendulum/datetime.py b/src/pendulum/datetime.py index da89b13d..60a5bdc6 100644 --- a/src/pendulum/datetime.py +++ b/src/pendulum/datetime.py @@ -692,9 +692,7 @@ def _subtract_timedelta(self, delta: datetime.timedelta) -> Self: Remove timedelta duration from the instance. """ if isinstance(delta, pendulum.Duration): - return self.subtract( - years=delta.years, months=delta.months, seconds=delta._total - ) + return self.subtract(**delta._signature) # type: ignore[attr-defined] return self.subtract(seconds=delta.total_seconds()) diff --git a/tests/datetime/test_sub.py b/tests/datetime/test_sub.py index 1e519779..69771f4c 100644 --- a/tests/datetime/test_sub.py +++ b/tests/datetime/test_sub.py @@ -120,6 +120,21 @@ def test_subtract_duration(): assert d.microsecond == 123456 +def test_subtract_duration_days_across_dst_matches_method(): + # `- duration(days=...)` must be calendar-aware like `.subtract(days=...)` + # and `+ duration(days=-...)`, not fold days into absolute seconds. + d = pendulum.datetime(2013, 4, 2, tz="Europe/Paris") # spring-forward is 2013-03-31 + + assert (d - pendulum.duration(days=3)) == d.subtract(days=3) + assert (d - pendulum.duration(days=3)) == (d + pendulum.duration(days=-3)) + assert (d - pendulum.duration(weeks=1)) == d.subtract(weeks=1) + + # round-trip stability across a transition + dt = pendulum.datetime(2023, 3, 12, 5, 0, tz="America/New_York") + one = pendulum.duration(days=1) + assert (dt - one) + one == dt + + def test_subtract_time_to_new_transition_skipped(): dt = pendulum.datetime(2013, 3, 31, 3, 0, 0, 0, tz="Europe/Paris")