Repository navigation
Add COUPDAYBS, COUPDAYS, COUPDAYSNC, COUPNCD, COUPNUM and COUPPCD functions - #1804
Tobiadefami wants to merge 9 commits into
Conversation
DateTimeHelper.dayCountByBasis(startDate, endDate, basis) returns the day count and the days in the year for day-count bases 0-4. Basis 0 applies toBasisUS to copies of the dates, basis 1 uses yearLengthForBasis, and basis 4 uses toBasisEU. YEARFRAC now returns dayCount / yearDays from the helper. Its results are unchanged. DAYS360 keeps its own days360Core and is not touched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nctions Six financial functions for discounted and interest-at-maturity securities, built on DateTimeHelper.dayCountByBasis. - Every argument is a SCALAR and is converted in the method, so booleans and the empty text give #VALUE! and invalid arguments are reported in Excel's order (dates, basis, then the amounts for the DISC-like functions; left to right for ACCRINTM). - Basis is truncated, but a negative basis is #NUM!, as in Excel. - Dates are truncated; dates outside the supported range give #NUM!. - The arithmetic follows the operation order that reproduces Excel's results to the 15th significant digit. - An empty or omitted ACCRINTM par is 1000. Excel rejects the 3-argument form; this is added to the list of differences. Adds catalogue entries, names in all language packs and a changelog entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolves the CHANGELOG conflict and lists ACCRINTM, DISC, INTRATE, PRICEDISC, RECEIVED and YIELDDISC in the ungrouped function capabilities, as the license capability completeness invariant requires. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@Tobiadefami thanks for the pull request. No CLA step needed here — our records show you signed the Contributor License Agreement on 2026-07-31. That signature came from our previous signing form and has been carried over, so there is nothing for you to re-sign. |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
hyperformula-docs | c86d1e4 | Commit Preview URL Branch Preview URL |
Oct 09 2026, 08:49 PM |
Performance comparison of head (c86d1e4) vs base (23ab1a4) |
coerceToSecurityDate truncated before checking the range, so a date such as -0.5 became serial 0, a valid date. Excel returns #NUM! for any negative date, before looking at the later arguments. Reject a negative value before truncating, as coerceToDayCountBasis already does for the basis. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the coupon functions that return a date or a count from the coupon schedule, with the runner and schedule helpers the day-count functions build on. - couponFunction validates the arguments in Excel's order: settlement, maturity, settlement before maturity, an empty settlement, maturity or frequency argument (#N/A), frequency, then basis. Dates and basis use the securities helpers (strictNumber, coerceToSecurityDate, coerceToDayCountBasis) unchanged; frequency is truncated and must be 1, 2 or 4 (new message ErrorMessage.CouponFrequency). - couponDate steps from maturity by 12 / frequency months. A month-end maturity gives month-end coupon dates; otherwise maturity's day is clamped to the month, leap years included, and never drifts. - couponsAfter counts the coupon dates after settlement in constant time, without stepping through the periods. - A previous coupon date before the earliest supported date is #NUM!. Adds catalogue entries, names in all language packs, the license capability entries and a changelog entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the coupon day-count functions on top of the coupon schedule. - COUPDAYBS counts from the previous coupon date to settlement with the day-count helper for every basis. - COUPDAYSNC uses the same helper for bases 1 to 4. For basis 0 it is the 30/360 length of the coupon period, with day 31 and the last day of February counted as 30 at both ends, minus COUPDAYBS, as in Excel. The three functions do not always add up, in Excel or here. - COUPDAYS is 360 / frequency for bases 0, 2 and 4 and 365 / frequency for basis 3. For basis 1 it is Excel's period length, which is not always the next coupon date minus the previous one: a walk back from maturity whose day can drift down at month ends, followed by an end-of-month step back. Computed in constant time. Adds catalogue entries, names in all language packs, the license capability entries and the changelog entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
f8578a9 to
c86d1e4
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit c86d1e4. Configure here.
| return new CellError(ErrorType.NUM, ErrorMessage.ValueSmall) | ||
| } | ||
| const {dayCount, yearDays} = this.dateTimeHelper.dayCountByBasis(settlement, maturity, basis) | ||
| return calculate(dayCount, yearDays, firstAmount, secondAmount) |
There was a problem hiding this comment.
Zero day-count yields Infinity or NaN
Medium Severity
DISC, INTRATE and YIELDDISC divide by dayCount after settlement is only required to be strictly before maturity. Under basis 0 or 4, 30/360 can still count zero days (for example the 30th and 31st of the same month), so the formula returns Infinity or NaN instead of a cell error.
Additional Locations (3)
Reviewed by Cursor Bugbot for commit c86d1e4. Configure here.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #1804 +/- ##
===========================================
+ Coverage 97.28% 97.32% +0.04%
===========================================
Files 207 207
Lines 16218 16426 +208
Branches 3588 3634 +46
===========================================
+ Hits 15777 15986 +209
+ Misses 433 432 -1
Partials 8 8
🚀 New features to boost your workflow:
|


