Repository navigation
Conversation
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
hyperformula-docs | 09df598 | Commit Preview URL Branch Preview URL |
Oct 08 2026, 03:07 PM |
Performance comparison of head (09df598) vs base (eff7928) |
marcin-kordas-hoc
left a comment
There was a problem hiding this comment.
The fix looks right to me. The clamp now follows daysInMonth(), and with leapYear1900 off the results match a Gregorian oracle for every start date and offset I tried (27,811 combinations per nullDate setting, including negative offsets across 1900). develop misses 214 of them, all in leap-year Februaries. The paired specs fail 6 of 9 at the merge-base and pass here.
One thing before I approve: the sentence in the description saying 29 February 1900 matches Excel. Excel Online returns 28 February there (details inline). The 29 is HyperFormula's own leapYear1900 model, so I'd keep the code, change the sentence, and add a row to list-of-differences.md.
| * Clamps the day of the month so that it does not exceed the number of days in that month. | ||
| * Leap years, including the configurable 1900 leap year, are taken into account. | ||
| */ | ||
| public truncateDayInMonth(date: SimpleDate): SimpleDate { |
There was a problem hiding this comment.
I measured this in Excel Online (not desktop). DAY(60) is 29, but no EDATE or EOMONTH result is ever serial 60. =EDATE(DATE(1900,1,31),1) gives 59 (28 Feb) and =EDATE(60,1) gives 89 (29 Mar), so only the February clamp is 28. Excel keeps the fake leap day in the serial-to-date mapping only. Its month arithmetic treats February 1900 as 28 days.
With leapYear1900: true and nullDate 1899-12-31, the setup the compatibility guide recommends, 16 of my 450 start/offset cases now return 60 where develop returned 59 (for example EDATE(DATE(1900,3,31),-1)). Another 161 moved closer to Excel.
I wouldn't special-case 1900. EOMONTH on develop already returns 60, and DATE(1900,2,29) is valid in that mode. A row in list-of-differences.md covering both functions should be enough.
There was a problem hiding this comment.
Thanks, agreed on all points. I've kept the code as is, reworded the description so it no longer claims the 1900 result matches Excel, and added a row to list-of-differences.md covering both EDATE and EOMONTH under leapYear1900. The Excel values in that row are from your Excel Online measurement, so let me know if you'd rather phrase them differently.
I also rebased both branches onto develop to clear a changelog conflict; the earlier browser-test failures were the VERSION specs from the paired tests branch lagging behind the HF-307 test updates, not this change.
EDATE clamped the shifted date's day with a static month-length table in which February always has 28 days, so any result landing in February of a leap year was one day short: =EDATE(DATE(2028, 1, 31), 1) returned 2028-02-28 instead of 2028-02-29. Move truncateDayInMonth onto DateTimeHelper so it can use the existing leap-aware daysInMonth(), which also honours the leapYear1900 option. EOMONTH already went through endOfMonth() and was unaffected. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
With leapYear1900 enabled, HyperFormula treats February 1900 as having 29 days, so EDATE and EOMONTH can return 29 February 1900. Excel keeps the 1900 leap day only in its serial-to-date mapping and returns 28 February from both functions. Record this in the list of differences. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ce4de30 to
09df598
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #1781 +/- ##
========================================
Coverage 97.28% 97.28%
========================================
Files 207 207
Lines 16218 16218
Branches 3588 3588
========================================
Hits 15777 15777
Misses 433 433
Partials 8 8
🚀 New features to boost your workflow:
|
Context
EDATEclamps the day of the shifted date so it does not exceed the length of the target month, but the clamp used a static month-length table in which February always has 28 days. Any result that landed in February of a leap year was therefore one day short:=EDATE(DATE(2028, 1, 31), 1)=EDATE(DATE(2028, 3, 31), -1)=EDATE(DATE(2028, 1, 29), 1)=EDATE(DATE(2000, 1, 31), 1)=EDATE(DATE(2100, 1, 31), 1)=EOMONTH(DATE(2028, 1, 31), 1)The fix moves
truncateDayInMonthfrom a module-level function ontoDateTimeHelper, where it can call the existing leap-awaredaysInMonth(). That helper already honours theleapYear1900option, so with that option on,=EDATE(DATE(1900, 1, 31), 1)now returns 29 February 1900 (serial60withnullDate1899-12-31), the same resultEOMONTHalready gave for that month. This is HyperFormula's ownleapYear1900model, not Excel behaviour: Excel keeps the 1900 leap day only in its serial-to-date mapping (DAY(60)is 29) and its month arithmetic treats February 1900 as 28 days, so Excel returns 28 February (serial59) for both functions. The difference is now recorded indocs/guide/list-of-differences.md, coveringEDATEandEOMONTH.EOMONTHalready useddaysInMonth()throughendOfMonth()and is unchanged. The removed function was not exported from the package entry point, so there is no public API change.How did you test your changes?
hyperformula-testsbranch (fix/edate-leap-year-february): handsontable/hyperformula-tests#67. They cover forward and backward shifts into a leap-year February, the 29th and 30th day boundaries, the 400-year and 100-year rules, a leap day shifted into a non-leap year, and bothleapYear1900settings. Six of the nine fail ondevelopand all pass with this change.function-edate,function-eomonth, anddatespecs: 67 passed.npm run lint: 0 errors, no new warnings.npm run verify:typings: clean.src/withts-node.Types of changes
Related issues:
EDATEoutput against other spreadsheet implementations.Checklist:
🤖 Generated with Claude Code
Note
Low Risk
Localized date arithmetic bug fix with no public API change; behavior change only for EDATE results landing in leap-year February (and 1900 when leapYear1900 is true).
Overview
Fixes
EDATEso month shifts that land in a leap-year February clamp to 29 days instead of always using 28 (e.g.=EDATE(DATE(2028, 1, 31), 1)→2028-02-29).truncateDayInMonthmoves from a module helper ontoDateTimeHelperand uses leap-awaredaysInMonth()(includingleapYear1900) instead of a static February length.EDATEinDateTimePlugincalls the instance method;EOMONTHis unchanged.Docs: CHANGELOG entry and a list-of-differences row for February 1900
EDATE/EOMONTHvs Excel/Google Sheets whenleapYear1900is enabled.Reviewed by Cursor Bugbot for commit 09df598. Bugbot is set up for automated code reviews on this repo. Configure here.