Releases: ryanduguid/Ozzit
Release list
v3.1.0
Everything since v3.0.0 in one cut: the FY27 example shift, the palette restyle, the deterministic clean-up tooling, the GST legislative scope note, and a run of help corrections.
Highlights:
- Every worked example starts 1 July 2026 (FY27); help text re-dated to match with column alignment preserved.
- palette pass across the workbook; printer settings, saved paths and always-calculate flags stripped; the zip rebuilds deterministically.
oz.GSTAddλandoz.GSTExtractλspill a legislative scope note: GST Act 1999 ss 9-70 and 9-75, arithmetic only.- Help corrections: CorkscrewλDV, AboutFinancialλ, the four CountCols index rows, two
<Coming Soon>WEBPAGE rows, and this cut's pair -oz.CurrentRatioλandoz.ROIλno longer link their neighbours' Investopedia articles. - The
oz.SumContainsλtab now carries the λ every sibling has. - Postbuild passes are tracked, idempotent tools in
tools/postbuild/with asserted hit counts and CI coverage.
Gates at release: verify_workbook 130 functions and 211 parts, verify_sources, verify_signatures 117 signatures, verify_previous_names 130 of 130, verify_index, verify_afe, Excel selftest 259 assertions 0 failed, verify_cache all 20,228 cached values match their formulas.
v3.0.0: the library is now Ozzit
The library is renamed from Nabla to Ozzit, and every function's prefix moves from nb. to oz. Nothing computes differently: the same 259 assertions pass on the same 1,129 formulas, and the only cells that differ from v2.6.0 are text.
Why
Nabla sits one edit from an acronym nobody wants beside their name on a public repository. It was also a poor description of the library: the nabla operator means a gradient, and there is no calculus anywhere in here.
Migrating
Replace nb. with oz. That is the whole of it.
The bare names are untouched, so nb.Amortiseλ becomes oz.Amortiseλ and nb.SumRowsUλ becomes oz.SumRowsUλ. No argument, result or rounding moves with the prefix. v2.0.0 shipped a 130-row mapping table because its rename collapsed six namespaces into one and tagged the collisions; this one is a uniform three-character swap, so a table would only restate the rule 130 times.
Formulas written against nb. stop resolving. The break is taken now, while nothing depends on them.
The rename is length-preserving
Nabla and Ozzit are both five characters, nb. and oz. both three, and the bare nb the help text names in prose is two, like oz. Every function carries a help block aligned in columns, so a replacement of any other width would have re-aligned 130 of them. tools/rebrand_to_ozzit.py asserts the property on every string it rewrites rather than trusting it.
What the rename does not touch
tools/released-names-v1.2.6.txt, the previous_name column of functions.csv, and the changelog entries before this one still read nabla.d.Aboutλ and the rest. They record what earlier releases actually shipped. Somebody migrating off an older workbook has to be able to look up a name that existed rather than one that never did.
Also in this release
nabla.xlsxis nowozzit.xlsx, and the mark inassets/no longer draws the operator the library never implemented- the repository itself is renamed, so the help blocks link to
github.com/ryanduguid/Ozzit. GitHub redirects the old address, so existing links and clones keep working tools/transform_from_upstream.pynames the upstream functionsozzit.*on the way through and lands them inoz., so a rebuild from the upstream workbook produces this release- the version stamp each function carries reads 19 August 2026
Verification
20,226 cached values match the values their formulas produce. 130 functions across 211 parts. src/ reproduces every one of them. 117 signatures, 122 parameter tables and 119 example blocks describe their own function. The Excel self-test recalculates 1,129 formulas and runs 259 assertions with none failing and none in error.
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.
v2.5.0: an example that runs
A documentation release. No formula and no defined name's behaviour differs from v2.4.0.
nb.TimelineOffsetλ's worked example could not be run as printed
The call was missing the two closing brackets that finish EDATE and the function call itself, so copying the one line a reader is meant to copy got a syntax error rather than an answer. The Result column beside it was empty, where every other example in the library prints what it returns, which is how the missing brackets went unseen: there was no answer to disagree with.
Before
Result →Formula (nb is assumed to be the module's name)
→=nb.TimelineOffsetλ("15/2/2024", EDATE("1/1/2025", SEQUENCE( , 12, 0)
After
Result →Formula (nb is assumed to be the module's name)
1 →=nb.TimelineOffsetλ("15/2/2026", EDATE("1/1/2026", SEQUENCE( , 12, 0)))
-11 →=nb.TimelineOffsetλ("15/2/2025", EDATE("1/1/2026", SEQUENCE( , 12, 0)))
Two rows now, against the same 1 January 2026 timeline the demonstration sheet builds, one date inside it and one before it, because a date falling before a model's timeline is what the function's own discussion comment is written for. Upstream wrote the example against 2/15/2022 and a timeline starting 1/1/2023, and the date sweep moved both forward two years with everything else, which still left it two years behind the rest of the sample data.
tools/excel_selftest.ps1 runs both examples and holds them to their printed results, 1 and -11, so the printed answer cannot drift from the call beside it again. 197 assertions, up from 195.
The parameter table above the example spelled "timline" and now spells the word the function is named after.
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,224 cached values match their formulas |
excel_selftest |
197 of 197, 1,129 formulas, 0 error cells |
Whole-workbook cell diff against v2.4.0: five cells, all of them on the sheet that displays this function's own help.
Known and not fixed
nb.TimelinePositionλ's parameter table carries the same "timline" spelling.nb.Allocateλaccumulates its result withHSTACKinside aREDUCE, which rebuilds the whole accumulator on every pass. It is quick at any sane input and quadratic at a large one, andnb.Depreciateλdoes not check that a life in years is a life rather than a date serial, so a mistyped argument stops responding rather than returning an error.nb.Amortiseλandnb.Depreciateλeach hold their own copy of the months-per-period rounding and fail on sub-monthly timelines.nb.InterestLRVλis not cap-aware, so in the one period where the cap binds it understates the interest it reports. Balances are unaffected.- The repository still states no licence for its own tooling.
Asset
nabla.xlsx, 435,862 bytes, sha256 912263caa3661d1efefa59ebe0cb78414db4a283c9878fe1732b703727242fa3.
nabla v2.4.0
Two functions that were named as known and not fixed in the v2.3.0 notes.
nb.PeriodStartλ returned a date that is not a period start whenever the anchor is a month end
It walked the calendar to the period's month, rebuilt the date with the anchor's day of the month, and corrected whatever overflowed by exactly one day. Anchored on 31 January, monthly, that asks for 31 February: Excel reads it as 3 March, one day back off it is 2 March, and 2 March is in neither the right month nor on any period boundary. 28 February, which is where that period does start, was unreachable.
A schedule anchored on a month end is what EDATE describes: the anchor's day of the month where the target month has one, the month's own end where it does not, so 31 January monthly runs 31 Jan, 28 Feb, 31 Mar. The procedure now counts the whole periods from the anchor to the date of interest and steps the anchor on by that many, then steps back one period where a truncated quotient overshoots.
Measured against that schedule over 9,900 cases, being eleven anchors by five period lengths by 180 dates at 13-day steps from January 2024, the old procedure was wrong 169 times. Every one had an anchor on the 29th, 30th or 31st, 29 February included. Anchors on the 1st, 15th and 28th were already right and answer exactly as before.
nb.PeriodStartλ("31/1/2026", 1, "5/3/2026")
v2.3.0 2 March 2026
v2.4.0 28 February 2026
nb.TimelineOffsetλ divided by zero on every daily, weekly and fortnightly timeline
It reads the interval off the timeline's first two dates and rounds it to whole months, which is zero for anything shorter than about a fortnight, and the next line divides by it. Every such call returned #DIV/0!.
A sub-monthly period is a fixed number of days, so the offset is now the day difference floored by that count: a date 20 days into a weekly timeline is in period 2, one three days before it starts is in period -1. The month path is untouched and answers exactly what it answered before on monthly, quarterly and yearly timelines, month-end anchored ones included, over 200 dates at 11-day steps.
This does not make nb.Amortiseλ work on a sub-monthly timeline. It calls nb.TimelineOffsetλ on whatever timeline it is handed, so it did inherit this failure, but it rounds the same interval to months itself and then divides twelve by it, so a weekly timeline still fails there on its own arithmetic. nb.Depreciateλ holds a third copy of the rounding and turns nought months into #N/A. Neither is changed here.
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,221 cached values match their formulas |
excel_selftest |
195 of 195, 1,129 formulas, 0 error cells |
tools/excel_selftest.ps1 gains 43 assertions, 152 to 195. Nineteen compare nb.PeriodStartλ against an EDATE schedule written out in the test rather than borrowed from the function, and four hold nb.TimelineOffsetλ's month path to the same comparison, so a later change cannot quietly redefine what a period start is.
Only xl/workbook.xml and the formula-environment store changed. No worksheet part differs from v2.3.0 and all 20,221 cached values are identical: the sheet that demonstrates nb.TimelineOffsetλ uses a monthly timeline, and no sheet calls nb.PeriodStartλ.
Known and not fixed
nb.Amortiseλandnb.Depreciateλeach hold their own copy of the months-per-period rounding and still fail on sub-monthly timelines, as above.nb.InterestLRVλis not cap-aware, so in the one period where the cap binds it understates the interest it reports. Balances are unaffected.nb.TimelineOffsetλ's worked example is missing two closing brackets and prints no result, so it cannot be copied.- The repository still states no licence for its own tooling.
Asset
nabla.xlsx, 435,780 bytes, sha256 29d641761e01367e623ec588874d712fe17298f314efc5acef5e5c898bb4da35.
nabla v2.3.0
Two things this release does that no earlier one could.
The workbook holds the numbers its own formulas produce
An .xlsx stores a formula and the answer Excel last got from it, and nothing keeps the two in step. The build edits the workbook as XML with no formula engine, so every cell downstream of a value it changes kept the answer it had before. Shifting the sample dates forward two years left 3,193 cached cells across 43 of the 50 sheets holding numbers their own formulas no longer produce; nb.IsOccurrenceDateλ alone had 978.
Nobody ever saw one, because Excel recalculates on open and replaces the lot. That is what made it worth finding: the file could be wrong in a way only a second tool could see, and everything that reads an .xlsx without a formula engine, from a diff to a converter to a web preview, reads the cached answer.
tools/refresh_cache.py recalculates the workbook in Excel and saves it. fullCalcOnLoad comes off with the stale values, so the workbook now opens reading correctly without recalculating, and Excel no longer asks to save a file you never edited. Nothing about the functions changes, measured rather than assumed: all 4,506 worksheet formula cells byte-identical, all 130 defined names byte-identical, the Advanced Formula Environment store byte-identical. The file grows by 247 bytes.
It no longer carries a path off the machine that built it
Excel stamps xl/workbook.xml with the directory a file was last saved from, so every release since v1.2.0 published one. The build strips it, the refresh strips it again because Excel puts it back on every save, and it joins the banned tokens so CI fails rather than trusting either step to have run.
What a full audit found
Thirteen readers across the build, the gates, the workbook, the LAMBDA sources, the prose and the CI, with every finding put to a second pass that tried to refute it.
nb.ScheduleValuesByItemsλandnb.ScheduleRatesByItemsλcompared a raw date against a converted one, the defect their siblings had until v2.2.0. The check added then could not see them: each reads its conversion, but only to hand it to the recursive call, so the first row came from the raw argument and every row below it from the converted one. With text period bounds the first returned 0 where the answer is 100, and the second the 20 January rate where the 1 January rate applies.nb.DebtSculptVariableLRVλ's worked example called its sibling and dropped its closing bracket.verify_signatureswas added in v2.1.0 for exactly this defect and structurally could not see it: it skipped the text on theEXAMPLESrow itself, leaving 43 of the 119 example blocks unread. It now reads all 119.functions.csvdisagreed with the workbook it is generated from:nb.Depreciateλ's signature cut off mid-parameter list, 31 descriptions carrying the raw OOXML escape for a line break, two starting with the help table's delimiter.docProps/app.xmldescribed a 49-sheet workbook that has 50.ATTRIBUTION.mdstill called the depreciation helpers ATO methods, which v2.1.0 retracted everywhere else.- Nine comment banners named or described the wrong function, and
nb.MaxColsλtold the reader it returns the minimum.nb.CountColsλpublished its signature with an empty second argument.
Gates
Six now, four in CI and two local. tools/verify_cache.py is new: it compares all 20,221 cached values against what the formulas produce, refuses to pass on fewer than 15,000 comparisons, and fails when any sheet contributes none. verify_workbook gained the sheet-list and build-path checks and stopped demanding fullCalcOnLoad. verify_previous_names now checks the two functions.csv columns nothing checked. CI dropped a dependency nothing imported, cancels superseded runs, and asks for read-only access.
All six pass, including 152 Excel assertions. Two cached cells moved against v2.2.0, both corrected help text on its own demonstration sheet.
Known and not fixed
nb.PeriodStartλreturns a date in the wrong month for month-end anchors:("31/1/2026", 1, "5/3/2026")gives 2 March where 28 February is the period start. The fix is anEDATErewrite, which changes results.nb.TimelineOffsetλdivides by zero on a daily or weekly timeline, andnb.Amortiseλcalls it unconditionally.nb.Depreciateλcarries a second copy of the same inference. Which intervals these suites support is a decision, not a repair.- The repository still states no licence for its own tooling.
ATTRIBUTION.mdis correct about the derived workbook and silent about everything else.
Full detail in CHANGELOG.md. nabla.xlsx is attached below.
nabla v2.2.0
v2.1.0 listed what it knew was still wrong and left it, because each of these changes moves results for anyone already relying on them. This release makes them right.
Nothing is renamed, and formulas written against v2.1.0 keep working. Across all 20,813 recalculated cells in the workbook, four moved, all four on the Periods demonstration sheet.
nb.Periodsλ counts the way its own examples count
It counted whole intervals, which is what DATEDIF returns, while every one of its four worked examples counts the period starts crossed between the two dates. Its description says as much: it lists "End Date is inclusive" as one of its differences from DATEDIF, and then the procedure called DATEDIF. From 31 March to 15 May is one whole month and two month starts, and the help says 2.
It now counts ordinals, the same rule for all five intervals, so a part period at the end counts once and a whole one does not count twice. All four documented examples hold for the first time: 2, -2, -12 and -53.
Dis unchanged, because a day ordinal is the serial number itself.Wfollowsnb.PeriodLabelλ's own week numbering, which restarts each 1 January and therefore labels every year with 53 weeks, the last of them one or two days long. That is what makes the fourth example -53 rather than the 52 whole weeks its two dates are apart.
This changes results. Anything measured in months, quarters, weeks or years returns one more than it did wherever the end date falls part way through a period, which is most of the time. The two demonstration cells that move are the quarters row, 4 to 5, and the weeks row, 52 to 53. If you were relying on whole-interval counts, DATEDIF is still there and still does that.
The third example was also the one line in this help nobody could copy: it passed four arguments to a function that takes three. The -12 it claims is right without the fourth.
Four more functions threw away their date conversions
The defect nb.OverLapDaysλ carried until v2.1.0. Two of them gave wrong answers for it:
nb.ScheduleValuesλcompared text period starts against converted effective dates, so nothing ever fell inside a period. It returned 0 where it should return 100.nb.ScheduleRatesλlooked up text dates in a text array, soXLOOKUPmatched them in dictionary order. Asked for the rate in force on"5/1/2026"against starts of"1/1/2026"and"10/1/2026", it returned the January 10 rate, because"5/1/2026"sorts after"10/1/2026"as text.nb.PeriodLabelλandnb.Timelineλwere saved by Excel: both feed the date straight intoTEXT,YEAR,EDATEorSEQUENCE, all of which coerce a text date themselves. Checked against every case tried, they returned the right answer before and after. Their fix is a guard, not a repair.
The workbook stopped shipping cached errors it does not reproduce
Five cells on the Periods demonstration sheet were saved holding #VALUE!. They come from the upstream workbook, which carries 65 such cells, and they have been in every tagged release from v1.2.0 on. Excel replaces them with the right answers as soon as the file opens, so no reader ever saw them and no recalculation reproduces them, but a file that disagrees with itself is a file nobody can check. They now hold the answers the formula gives.
One cached error is left, #NAME? on the Essentials About sheet, which resolves on open the same way. Its cell holds a spilled table rather than a number, so it needs the table built rather than a value written.
Gates
tools/verify_sources.py now requires that a function which converts a date argument goes on to read the conversion. It is the check that would have caught all five of these, and it could not be added before they were fixed, because it fails on them. Run against v2.1.0 it names all six bindings and the function each belongs to. A general unused-value check would need a real parser and would report a great deal more; this one asks a narrower question and gets a clean answer.
Five gates now, four of them in CI, plus the local Excel gate: 130 functions and 210 parts, src/ reproducing all 130, 117 signatures with 122 parameter tables and 76 example blocks, 130 previous_name entries one to one against the v1.2.6 baseline, and 1,129 formulas recalculated with 138 assertions.
Full detail in CHANGELOG.md. nabla.xlsx is attached below.
nabla v2.1.0
v2.0.0 renamed every function. This one fixes what three of them compute, and stops two more from claiming to be something they are not.
Nothing is renamed, so formulas written against v2.0.0 keep working. Three functions do return different numbers, and only for the inputs they were handling wrongly.
Three functions did not do what they said
nb.OverLapDaysλcompared its dates as text. It converts all four arguments, so that a date written as text becomes a serial number, and then compared the raw arguments anyway. The four conversions were never read. Text dates compared as strings, which ranks"7/1/2025"after"17/1/2025", so the function took the wrong start and end and the subtraction that follows coerced them back to dates. Its own third example claimed 12 shared days for two January 2025 periods that share 2. Numbers and real dates were never affected.nb.Periodsλcould not return a negative. Its description promises "Returns negative values if Date1 is after Date2" and two of its four examples show one, but the difference was floored at 1 beforeSIGNsaw it.=nb.Periodsλ("15/5/2025", "31/3/2025")returned 1 where it should return -1.nb.VDBλignored itsNo_Switchargument. It declares the argument and defaults it toFALSE, then called Excel'sVDBwithout it, so passingTRUEdid nothing.=nb.VDBλ(1000, 100, 5, 1.5, TRUE)now gives 300.00, 210.00, 147.00, 102.90 and 72.03, against the 121.50, 121.50 tail that switching to straight line produces. Calls that omit the argument are unaffected, includingnb.Depreciateλ'sVDBmethod.
Two of the three were hidden by their own worked example, which printed the answer the bug produced, so those examples are corrected with them. Every other cached value in the workbook is byte-identical to v2.0.0; the one cell that moved is the corrected example result on nb.OverLapDaysλ's sheet.
The depreciation helpers no longer claim to be ATO methods
nb.DiminishingValueλ described itself as the ATO 200% diminishing value method and nb.PrimeCostλ as the ATO prime cost method. Neither is one. They take a cost and an effective life and nothing else: no acquisition date, no income year, no days held, no disposal. nb.DiminishingValueλ also writes the entire undeducted residual off in its final period, so for a cost of 1,000 over five years it returns 400, 240, 144, 86.40 and 129.60, where a diminishing-balance calculation deducts 51.84 that year and carries the rest forward.
The schedules are unchanged and still useful for modelling. What changes is that they are described as modelling schedules rather than tax calculations, in the function help, the method codes on nb.Depreciateλ, the Data Validation sheet, the Australian tax worksheet and the README. The worksheet says so on its face, and the README says plainly not to use them to prepare a return.
Help that had drifted from the code
- Three worked examples called a different function than the one they document:
nb.RollingMinλ's callednb.RollingMaxλ,nb.RollingSumλ's callednb.RollingMinλ, andnb.ScheduleValuesByItemsλ's callednb.ScheduleRatesByItemsλ. Each claimed result was correct for the function it wrongly called. nb.RollingAvgλclaimed1,3,6,9,12, the running total. Excel gives1,1.5,2,3,4.nb.Amortiseλ's help pointed atLableAmortiseλ, a transposition of a function that does exist.
Gates
Five now, four of them in CI.
tools/verify_signatures.pyreads the worked examples as well as the signatures and parameter tables: 117 signatures, 122 parameter tables, 76 example blocks. Run against the source before these fixes it names all four example defects.tools/verify_previous_names.pyis new. It checks the publishedfunctions.csvagainsttools/released-names-v1.2.6.txt, which records the 130 names the last release before the rename shipped. Counting alone proves a bijection, not the right one, so it also requires each new bare name to begin with the old one, which is the only shape the rename ever took.previous_nameis pinned to that released baseline rather than to the build's own intermediate names. A function whose predecessor is not in the file records nothing rather than a plausible-looking guess.- The build's help corrections can no longer reach a live formula. That refresh was a blanket text replace across the whole worksheet, and it once rewrote a real formula on a neighbouring sheet and spilled
#SPILL!across it. It is now confined to cached values.
Known and not fixed
-
nb.Periodsλcounts complete intervals where its examples count boundaries crossed, so its forward example returns 1 against the 2 it claims and itsWexample 52 against 53. Its demonstration sheet also ships five cached#VALUE!cells, which Excel replaces with the right answers the moment the file opens.Corrected 18 August 2026: this bullet first said the function returns
#VALUE!for range arguments. It does not. That came from a probe naming a worksheet that does not exist; the cached errors are stale, not live. -
Four functions ignore their date conversions the way
nb.OverLapDaysλdid:nb.PeriodLabelλ,nb.ScheduleRatesλ,nb.ScheduleValuesλandnb.Timelineλ.
Full detail in CHANGELOG.md. nabla.xlsx is attached below.
nabla v2.0.0
One namespace.
This release renames every function and breaks every formula written against v1.2.x.
nabla.f.Amortiseλ is now nb.Amortiseλ. Models built on the old names keep
working only if they keep the old workbook, which stays available at the v1.2.6 tag.
Six module prefixes became one. The gain is five fewer characters on every call and
autocomplete that works: typing =nb. narrows to this library instead of requiring you to
remember which of six modules a function lived in first.
Of the 130 functions, 106 change prefix and nothing else. Seventeen bare names existed in
more than one module: the fuller implementation keeps the plain name and the other 19 take
a one-letter tag, B for debt, E for essentials, U for utilities. The five About
tables take words.
nabla.xlsx is attached below, which earlier releases did not offer.
Every rename
| Was | Is now |
|---|---|
nabla.d.Aboutλ |
nb.AboutDatesλ |
nabla.d.CountDOWλ |
nb.CountDOWλ |
nabla.d.FinancialYearλ |
nb.FinancialYearλ |
nabla.d.IsBetweenλ |
nb.IsBetweenλ |
nabla.d.IsOccurrenceDateλ |
nb.IsOccurrenceDateλ |
nabla.d.OverLapDaysλ |
nb.OverLapDaysλ |
nabla.d.PeriodLabelλ |
nb.PeriodLabelλ |
nabla.d.Periodsλ |
nb.Periodsλ |
nabla.d.ScheduleRatesByItemsλ |
nb.ScheduleRatesByItemsλ |
nabla.d.ScheduleRatesλ |
nb.ScheduleRatesλ |
nabla.d.ScheduleValuesByItemsλ |
nb.ScheduleValuesByItemsλ |
nabla.d.ScheduleValuesλ |
nb.ScheduleValuesλ |
nabla.d.Timelineλ |
nb.Timelineλ |
nabla.debt.Amortiseλ |
nb.AmortiseBλ |
nabla.debt.DebtSculptFixedλ |
nb.DebtSculptFixedλ |
nabla.debt.DebtSculptVariableLRVλ |
nb.DebtSculptVariableLRVλ |
nabla.debt.DebtSculptVariableλ |
nb.DebtSculptVariableλ |
nabla.debt.InterestLRVλ |
nb.InterestLRVλ |
nabla.e.Aboutλ |
nb.AboutEssentialsλ |
nabla.e.AvgColsλ |
nb.AvgColsλ |
nabla.e.AvgRowsλ |
nb.AvgRowsλ |
nabla.e.CountAColsλ |
nb.CountAColsλ |
nabla.e.CountARowsλ |
nb.CountARowsλ |
nabla.e.CountColsλ |
nb.CountColsλ |
nabla.e.CountCλ |
nb.CountCλ |
nabla.e.CountRowsλ |
nb.CountRowsλ |
nabla.e.IsBetweenλ |
nb.IsBetweenEλ |
nabla.e.IsInListλ |
nb.IsInListλ |
nabla.e.MaxColsλ |
nb.MaxColsλ |
nabla.e.MaxRowsλ |
nb.MaxRowsλ |
nabla.e.MinColsλ |
nb.MinColsλ |
nabla.e.MinRowsλ |
nb.MinRowsλ |
nabla.e.RangeToDAλ |
nb.RangeToDAEλ |
nabla.e.SumColsλ |
nb.SumColsλ |
nabla.e.SumRowsλ |
nb.SumRowsλ |
nabla.f.Aboutλ |
nb.AboutFinancialλ |
nabla.f.Allocateλ |
nb.Allocateλ |
nabla.f.Amortiseλ |
nb.Amortiseλ |
nabla.f.AmortiseλDV |
nb.AmortiseλDV |
nabla.f.CorkScrewReversalλ |
nb.CorkScrewReversalλ |
nabla.f.Corkscrewλ |
nb.Corkscrewλ |
nabla.f.CorkscrewλDV |
nb.CorkscrewλDV |
nabla.f.Cumulativeλ |
nb.Cumulativeλ |
nabla.f.DBλ |
nb.DBλ |
nabla.f.DDBλ |
nb.DDBλ |
nabla.f.Depreciateλ |
nb.Depreciateλ |
nabla.f.DepreciateλDV |
nb.DepreciateλDV |
nabla.f.DiminishingValueλ |
nb.DiminishingValueλ |
nabla.f.FilterContainsλ |
nb.FilterContainsλ |
nabla.f.GSTAddλ |
nb.GSTAddλ |
nabla.f.GSTExtractλ |
nb.GSTExtractλ |
nabla.f.IRRλ |
nb.IRRλ |
nabla.f.IntOnIntλ |
nb.IntOnIntλ |
nabla.f.LabelAmortiseλ |
nb.LabelAmortiseλ |
nabla.f.LabelDepreciateλ |
nb.LabelDepreciateλ |
nabla.f.Movementλ |
nb.Movementλ |
nabla.f.PeriodDiffλ |
nb.PeriodDiffλ |
nabla.f.PeriodStartλ |
nb.PeriodStartλ |
nabla.f.PrimeCostλ |
nb.PrimeCostλ |
nabla.f.RangeToDAλ |
nb.RangeToDAλ |
nabla.f.Reversalλ |
nb.Reversalλ |
nabla.f.RollingAvgλ |
nb.RollingAvgλ |
nabla.f.RollingMaxλ |
nb.RollingMaxλ |
nabla.f.RollingMinλ |
nb.RollingMinλ |
nabla.f.RollingSumλ |
nb.RollingSumλ |
nabla.f.SLNλ |
nb.SLNλ |
nabla.f.SYDλ |
nb.SYDλ |
nabla.f.SumAmortiseλ |
nb.SumAmortiseλ |
nabla.f.SumContainsλ |
nb.SumContainsλ |
nabla.f.SumDepreciateλ |
nb.SumDepreciateλ |
nabla.f.SumPeriodsλ |
nb.SumPeriodsλ |
nabla.f.TimelineOffsetλ |
nb.TimelineOffsetλ |
nabla.f.TimelinePositionλ |
nb.TimelinePositionλ |
nabla.f.VDBλ |
nb.VDBλ |
nabla.r.Aboutλ |
nb.AboutRatiosλ |
nabla.r.AssetTurnoverRatioλ |
nb.AssetTurnoverRatioλ |
nabla.r.BVPSλ |
nb.BVPSλ |
nabla.r.CAPERatioλ |
nb.CAPERatioλ |
nabla.r.CashFlowMarginλ |
nb.CashFlowMarginλ |
nabla.r.CashRatioλ |
nb.CashRatioλ |
nabla.r.CurrentRatioλ |
nb.CurrentRatioλ |
nabla.r.DPRλ |
nb.DPRλ |
nabla.r.DSCRλ |
nb.DSCRλ |
nabla.r.DSIλ |
nb.DSIλ |
nabla.r.DebtRatioλ |
nb.DebtRatioλ |
nabla.r.DebtToAssetRatioλ |
nb.DebtToAssetRatioλ |
nabla.r.DebtToCapitalRatioλ |
nb.DebtToCapitalRatioλ |
nabla.r.DebtToEquityRatioλ |
nb.DebtToEquityRatioλ |
nabla.r.DividendYieldRatioλ |
nb.DividendYieldRatioλ |
nabla.r.EBITDAMarginλ |
nb.EBITDAMarginλ |
nabla.r.EPSλ |
nb.EPSλ |
nabla.r.EquityMultiplierλ |
nb.EquityMultiplierλ |
nabla.r.EquityRatioλ |
nb.EquityRatioλ |
nabla.r.GrossMarginλ |
nb.GrossMarginλ |
nabla.r.InterestCoverageRatioλ |
nb.InterestCoverageRatioλ |
nabla.r.InventoryTurnoverRatioλ |
nb.InventoryTurnoverRatioλ |
nabla.r.NetProfitMarginλ |
nb.NetProfitMarginλ |
nabla.r.OperatingCashFlowRatioλ |
nb.OperatingCashFlowRatioλ |
nabla.r.OperatingMarginλ |
nb.OperatingMarginλ |
nabla.r.OperatingRatioλ |
nb.OperatingRatioλ |
nabla.r.PretaxMarginλ |
nb.PretaxMarginλ |
nabla.r.PriceEarningsRatioλ |
nb.PriceEarningsRatioλ |
nabla.r.PriceToBookRatioλ |
nb.PriceToBookRatioλ |
nabla.r.PriceToCashRatioλ |
nb.PriceToCashRatioλ |
nabla.r.PriceToSalesRatioλ |
nb.PriceToSalesRatioλ |
nabla.r.QuickRatioλ |
nb.QuickRatioλ |
nabla.r.ROAλ |
nb.ROAλ |
nabla.r.ROEλ |
nb.ROEλ |
nabla.r.ROICλ |
nb.ROICλ |
nabla.r.ROIλ |
nb.ROIλ |
nabla.r.ReceivablesTurnoverRatioλ |
nb.ReceivablesTurnoverRatioλ |
nabla.r.RetentionRatioλ |
nb.RetentionRatioλ |
nabla.r.WorkingCapitalTurnoverRatioλ |
nb.WorkingCapitalTurnoverRatioλ |
nabla.u.Aboutλ |
nb.AboutUtilitiesλ |
nabla.u.AvgColsλ |
nb.AvgColsUλ |
nabla.u.AvgRowsλ |
nb.AvgRowsUλ |
nabla.u.CountAColsλ |
nb.CountAColsUλ |
nabla.u.CountARowsλ |
nb.CountARowsUλ |
nabla.u.CountColsλ |
nb.CountColsUλ |
nabla.u.CountCλ |
nb.CountCUλ |
nabla.u.CountRowsλ |
nb.CountRowsUλ |
nabla.u.IsBetweenλ |
nb.IsBetweenUλ |
nabla.u.IsInListλ |
nb.IsInListUλ |
nabla.u.MaxColsλ |
nb.MaxColsUλ |
nabla.u.MaxRowsλ |
nb.MaxRowsUλ |
nabla.u.MinColsλ |
nb.MinColsUλ |
nabla.u.MinRowsλ |
nb.MinRowsUλ |
nabla.u.RangeToDAλ |
nb.RangeToDAUλ |
nabla.u.SumColsλ |
nb.SumColsUλ |
nabla.u.SumRowsλ |
nb.SumRowsUλ |
nabla v1.2.6
Parameter tables that describe their own function.
Every function's help repeats its parameters as a table below the signature. v1.2.4 corrected the signatures; this corrects the tables, which are a separate piece of hand-written text and had drifted on their own. Every parameter table in the library now lists exactly what its function declares.
nabla.r.EquityRatioλ documented a different function's arguments
Its table listed OperatingIncome and InterestExpenses, which belong to InterestCoverageRatioλ, while the function takes ShareholdersEquity, TotalAssets and IntangibleAssets. This is the second half of the copy that gave it the wrong name until v1.2.3: the name and the table came across together and only the name was corrected then, so its three real parameters had never been described anywhere. They are now, in the wording its neighbours already use for the same quantities, checked against the function's own formula.
nabla.r.EquityMultiplierλ dropped a word
Its table called the second parameter ShareholdersEquity; the LAMBDA and the signature above it both say TotalShareholdersEquity. The shortened name is what the neighbouring DebtToEquityRatioλ genuinely takes.
IsBetweenλ named a parameter it does not have
Its table called the second argument Lo where the LAMBDA and the signature both say Low, and the row for Hi read "The lower limit that the value must be less than", copied from the row above, so a function whose whole purpose is bounding a value described its upper bound as a lower one. The Inclusive row referred to Lo as well. All three are corrected in the Essentials and Utilities copies; the Dates module's own IsBetweenλ has always had them right and supplied the wording.
Verified
All four gates pass, 1,129 formulas recalculate with no error cells, 138 of 138 assertions. Both equity functions still return what their own examples claim: 20/(60-10) is 40%, and 366.6/83.2 is 4.40625. Every corrected table was read back out of a running Excel rather than trusted from the file.