Skip to content

Carry the UTC shift into the date in the Rust precise_diff - #1060

Open
ybai08 wants to merge 1 commit into
python-pendulum:masterfrom
ybai08:fix-rust-precise-diff-utc-rollover
Open

ybai08 wants to merge 1 commit into
python-pendulum:masterfrom
ybai08:fix-rust-precise-diff-utc-rollover

Conversation

@ybai08

@ybai08 ybai08 commented Oct 18, 2026

Copy link
Copy Markdown

Pull Request Check List

  • Added tests for changed code.
  • Updated documentation for changed code. (no documented behaviour changes)

When the two datetimes are in different time zones (or fall on the same day), the Rust precise_diff moves each one to UTC by adjusting the hour, minute and second fields by hand. That adjustment only carries when a field goes strictly past 60 or 24, and it never carries into the month or year. So:

  • a time that lands exactly on 24:00 stays as hour 24,
  • a day pushed past either end of the month becomes day 0 or day 32.

The pure-Python precise_diff subtracts the offset with datetime arithmetic and does not have this problem, so the results depend on whether the extension is loaded:

import pendulum

a = pendulum.datetime(2024, 6, 1, tz="UTC")
b = pendulum.datetime(2024, 6, 1, 20, tz="America/New_York")   # 2024-06-02 00:00 UTC
a.diff(b).in_words()
# extension:    '24 hours'   (diff.days == 1, diff.hours == 24)
# pure Python:  '1 day'

a = pendulum.datetime(2023, 6, 1, tz="UTC")
b = pendulum.datetime(2023, 12, 31, 20, tz="America/New_York") # 2024-01-01 01:00 UTC
a.diff(b).in_words()
# extension:    '6 months 4 weeks 3 days 1 hour'
# pure Python:  '7 months 1 hour'

a = pendulum.datetime(2024, 1, 30, tz="UTC")
b = pendulum.datetime(2024, 3, 1, 5, tz="Asia/Tokyo")          # 2024-02-29 20:00 UTC
a.diff(b).in_words()
# extension:    '1 month 20 hours'
# pure Python:  '4 weeks 2 days 20 hours'

This change does the shift on the total seconds of the day and carries any day change into the day, month and year. Since UTC offsets are always less than a day, the date moves by at most one day. The two hand-written copies (one per datetime) are replaced by one DateTimeInfo::shift_to_utc method.

This is separate from #901 / #906. The example in #901 comes from is_exact_type_of on the second argument, which #953 changes. The new test uses stdlib datetime objects with zoneinfo, so it does not depend on that change.

Testing

  • New test test_precise_diff_timezone_crossing_a_date_boundary in tests/test_helpers.py. It covers the three cases above, in both argument orders.
    • Before the fix, with the extension: 1 failed, 18 passed (assert 0 == 1, PreciseDiff(... days=0, hours=24 ...)).
    • With PENDULUM_EXTENSIONS=0, before and after: 19 passed.
    • After the fix, with the extension: 19 passed.
  • pytest -q tests with the extension: 1871 passed, 3 skipped. With PENDULUM_EXTENSIONS=0: 1871 passed, 3 skipped.
  • A random comparison of pendulum._pendulum.precise_diff against pendulum._helpers.precise_diff. It used 200k pairs per seed and three seeds, with stdlib aware, naive and date inputs over UTC, New York, Paris, Kolkata, Lord Howe, St Johns, Kathmandu and Chatham. Before: 59 mismatches for seed 1. After: 0 mismatches for every seed.
  • cargo fmt --all -- --check and cargo clippy --tests -- -D warnings are clean, and so are ruff check and ruff format --check (0.14.11) on the test file.

Not run: mypy, PyPy, the codspeed benchmarks, and other platforms (macOS arm64, CPython 3.13 only).

Note: this changes the diff() / Interval results that the extension returns for datetimes in different time zones (or on the same day) when the UTC shift crosses a date boundary. They now match the pure-Python results.

When the two datetimes are in different time zones (or on the same
day), the Rust precise_diff moves each one to UTC by adjusting the
hour, minute and second fields by hand. That adjustment only rolls
over when a field goes strictly past 60 or 24, so a result of exactly
24:00 stays as hour 24, and a day pushed past either end of the month
becomes day 0 or day 32 instead of moving into the neighbouring month.

The pure-Python implementation subtracts the offset with datetime
arithmetic and does not have this problem, so the two disagree, e.g.
2024-06-01 00:00 UTC to 2024-06-01 20:00 America/New_York is
"1 day" in Python but "24 hours" with the extension.

Do the shift on the total seconds of the day and carry any day change
into the day, month and year.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant