Skip to content

Add diff, add-months, and add-years to Datetime#7

Merged
hellerve merged 1 commit into
masterfrom
claude/datetime-arithmetic
May 29, 2026
Merged

Add diff, add-months, and add-years to Datetime#7
hellerve merged 1 commit into
masterfrom
claude/datetime-arithmetic

Conversation

@carpentry-agent

Copy link
Copy Markdown

Summary

Adds three calendar arithmetic functions to Datetime:

  • diff — returns the signed difference in seconds between two Datetimes. Positive when the first argument is later, negative when earlier. Uses ordinal-based calculation (no dependency on the buggy to-unix-timestamp).

  • add-months — adds (or subtracts) N months with end-of-month clamping. Jan 31 + 1 month = Feb 28 (or 29 in a leap year). Mar 31 + 1 month = Apr 30. Crosses year boundaries correctly in both directions.

  • add-years — adds (or subtracts) N years with leap-year-aware day clamping. Feb 29 2024 + 1 year = Feb 28 2025. Handles century leap year rules (1900 not leap, 2000 leap).

All three functions preserve time components (hours, minutes, seconds, nanoseconds, timezone).

27 new tests covering: basic operations, end-of-month clamping, leap year edge cases, negative values, year boundary crossings, century leap years, time component preservation.


Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.

diff returns signed seconds between two Datetimes using ordinal-based
calculation. add-months handles end-of-month clamping (e.g. Jan 31 + 1
month = Feb 28/29). add-years handles leap year Feb 29 clamping. All
three preserve time components and support negative values.

@carpentry-reviewer carpentry-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build & Tests

CI passes on macos-latest (the only runner configured for this repo — no ubuntu-latest, pre-existing). No local Carp compiler available, so build/test verified via CI only.

Findings

1. Verified correct: diff (time.carp:926-935)

to-ordinal returns an absolute Gregorian ordinal (via ymd2ord at line 197 → days-before-year + days-before-month + day), NOT a day-of-year. This means (- ord-a ord-b) correctly handles year boundaries and leap years without any extra logic. Manual verification:

  • to-ordinal(2025,1,1) - to-ordinal(2024,1,1) = 366 (2024 is leap) ✓
  • 1 day + 1h1m1s = 86400 + 3661 = 90061

diff defaults Nothing time components to 0, which is correct and consistent with the existing > and < implementations.

2. Verified correct: add-months (time.carp:944-957)

Converts to 0-indexed month count (year*12 + month-1), adds n, then converts back using div- and mod-. Critically, div- (line 245) uses floor(a/n) — this is floor division, not C truncation — so negative inputs work correctly. For example: Jan 2024 - 1 month → total=24287, div-(24287,12)=2023, mod-(24287,12)+1=12 → Dec 2023. Correct.

Day clamping via min(day, days-in-month) handles all end-of-month cases. Time/nanoseconds/timezone are preserved by copying from the input.

3. Verified correct: add-years (time.carp:965-976)

Simple and clean — delegates leap year logic entirely to days-in-month, which handles Feb 29 → non-leap clamping correctly. Century leap year rules (1900 not leap, 2000 and 2400 leap) are covered by is-leap at line 129.

4. Int overflow in diff for distant dates (Informational)

diff returns Int, and (- ord-a ord-b) * DAY overflows 32-bit Int for dates ~68 years apart. This is consistent with the existing API (add-seconds, to-unix-timestamp all use Int), so it's a pre-existing design limitation, not a regression. A brief note in the diff docstring would be a nice courtesy but isn't required.

5. diff ignores nanoseconds (Informational, correct)

The docstring says "signed difference in seconds" — dropping nanoseconds is intentional. Noted for completeness since =, >, < do compare nanoseconds.

6. Test coverage is good (27 tests)

Covers: same date, one day, reversed order, time-only diff, mixed date+time, non-leap year, leap year, all add-months cases (clamp, year-cross, negative, 12-months, -24-months, no-clamp, time preservation), all add-years cases (simple, leap→non-leap, leap→leap, negative, identity, century rules, time preservation).

Nice-to-have gaps (not blocking): no test for add-months FROM Feb 29 (e.g., add-months(2024-02-29, 12) → should clamp to Feb 28 2025), and no test with very large month additions (+120).

Verdict: merge

Clean, correct implementation. diff properly uses absolute Gregorian ordinals, add-months properly uses floor division for negative arithmetic, and add-years correctly delegates to days-in-month. 27 tests cover the important cases well. No bugs found.

@hellerve hellerve merged commit 2d7c04e into master May 29, 2026
1 check passed
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