Context
This PR adds the six Excel bond coupon functions:
COUPPCD(previous coupon date),COUPNCD(next coupon date),COUPNUM(number of coupons),COUPDAYBS(days from the start of the coupon period to settlement),COUPDAYSNC(days from settlement to the next coupon date) and
COUPDAYS(days in the coupon period).Important
Stacked on #1799 (
ACCRINTM,DISC,INTRATE,PRICEDISC,RECEIVED,YIELDDISC). The coupon functionsreuse that PR's day-count helper (
DateTimeHelper.dayCountByBasis) and its argument helpers (strictNumber,coerceToSecurityDate,coerceToDayCountBasis), so this branch is based onfeature/HF-454. Until #1799 merges,this diff also shows its commits. The changes in this PR are the last three commits:
feat(HF-373): add the coupon schedule and COUPPCD, COUPNCD and COUPNUM,feat(HF-373): add COUPDAYBS, COUPDAYSNC and COUPDAYSanddocs(HF-373): link the pull request in the changelog. Once #1799 merges, this branch will be rebased ontodevelopand only those three commits will remain.Behavior, checked against Excel:
maturityby 12 /frequencymonths, each computed frommaturityitself, so the day never drifts. Ifmaturityis the last day of its month, every coupon date is thelast day of its month (28 February counts only in non-leap years). Otherwise
maturity's day is kept, clamped tothe month length with leap years: maturity 30 August 2012, semiannual, gives 29 February 2012 and then 30 August
2011.
COUPPCDis the last coupon date on or beforesettlement(settlement on a coupon date returns it),COUPNCDthe next one, and
COUPNUMthe number of coupon dates aftersettlementup to and includingmaturity.basisisvalidated but does not change these three.
settlement,maturity,settlementbeforematurity, empty arguments,frequency, thenbasis. Unlike Add ACCRINTM, DISC, INTRATE, PRICEDISC, RECEIVED and YIELDDISC functions #1799's functions,basisis checked last:COUPPCD(s, m, 3, "abc")is#NUM!, not#VALUE!.settlement,maturityorfrequencyargument, as inCOUPPCD(s, m, , 1), gives#N/A.settlementaftermaturitystill gives#NUM!first. A blank cell is not an empty argument: it is 0, so ablank
frequencycell gives#NUM!.#VALUE!; a blank cell is 0; numeric, percent,currency and date text are accepted (date text per the
dateFormatsconfig). Dates are truncated, and dates outsidethe supported range give
#NUM!; a negative date such as −0.5 is#NUM!, before any later argument.frequencyis truncated (4.9 → 4) and must then be 1, 2 or 4;basisistruncated, but a negative value such as −0.5 is
#NUM!. A range is#VALUE!; array constants are lifted withuseArrayArithmetic: true.actual days.
settlementto the next coupon date. Basis 0 is the 30/360 length ofthe coupon period, with day 31 and the last day of February counted as 30 at both ends, minus
COUPDAYBS:COUPDAYSNC(DATE(2011,3,1), DATE(2011,8,29), 2, 0)is 178 (period 28 February to 29 August counts 179 days, minus 1).frequencyfor bases 0, 2 and 4, and 365 /frequencyfor basis 3 (182.5, 91.25). For basis 1it is Excel's own period length, which is not always the next coupon date minus the previous one when the maturity
falls on the 28th to the 31st: walking back from
maturity, Excel keeps each date's day clamped to the previousdate's (so 31 March, 30 September, 30 March), then steps back once with the end-of-month rule.
COUPDAYS(DATE(2014,11,28), DATE(2015,5,29), 4, 1)is 91, whileCOUPNCD − COUPPCDis 92.COUPDAYBS + COUPDAYSNCis not alwaysCOUPDAYS, in Excel and here.previous coupon date before the earliest supported date gives
#NUM!.Note: Microsoft's pages for these functions are wrong in three places, and this implementation follows Excel:
basisorsettlementof−0.5 gives
#NUM!.empty text give
#VALUE!; numbers outside the date range give#NUM!.COUPPCDandCOUPNCDexamples show the result as a date; Excel returns a plain serial number, in a Generalcell. These functions set no date number format.
Implementation
FinancialPlugin, next to Add ACCRINTM, DISC, INTRATE, PRICEDISC, RECEIVED and YIELDDISC functions #1799's securities functions: six methods, one runner(
couponFunction) that validates the arguments and builds aCouponSchedule(count,previous,next), andprivate helpers:
couponDate,couponsAfter,coerceToCouponFrequency,daysBeforeSettlement,thirtyDayPeriodLength,thirtyDayMonthDay,actualCouponPeriodLengthanddriftedCouponDate.DateTimeHelper.dayCountByBasis(COUPDAYBSfor every basis,COUPDAYSNCforbases 1–4),
strictNumber,coerceToSecurityDateandcoerceToDayCountBasis. Also reusesoffsetMonthanddaysInMonthfromDateTimeHelper; nothing is added there.couponsAftercounts the coupon dates without stepping through the periods (a quarterly scheduleover the whole supported date range has 32,400 of them), and
driftedCouponDatestops after2 × frequencysteps,after which the drifted day no longer changes.
ErrorMessage.CouponFrequency("Frequency must be 1, 2 or 4."). Empty arguments use the existingEmptyArg. No new error types.categories/financial.ts, and names in all language packs, read from Microsoft Excel Onlinein each language.
csCZandidIDkeep the English names, as Excel does.UNGROUPED_FUNCTIONSinfunctionCapabilities.ts, like Add ACCRINTM, DISC, INTRATE, PRICEDISC, RECEIVED and YIELDDISC functions #1799's.How did you test your changes?
test is the formula measured in Excel, on the same fixture layout, so any case can be re-checked by entering it in
Excel. Cases cover the docs examples, every basis, frequency values and truncation, settlement against maturity,
settlement on and next to a coupon date, end-of-month and February maturities, date bounds, coercion of every
argument, error precedence, empty arguments, arrays and nesting. Config-dependent cases pass their config:
dateFormats: ['MM/DD/YYYY', 'YYYY-MM-DD']for text dates,useArrayArithmetic: truefor array constants. Datesbefore 1 March 1900 pin HyperFormula's results.
function-couppcd394,function-coupncd383,function-coupnum399,function-coupdaybs587,function-coupdaysnc634,function-coupdays513.15th and on the 28th to the 31st of every month, frequencies 1, 2 and 4, settlement the day after each of the five
previous coupon dates) and 354 basis-0 schedules around month ends. The specs take the rows that exercise a rule rather than all of them: for
COUPDAYS,the 120 schedules where the period length is not
COUPNCD − COUPPCDplus the 5 others where the end-of-month stepback passes settlement (125); for
COUPDAYBS(207) andCOUPDAYSNC(254), every schedule where a 30/360 month-endrule changes that function's result. The implementation matches Excel on all 1,959 schedules (6,585 values, checked outside the specs).
#NAME?).and the YEARFRAC spec pass unchanged.
COUPPCDwithout the end-of-month rule: 44;COUPNCDwith settlement on a coupon date counted as before it: 46;COUPNUMwithout the day check: 103;COUPDAYBSin actual days for every basis: 185;COUPDAYSNCwithout the February end counted as 30: 202;COUPDAYSbasis 1 without the second end-of-month step: 21, and without the stop at settlement: 9.npm run lint(0 errors),npm run compile,npm run docs:generate-function-docs,git diff --check.Types of changes
Related issues:
Checklist:
🤖 Generated with Claude Code
Note
Medium Risk
Large additive surface in financial/date logic where Excel parity and edge cases (30/360, month-end coupons) can affect numeric results; no changes to core mutation or auth paths.
Overview
Adds twelve Excel-style bond/financial functions and the shared date math they depend on.
Coupon schedule (this PR’s focus):
COUPPCD,COUPNCD,COUPNUM,COUPDAYBS,COUPDAYSNC, andCOUPDAYSare implemented inFinancialPluginvia a commoncouponFunctionrunner that builds aCouponSchedule(previous/next coupon dates, count) from settlement, maturity, and frequency (1/2/4), with Excel-specific rules for month-end maturities and basis-0/1 day counts.Discounted securities (stacked with #1799 in this diff):
ACCRINTM,DISC,INTRATE,PRICEDISC,RECEIVED, andYIELDDISCsharediscountedSecurityvalidation and formulas using the same day-count basis.Shared infrastructure:
DateTimeHelper.dayCountByBasis(anddaysIn30DayMonths) centralize US/European 30/360 and actual-day bases;YEARFRACis refactored to call it. NewErrorMessage.CouponFrequency, function metadata, i18n entries, licenseUNGROUPED_FUNCTIONSentries, changelog, and anACCRINTMrow in the differences doc accompany the engine changes.Reviewed by Cursor Bugbot for commit c86d1e4. Bugbot is set up for automated code reviews on this repo. Configure here.