Add diff, add-months, and add-years to Datetime#7
Conversation
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.
There was a problem hiding this comment.
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.
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 buggyto-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.