v2.6.0: any period length
Five defects, and a licence over the parts of this repository that can carry one. Nothing any demonstration sheet computes has changed: twenty-eight cells differ from v2.5.0, all of them text, all on the four sheets that display the help of the functions below.
nb.Amortiseλ returned #DIV/0! on every timeline shorter than a month
It reads the period length off the timeline's first two dates and rounds it to whole months, which is nought for anything under about a fortnight, and the next two lines divide by it. Daily, weekly and fortnightly timelines all failed.
The schedule is solved monthly whatever the timeline, because the function's own description says the payments are monthly, and only then folded into the timeline's periods. Flooring the count at one month is not enough on its own: the folded block is laid down one period at a time, so a weekly timeline would date month two a week after month one and report a year of interest inside a quarter. Each month's figures now go in the one period that contains that month's start, and the periods between hold nothing, which is what nb.Depreciateλ has always done on the same timelines. The two balance rows are dated the same way rather than divided, because they are balances.
A twelve-month loan drawn on 1 January 2026 now reports the same money on a weekly, a fortnightly and a daily timeline as it does on a monthly one, to the cent, and puts the first month in period one and the second in the week that holds 1 February rather than the week after the first.
Two details are worth stating, because the obvious version of each is wrong. The by-date path runs whenever the period is under 28 days, not whenever the period rounds to nought months: no month is shorter than 28 days, and a period of 16 to 27 days is no whole number of months either, but used to round to one and be laid out as though it were a month long, which dated month twelve of a 20-day timeline about a hundred days early. And a period ends where the next one opens rather than a fixed number of days after it opens, so a timeline whose periods are not all the same length still counts every month exactly once. Only the last period has no successor to ask, and it runs on as far as the period before it did.
Whole-month timelines answer exactly what they answered before.
nb.Depreciateλ dropped the last period of any timeline shorter than a month
Every period but the last takes its end date from the next period's start. The last has no successor and was given EDATE( its own start, MpP) - 1, which at nought months is the day before it opens, so nothing could fall inside it.
Depreciation on 10,000 over five years, from 1 January 2026
48 weekly periods v2.5.0 1,833.37 v2.6.0 2,000.00
49 weekly periods v2.5.0 2,000.00 v2.6.0 2,000.00
12 monthly periods v2.5.0 2,000.00 v2.6.0 2,000.00
December went missing outright in the first row. The last period now runs on as far as the period before it did: a whole number of months where the timeline is monthly or longer, and the same span of days as its predecessor where it is shorter.
A correction: nb.Depreciateλ never failed on two-, four- or six-month timelines
The v2.4.0 and v2.5.0 notes both said it did. That was wrong, and it came from reading the source rather than running it. The published source carries two SWITCH lookups that list only monthly, quarterly and yearly and return #N/A for anything else, but each is read by exactly one binding, nothing reads those two bindings, and Excel never evaluates them. Measured in a real Excel, a six-month timeline has always returned a correct schedule.
All four bindings are deleted rather than generalised, and the parameter table no longer names three intervals as though they were the only ones.
nb.Depreciateλ stopped responding when a life in years was not a life
Its arguments read InitialValues, InServiceDates, LifeInYears, Timeline. Transposing the middle two puts a date serial where the life belongs, and 1 January 2026 is 46,023, so the function is asked for a schedule 552,276 months long and Excel stops answering rather than erroring.
It now checks that every life is a number over 0 and no more than 100, returns a message naming the argument order, and clamps the life it uses as well as reporting it, so the arrays are never built. A life of 100 years still works; 101, 0, a date and a word are all refused. A life that arrives as text rather than as a number still works: Excel coerced it before and refusing it now would be a regression, so the check reads the value rather than the type.
nb.Allocateλ rebuilt its whole answer on every pass
It accumulated with HSTACK inside a REDUCE, so each new group copied everything already built and the work was quadratic in the number of amounts. It is quick at any sane input, and it is what turned the transposed call above from an error into a workbook that stops responding: 46,023 amounts spread over 552,276 months is of the order of 10^10 element copies.
The answer is a closed form, so the row is sized once and each column computed from its own index. Every published result is unchanged to the digit, the deliberate last-column adjustment included: it is still the amount less the SUM of the same array of equal parts, in the same order, so even the floating-point residue is identical. The demonstration sheets prove it, since nb.Depreciateλ allocates through this function on every one of them and not one depreciation figure moved.
nb.InterestLRVλ understated the interest in the period a debt is retired
It solves for the interest on the average balance over a period, taking the principal repayment as the cash available for debt service less that interest. Nobody repays more than they owe, and its caller says so: nb.DebtSculptVariableLRVλ caps the payment at the principal. This function did not, so wherever the cap binds it solved a repayment larger than the debt, put the average balance below half the opening balance, and reported too little interest.
nb.InterestLRVλ(1200, 1, 1000, 5%)
v2.5.0 20.51
v2.6.0 25.00 the balance runs from 1,000 to nil, so half of it, for a period
The repayment is now capped inside the iteration, both where it is assumed and where it is solved. It converges faster, not slower: once the cap binds, the assumed and solved repayments are both the principal and the first pass is the last. The published example is nowhere near the cap and still prints 222.90.
No balance anywhere moves, because a closing balance is the principal less the payment and never read the interest, which is exactly why the six balance identities in the self-test could not see this.
The last two help misspellings
nb.TimelinePositionλ's parameter table spelled the word the function is named after "timline", which v2.5.0 fixed in nb.TimelineOffsetλ and named as still outstanding. nb.LabelDepreciateλ's said "teh".
A licence
LICENCE is MIT and covers tools/, .github/, the Markdown files and assets/, which were written for this repository. It does not extend to nabla.xlsx, src/ or functions.csv. Those derive from a workbook whose author retains all rights, as ATTRIBUTION.md has said since v1.0.0, and nothing here can grant what is not ours to grant.
GitHub reports this repository's licence as "Other" rather than "MIT" as a result. The scope note at the top of the file is what stops its licence scanner matching the MIT text, and the note is the part that matters: the workbook is not MIT and saying so plainly is worth more than the badge.
ATTRIBUTION.md now also records the build input, which is not in this repository and cannot be: its size, its sha256 and its part count, which is enough to tell a rebuild from the same file apart from a rebuild from a different one.
Verification
| Gate | Result |
|---|---|
verify_workbook |
130 functions, 211 parts |
verify_sources |
src reproduces all 130, every date conversion read |
verify_signatures |
117 signatures, 122 parameter tables, 119 example blocks |
verify_previous_names |
130 one-to-one against v1.2.6 |
verify_cache |
all 20,226 cached values match their formulas |
excel_selftest |
259 of 259, 1,129 formulas, 0 error cells |
tools/excel_selftest.ps1 goes from 197 assertions to 259, and now waits for Excel to finish calculating before reading them. It did not: it asked for the answers immediately after ordering a full rebuild, and a range Excel is still calculating hands back a null rather than a partial answer. The lighter assertion set never lost that race; these are heavy enough to.
Known and not fixed
nb.AmortiseλDV, the data-validation helper fornb.Amortiseλ, still rejects a timeline whose periods are under 30 days, with the message "Timeline must a row of period start dates spanning no less than a month". That guard now contradicts the function it validates. Its wording is also missing a word, in three places.nb.Amortiseλandnb.Depreciateλmeasure the period length from the first gap alone, so a timeline whose periods are not all the same length is silently mis-measured.nb.Amortiseλstill stacks each loan onto the whole result so far, which is quadratic in the number of loans, andnb.Depreciateλdoes the same with its assets.nb.Amortiseλuses 30.42 days a month wherenb.Depreciateλandnb.TimelineOffsetλuse 30.5. Three copies of the same inference, two constants. No input tested tells them apart.nb.SumAmortiseλdoes not total the debt-issuance row, which is a flow like the three it does total.
Asset
nabla.xlsx, 438,194 bytes, sha256 58dde025323d5e4edd64be0434b28ee28d63f38aca76785c2618f5df0169b40b.