Excel
nanoql's Excel add-in puts the library in worksheet formulas: =nqlFlatForward(A1, 0.03, "Actual365Fixed") builds a
curve, and =nqlYieldTermStructureDiscount(A2, DATE(2030,1,1)) reads it. It is an Excel-DNA 1.9 add-in on .NET 10
(net10.0-windows), for 64-bit Excel for Microsoft 365, whose dynamic arrays it spills into. It comes in two builds:
nanoql64.xll, the stable API;nanoql-experimental64.xll, the stable API and the experimental one (bindings.md's opt-in: loading it is the consent).
Both carry the same names, so a workbook built on the stable build opens unchanged in the experimental one. Load one of them per Excel process.
The add-in is generated by nanoql-bind from the surface the bindings cross (bindings.md), for the
types a desk's tasks need, listed by task. This page gives its rules, and how to install, build, debug and check it.
Installing
A release's nanoql-excel-<version>-win-x64.zip holds the two add-ins, nanoql's LICENSE, NOTICE and
THIRD_PARTY_NOTICES.md (with Excel-DNA's and .NET's notices, whose code each .xll carries), a README, and the
example workbooks with their conventions file under examples/ (Example workbooks, below).
- What it needs: 64-bit Excel for Microsoft 365 on Windows x64, and the .NET 10 Desktop Runtime, x64. Without the runtime, the add-in says so when it loads.
- Unblocking: Excel blocks an add-in downloaded from the internet, signed or not. Tick Unblock in the
.xll's Properties, or put its folder in one of Excel's trusted locations (Trust Center > Trusted Locations). - Loading: File > Options > Add-ins, Manage: Excel Add-ins, Go, Browse to the
.xll; Excel then loads it at each start. Opening the.xllloads it for the session alone. - Unsigned add-ins: the add-ins are unsigned. Excel set to "Require Application Add-ins to be signed by Trusted
Publisher", which group policy often sets, disables an unsigned
.xllwithout a prompt; a firm that enforces it signs the.xllwith its own code-signing certificate.
Example workbooks
Five workbooks in bindings/excel/examples/, and in the release's zip under examples/,
show the add-in at work, each on one sheet, its inputs in cells and its results named:
curve.xlsx: a Euribor 6M curve bootstrapped from deposits and swaps, read as discounts, zero rates and forwards;swap.xlsx: Euribor and €STR swaps on that curve;bond.xlsx: a five-year fixed-rate bond's NPV, prices and yield;option.xlsx: a European call and put by Black-Scholes, with their Greeks;sofr.xlsx: the USD SOFR swaps of the SOFR example, at QuantLib'sMakeOISdefaults and at the market's convention, which its cells spell out onOISRateHelperandMakeOIS, New York's calendar read from the conventions filemarket.xmlbeside it.
They call the stable add-in's functions alone. The curve, the swaps and the SOFR trades are the docs' own examples,
and give their numbers. Load the add-in, open a workbook and recalculate it (Ctrl+Alt+F9). sofr.xlsx reads
market.xml from its own folder, so open it from a folder holding both: one opened straight from a zip is a copy
with nothing beside it.
Other .NET add-ins
Excel holds one .NET runtime per process, and the add-ins run on .NET 10, so an add-in on another .NET version fails
silently in the same Excel, its functions or nanoql's #NAME?:
| Loaded first | Then | Result |
|---|---|---|
| nanoql | an add-in on .NET 8 | the other add-in's functions #NAME? |
| an add-in on .NET 8 | nanoql | nanoql's functions #NAME? |
| nanoql | an add-in on .NET 10 | both work |
| nanoql | an add-in compiled to native code (Native AOT) | both work |
The tool: nanoql-bind excel
In the repository, dotnet run --project src/NanoQL.Bind -c Release -- excel <command> runs it; outside it, the tool
installed from NuGet runs generate and build from the defaults it carries, and refuses update, check, package
and --test-exports, which work on the repository's own files (bindings.md, the tool):
| Command | What it does |
|---|---|
update |
writes bindings/excel/functions.json, the experimental build's surface: each function's Excel name, C name, arguments and their kinds, result, category, experimental mark and loop mask; the add-in's own functions; and the functions the completeness check withholds |
check |
regenerates the surface into memory and compares its bytes with the checked-in file; checks its names are distinct regardless of case, none a cell reference, none the add-in's own, none over 255 characters |
generate --out <folder> |
writes a build's generated C#, its surface and its repro templates alone |
build --out <folder> |
generates a build into an Excel-DNA project in <folder>, builds it and packs its .xll there |
package --out <folder> |
builds nanoql's two add-ins and writes the release's zip, nanoql-excel-<version>-win-x64.zip, with the example workbooks, and its symbols zip there |
The others take:
--experimental: the experimental API beside the stable;--assembly <prefix>=<path>: nanoql's two assemblies undernqlby default; see Users' assemblies below;--list <prefix>=<file>|none: see The list below;--rolesand--shipped, as the tool's other commands do.
build also takes:
--runtime-source <folder>: a local feed holding the runtime package, which a user's build otherwise restores from NuGet;--no-pack: compile only, with no.xllmade.
package takes --runtime-source alone: it packs nanoql's release.
In the repository the runtime comes from its own pack:
dotnet pack src/NanoQL.Excel -c Release -o <feed>
dotnet run --project src/NanoQL.Bind -c Release -- excel build --runtime-source <feed> --out <folder> [--experimental]
The build's folder then holds the add-in, nanoql64.xll or nanoql-experimental64.xll, and the notices. The project
the tool writes, NanoQL.Excel.Functions, holds only the generated functions and references the runtime package,
NanoQL.Excel, at the tool's version. Its .dna lists both assemblies, so the runtime's own functions register beside
the generated ones, and the .xll packs nanoql's assemblies inside it.
Names
The prefix and the words:
nql, then the words run together, QuantLibXL's form:- a constructor is the type's name:
nqlFlatForward; - any other member is the type's and the member's:
nqlYieldTermStructureDiscount,nqlInstrumentResultsNPV; - a getter drops its
get_, an init-only property'swith_isWith(nqlEvaluationContextWithFixings), and an operator drops itsop_(nqlDateAddition); - a static or extension member goes under its declaring type, a
<Header>Functionsclass keeping its name (nqlBlackFormulaFunctionsBlackFormula).
- a constructor is the type's name:
Overloads and collisions: names that meet regardless of case form a group, numbered in a stated order: the first plain, the rest
2,3, and on.nqlCalendarAdvanceisAdvance(Date, int, TimeUnit, …), andnqlCalendarAdvance2isAdvance(Date, Period, …). Within a group the order is:- the function's own type, by full name;
- its own members before inherited ones;
- the declaring type's full name, then the member's metadata token (QuantLib's declaration order, which the port keeps);
- binds by their engine input set, after the members;
- draws last.
A numbered name doesn't say which overload it is: the Function Wizard's argument names and descriptions tell them apart, and
functions.jsongives each one's arguments.One numbering for every build: names are numbered over the whole surface, the experimental API included, before the stable build and the list apply. A function has one name in both builds and in any user's build, and a type added to the list renumbers nothing.
Case: Excel matches names without regard to case and shows the registered case.
Optional arguments: trailing optional arguments may be left out (see Values).
Categories and help: a function's category is
nanoql.<namespace>. Its description and each argument's come from nanoql's XML documentation, with the argument's form ("The calendar (a Calendar handle or its name)"), each cut at a word to 253 characters. IntelliSense in cells isn't packaged; Excel-DNA's standalone IntelliSense add-in reads the registered descriptions.Frozen from 2.0: each release writes
bindings/excel/functions.shipped.json(nanoql-bind freeze), every released name, the experimental build's included, with its C name or its family's key.- The tool keeps each released name. A new function takes its group's lowest unused number, so adding an overload never renumbers a released one.
- A removed function's name stays in the file, retired, and is never given to another function, so a workbook calling it gets #NAME?, never another function's result.
- A released stable name changes only through deprecation (two releases and a year); an experimental function may change or go in any release, its name retired.
- Before 2.0 nothing is frozen.
The add-in's own functions take a prefix of their own, nqlxl; the tool refuses a generated name equal to one of them,
regardless of case:
| Function | What it gives |
|---|---|
nqlxlError(cell) |
the message of the error a cell's call returned, while the cell shows the error (Errors, below) |
nqlxlRepro(cell, format) |
the calls behind a cell: a vector, or the C# or the Python that makes them (Repro export, below) |
nqlxlRecipe(handle) |
the call that made a handle: its worksheet function and its arguments |
nqlxlVersion() |
nanoql's version and the .NET runtime the add-in runs on |
nqlxlDrawsValues(draws) |
a batch of draws' values: a column, or a draw per row for sequences |
nqlxlDrawsWeights(draws) |
a batch of draws' weights, a column |
nqlxlDrawsNext(draws) |
the generator's snapshot after the batch, from which the next batch draws |
The list: what Excel holds
Excel holds the functions of the types on bindings/excel/list.json, each entry a type's full name and its task (a
generic type by its definition, for its family). The tool carries it for nanoql's builds. It holds 309 types in 11
tasks:
| Task | Types | Some of them |
|---|---|---|
| dates and calendars | 16 | Date, Period, Calendar, JointCalendar, DayCounter, MakeSchedule, IMM, ECB |
| conventions and context | 6 | EvaluationContext, ConventionSet, ConventionsXml, ConventionSnapshot, Currency, IndexFixings |
| quotes | 6 | ForwardSwapQuote, ImpliedStdDevQuote, LastFixingQuote |
| indexes | 9 | IborIndex, OvernightIndex, SwapIndex, BMAIndex, the inflation indexes |
| curves | 30 | FlatForward, DiscountCurve, PiecewiseYieldCurve, the rate helpers, the default and inflation curves |
| cash flows | 23 | CashFlows, the legs and coupons, the coupon pricers |
| instruments | 84 | swaps, bonds, options, caps and floors, swaptions, credit default swaps and their builders |
| engines | 53 | the pricing engines, and every results record a listed function returns |
| volatility | 37 | BlackConstantVol, BlackVarianceSurface, the swaption and optionlet volatilities, SABR |
| models and calibration | 19 | HullWhite, G2, HestonModel, the calibration helpers, the optimizers |
| math | 26 | the interpolations, the normal distribution, the sequence generators |
- What an entry brings:
- the type's own functions: its constructors, members, statics, binds and draws;
- the instance members it inherits, each once from its nearest declaration (its own, then its base classes', an interface's only where no class declares it), constructors and statics excepted: a swap brings its legs and fixed rate, a model its parameters;
- the extension methods on it, such as a model's
Calibrate.
- Excel's filter still applies inside a listed type. It drops:
- equality and increment operators, and a results record's constructors and
with; - setters, and members that return nothing but change an object other cells hold (a validation's check is kept);
- members writing files or reading the clock (
ConventionsXml.WriteDefaults,Date.TodaysDate: Excel's ownTODAY()serves); - an engine's hooks, and members taking a writable span or an in-out argument.
- equality and increment operators, and a results record's constructors and
- The completeness check: a listed function is withheld, to a fixpoint, when no listed function makes a required
input of it, an object or an engine, and no lookup by name builds one.
functions.json's"withheld"lists each, with the types it lacks. 19 on the list: the bond yields taking a solver, the short-rate models' trees, the swaps a listed engine prices but the list doesn't hold, among them. - A family whole: a listed generic type keeps all its constructions. Where the check withholds one, the family's text differs from the tests' build's, and the build's tests fail naming the family and the type its construction lacks: a gap for the list to close.
- A type made by a lookup: where no listed function makes a type, the convention set's lookup by its record's name
may:
=nqlConventionSetInterestRateIndex(, "BMAIndex", curve)makes the BMA index. - Asking for a type: open an issue naming the type and the task it serves. An entry added is a change that breaks no workbook. An entry removed after a release is a removed function: a stable one under deprecation (two releases and a year), an experimental one in any release, its name retired.
- A user's own list:
--list nql=<file>widens or narrows nanoql's list for a user's build, in the same format;--list nql=nonetakes nanoql's whole surface, 10,226 functions with the experimental API and 8,008 without. A wider build names every function as nanoql's builds do. - The budget: Excel-DNA's loader registers at most 10,000 functions, and one past them is silently missing. The tool counts a build's functions, the generated and the add-in's own, and refuses to pack one over 10,000, naming the count and the largest namespaces. The builds on the list hold 2,652 functions with the experimental API (328 experimental) and 2,324 without, leaving slots for a user's assemblies.
Values
| Kind | In a cell or an argument | Out |
|---|---|---|
| date | Excel's serial number, which is QuantLib's; a time of day refused | a serial number |
| period | text: "6M", "1Y6M"; days, weeks, months and years only |
text: "6M" |
| enum | its member's name, any case: "ModifiedFollowing", "Call" |
the name |
| boolean | TRUE or FALSE (1 or 0) | TRUE or FALSE |
| whole number | a number with no fraction | a number |
| number, text | as such | as such; a non-finite number #NUM! |
| object | its handle (Handles, below); a calendar or a day counter also by name | a handle |
| results record | a handle | a handle, with a function per field: nqlInstrumentResultsNPV(h) |
| array | a row, a column or an array constant, read by rows | a spill down a column; objects as handles |
| matrix | a 2-D range, by rows | a spill by rows |
| array of optional elements | a range, its shape the array's | a spill, none as #N/A |
| map | a range of two columns: keys, values | two columns |
| jagged array | a range, a row per inner array | a spill, short rows padded with #N/A |
| tuple | a row or a column of its items | across a row |
| array of tuples | a range, a row per element and a column per item | a row per element |
| complex number | Excel's complex text, "3+4i", as COMPLEX writes it |
the same, with round-trip digits |
| loss distribution | (not taken) | six columns under a header row: x, dx, density, cumulative, excess, average |
| generator | a snapshot's handle | a snapshot's handle |
| draws | (not taken) | a handle, read through nqlxlDrawsValues, nqlxlDrawsWeights and nqlxlDrawsNext |
- A 1904 workbook's serial numbers are converted at the boundary, 1,462 days, the date system read per call.
- An argument left out, or an empty cell, takes the parameter's default: none where the default is none, otherwise
the default value. So no cell passes none where C# defaults to something else, such as the mixed interpolations'
leftConditionValue = 0. - A range's empty cells: trailing ones are trimmed, and an empty cell inside a range is refused, so two parallel ranges can't fall out of step. An array of optional elements is the exception: its range is the array, an empty cell none and nothing trimmed. A map's and an array of tuples' trailing empty rows are trimmed.
- What is refused: a map's duplicate key, and an array of tuples' range of another width, its message naming the form ("a range, a row per element: Key, Curve"); each with #VALUE!.
- A family's type arguments: a generic type's constructions fold into one function per family, its type arguments
first, by name, in any case:
=nqlPiecewiseYieldCurve("Discount", "LogLinear", "IterativeBootstrap", date, helpers, dayCounter, ctx). A nested generic's arguments join with underscores. A combination no construction has is refused, its message listing the constructions. In the stable build a family holds its stable constructions alone, and an experimental one is refused, its message naming the experimental build. - What feeds back: an Excel error inside a range is refused, as an error argument passes through whole. So a returned none (#N/A), or a jagged spill's padding, fed back into another function's range refuses. A formula can't return an empty cell, so a computed range can't say none.
- A period in hours or minutes, which a few results carry, shows as
"1h": text the period's parser, which reads days, weeks, months and years, doesn't take back. - A file's path in a cell must be absolute (
nqlConventionsXmlRead's).
Looping over a range
A range where a function takes one value is looped over: the function is called once per element, and the results spill in the range's shape, as Excel lifts its own functions.
=nqlYieldTermStructureDiscount(curve, A3:A12) ten discount factors, down a column
=nqlYieldTermStructureForwardRate2(curve, A3:A5, {"1M","3M","6M"}, dayCounter, "Compounded", "Annual")
a 3 x 3 grid of forward rates' handles
=nqlDepositRateHelper(B1:B3, "3M", 2, "TARGET", "ModifiedFollowing", FALSE, "Actual360", ctx)
three deposits, one per rate
- Which arguments loop: every argument of a kind one cell holds: a number, a whole number, a boolean, text, a date,
a period, an enum, a complex number, a handle, a snapshot, a bound engine, and their optional forms. An array,
matrix, vector, jagged or map argument keeps its range. A family's type arguments don't loop, and a function whose
result is itself a range doesn't loop, since Excel spills no array of arrays. A 1 × 1 range in a looped place is its
value.
functions.jsongives each function's mask, a1per looping argument. - Several ranges pair by Excel's rule: a single row or column repeats, and a position past a shorter range's end is #N/A.
- Each element is the single call, keyed as it is: three deposits from one formula are the three cells' deposits, and a curve built from them is the same curve.
- A failed element shows its error in its place, the others their values.
- Messages by cell:
- the formula's cell gets the first failed element's message, with its place ("row 2, column 1"), whether the looped
call is the formula's value or nested in it (
=SUMPRODUCT(nqlYieldTermStructureDiscount(curve, A3:A12), B3:B12)); nqlxlErroron a spilled cell gives its element's message when the spill shows the looped call element by element, and nothing for an element showing a value;- a spill that reorders the elements (
INDEXover a reversedSEQUENCE) gives the formula's message; - a cell outside the spill keeps its own call's message.
- the formula's cell gets the first failed element's message, with its place ("row 2, column 1"), whether the looped
call is the formula's value or nested in it (
- Release: each looped object is held by its cell. Clearing the cell releases them all, shrinking its range releases the elements it leaves, and an element whose input changes releases its old object.
Names for calendars and day counters
Text where a Calendar or a DayCounter goes names it in the current convention set, as an empty set argument reads
it (The context and the conventions, below):
=nqlDepositRateHelper(0.03, "6M", 2, "TARGET", "ModifiedFollowing", FALSE, "Actual360", ctx)
=nqlJointCalendar("TARGET", "UnitedStates.Settlement")
- The names: the set's own and its aliases, FpML's codes (
EUTAis TARGET,ACT/360is Actual360). A name is as the set spells it:"target"is #VALUE!. - What isn't a name:
- a name the set doesn't know is #VALUE!,
nqlxlErrornaming it; - a handle's text stays a handle, #REF! when this session doesn't hold it;
- a name where another type goes stays a missing handle, #REF!.
- a name the set doesn't know is #VALUE!,
- Under a desk's conventions (the settings file), a name is the desk's calendar.
- An array of calendars takes names too.
- Indexes stay handles: the set's index needs a curve:
=nqlConventionSetIborIndex2(set, "Euribor", "6M", curve).
Handles
An object is held by the add-in and shown in its cell as a handle, <Type>#<16 hexadecimal digits>, such as
FlatForward#B4D02F06C4DB1663; any cell passes the handle to another function.
The key: the call itself:
- the function's name;
- each argument's value (numbers by their bits, text, booleans, a missing argument, a range's shape and cells), each handle argument by its own key;
- the workbook's date system;
- the convention provider in effect.
Equal calls give one object, in any cell, sheet or workbook. The 16 digits are the key's SHA-256; an object whose 16 digits meet another's shows 32.
A call that reads files (
nqlConventionsXmlRead,nqlConventionsXmlRead2) keys their content beside their paths. An edited file gives a new object when its cell recalculates: F2 and Enter on the cell, or Ctrl+Alt+F9 for the whole workbook. Neither function is volatile, so a plain recalculation doesn't reread the file. One content read from two paths is two objects, each reporting its own path.Lifetime: an object lives while some cell holds its handle, through Excel's RTD topics, and is released when the last one goes. An object inside a returned array (a leg's coupons) is keyed by its parent call and its position, and goes with its parent's cell.
Saving and reopening: a saved workbook's handle cells recalculate when it opens. Its handles are rebuilt, the same texts as saved, and every later edit finds them.
Threads: every generated function is thread-safe, objects are built once per key, and recalculation runs on all of Excel's threads.
Across workbooks: a handle referenced from another workbook works while some cell holds it.
Errors
| Excel shows | When |
|---|---|
| #VALUE! | the arguments are refused: a wrong value or type, an unknown name, an unreadable or malformed file, a feature not supported, a bound engine refused, the add-in's conventions failing to load |
| #NUM! | QuantLib's own checks and failed calculations, anything else, and a non-finite result |
| #REF! | a handle this Excel session doesn't hold |
| #N/A | no value: a null result, an empty collection |
Excel's own errors pass through: an error from Excel in an argument returns as itself, as Excel's own functions pass it.
The message:
=nqlxlError(cell)gives the failed call's message, with the parameter it names and the exception's kind, while the cell shows the error:=nqlFlatForward(A1, 0.03, A14) #VALUE!, A14 holding a calendar =nqlxlError(B5) nqlFlatForward: ArgumentException: dayCounter: expected a DayCounter, got a …The codes: each exception takes the C interface's error code first (bindings.md), then the code its Excel error, so the add-in and the bindings classify a failure alike.
The context and the conventions
The evaluation context is a handle:
=nqlEvaluationContext(date), and aWith<Field>per field (nqlEvaluationContextWithFixings), passed to every function that takes one. There is no today default.Conventions through a set:
nqlConventionSetQuantLib(), QuantLib's own;nqlConventionsXmlRead(path)andnqlConventionSetWith(set, snapshot), a conventions file's (conventions.md);- the presets by name:
=nqlConventionSetIborIndex2(set, "Euribor", "6M", curve).
An empty set argument reads the current conventions: the add-in's default, or QuantLib's.
The settings file: each build reads its own,
<add-in name>.settings.jsonbeside the.xll(nanoql64.settings.json,nanoql-experimental64.settings.json), when Excel loads the add-in:{ "conventions": "desk-conventions.xml", "recordCalls": true }"conventions": a conventions file, relative to the settings file's folder, loaded as the default conventions."recordCalls": whether each call is recorded for export, on by default.- The environment variable
NANOQL_CONVENTIONSoverrides"conventions", a relative path resolving against the.xll's folder. - No worksheet function sets the default conventions: a setting written by one cell and read by others would make a result depend on Excel's calculation order.
A failed load never falls back to QuantLib's conventions: a missing or malformed conventions file, an unknown key (a key in another case included) or a value of the wrong type each refuse every lookup on the default conventions. Every function reading them returns #VALUE!,
nqlxlErrorgiving the load's error, while a function given an explicit set still works.The provider in effect enters every handle's key: QuantLib's, the loaded file's content, or the failed load.
Bound engines
An engine is bound in a cell with its inputs and given where a member takes one, as the bindings do (bindings.md):
=nqlDiscountingSwapEngineBind(curve) a bound engine, every input optional
=nqlMakeVanillaSwapWithPricingEngine(builder, B1) the builder solves its fixed rate through it
The member's own arguments are placed by role, and the bound inputs fill the rest. An engine of the wrong shape, an input both bound and supplied, or an engine without the member's calculation is refused, #VALUE!. Functions of the caller's own (solvers, integrators, quotes computed by a formula) stay out of 2.0, as in every binding.
Users' assemblies
A user's own assembly built on nanoql gets its worksheet functions in the same add-in as nanoql's, so its objects and nanoql's meet:
nanoql-bind excel build --assembly nql=NanoQL.dll --assembly nql=NanoQL.Extensions.dll --assembly acme=Acme.dll --out x
- Names: each assembly's functions take its prefix:
=acmeExponentialCurve(A1, 0.03, 0.001, A3)builds the user's curve, and nanoql's=nqlYieldTermStructureDiscount2(A4, 5)prices it. The same naming, freezing and budget rules apply as for nanoql's own. - The build: the stable API and the user's assemblies, unless
--experimentalis given. nanoql's list applies to nanoql's assemblies; a user's assembly is generated whole unless--list <prefix>=<file>gives it a list. - What the check withholds: a user's function whose required input only a type off nanoql's list makes is withheld
as nanoql's are. The tool prints each one with the types it lacks and the remedy:
--list nql=<file>, with the type added. - The runtime package: a user's build restores the runtime,
NanoQL.Excel, from NuGet at the tool's version. - One add-in per Excel process: each Excel-DNA add-in loads its assemblies into a context of its own, so two add-ins' objects are different types and can't meet. A desk's add-in holds every assembly its workbooks price with.
Repro export
=nqlxlRepro(cell, format) gives the nanoql calls behind a cell, a line per row:
"vector", the default: a test vector in the vector suite's envelope. It holds each call's worksheet function and the Excel values it was given: a range with its shape and its empty cells, a handle as a reference to the call that made it, an object inside a returned array as its parent call's item. Beside them: the provider, the calls' date system, and the conventions file and every file a call read, embedded. The expected outcome is the cell's own: a spill's whole value, a handle's text (its type alone where a file was read),throwsfor a failed call,nullfor no value, a non-finite number's#NUM!."csharp": a C# program making the same calls through nanoql alone. Compiled againstNanoQL.dllandNanoQL.Extensions.dll, it prints the cell's outcome as a grid of cells."python": the same over nanoql's Python package and NumPy.
A looped call's C# and Python loop over its ranges, each element guarded as Excel shows a failed element; a name where a calendar or a day counter goes is the convention set's lookup. The date system is the calls' own: a 1904 workbook's cell exported from a 1900 workbook gives a 1904 vector.
The export refuses, #VALUE! with nqlxlError giving why:
- a cell whose value isn't its call's outcome: a scaled cell, or a cell edited to a constant;
- a call given an Excel error: export the cell that gave it;
- an object inside a returned map or tuple;
- calls made in workbooks of both date systems;
- a call reporting a read file's path (
nqlConventionSnapshotSources,nqlConventionSetProvenance), which a replay reading the file from a temporary path can't reproduce.
With "recordCalls": false in the settings, a value cell can't export; a handle cell still does, from its handle's
recipe.
The replayer runs a folder of vectors through a tests' build, each on its own (a fresh store, the vector's conventions, date system and files), every number within the vector suite's tolerance:
dotnet run --project bindings/excel/replay -c Release -- vectors <folder> <the experimental tests' build> [--reverse]
An export as a test: commit the vector as bindings/excel/tests/repro/<name>.json and the workbook it was
exported from as bindings/excel/tests/repro/workbooks/<name>.xlsx, its read files under workbooks/files/. A
workbook holds each call in a cell of its sheet Calls and the export in its sheet Repro; a read file's path is
built from the workbook's own folder (its name Here), so it reads its files wherever the checkout is. The read files
are kept as written, their line endings included (-text in .gitattributes), since the vector embeds their bytes.
CI replays every committed vector; the Excel tests make each one's calls again and export them, and render it as C#
and run it; the suite inside Excel exports it again from its workbook. Each comparison leaves out the generator's
version and the checkout's own path.
Debugging
nanoql's symbols ship apart from the add-ins, in nanoql-excel-<version>-win-x64-symbols.zip: NanoQL.pdb and
NanoQL.Extensions.pdb, with nanoql's source embedded. To stop in nanoql's source from a cell in Visual Studio:
- Give it the symbols' folder once: Tools > Options > Debugging > Symbols.
- Turn on Suppress JIT optimization on module load and turn off Enable Just My Code (Tools > Options > Debugging > General). The release build's code is optimized: without the first, a breakpoint may bind and never stop.
- Attach to Excel before the add-in loads (Debug > Attach to Process, Managed (.NET Core) code), or start Excel from
Visual Studio with the
.xllas its argument.
In the repository, python -I bindings/excel/xltests/stepin.py [--netcoredbg <netcoredbg.exe>] checks the way in on
nanoql's Debug build: it builds an add-in over it, nanoql's PDB left apart, attaches netcoredbg 3.2.0 to an Excel of its
own, and stops in FlatForward's discount from a cell's call, the generated worksheet function a frame above.
Checking the add-in
| Check | Command | Where |
|---|---|---|
| The surface | dotnet run --project src/NanoQL.Bind -c Release -- excel check |
CI |
| The back end | dotnet test --project bindings/NanoQL.Bind.Tests -c Release: names, the list, the fold, the forms' code, the builds |
CI |
| The runtime | dotnet test --project bindings/excel/NanoQL.Excel.Tests -c Release: the conversions and forms, keys, errors, settings, looping and messages, through a test host standing in for Excel, on the tests' builds; and the example workbooks' formulas, every function they call listed and stable |
CI |
| The replay | dotnet run --project bindings/excel/replay -c Release -- <trace> <build folder> --floor bindings/excel/replay-floor.json |
CI |
| The repro vectors | dotnet run --project bindings/excel/replay -c Release -- vectors bindings/excel/tests/repro <build folder>, then with --reverse |
CI |
| The package | dotnet run --project src/NanoQL.Bind -c Release -- excel package --runtime-source <feed> --out <folder> |
CI |
| Inside Excel | python -I bindings/excel/xltests/run.py [--python <interpreter>] |
a machine with Excel |
| Stepping into nanoql | python -I bindings/excel/xltests/stepin.py [--netcoredbg <netcoredbg.exe>] |
a machine with Excel |
The tests' builds hold nanoql's whole surface with the list off, compiled with no
.xll:--list nql=none --no-pack, with--experimentalfor one of them. Every listed function is generated in them byte for byte, so what they pass the shipped builds pass, and the kinds the list doesn't reach today stay tested.The replay: the vector suite's recorded trace (bindings.md) through the experimental tests' build's worksheet functions, outside Excel, each result against C#'s, bit for bit. A fact stops at an input no cell gives: NaN, a whole number past 2^53, none where the default isn't none, a period in hours, an empty matrix, an enum value no member has.
The suite inside Excel (
bindings/excel/xltests/, ExcelDna.Testing on xUnit 2, with its ownglobal.json) runs on the shipped builds, a test build and a user's build, in Excel instances of its own that load the add-ins for their session alone. It checks:- loading, each build's registered functions its surface's;
- a desk's tasks to their bits; bound engines in cells; handles across saving and reopening, and in both date systems; edited conventions files and the settings' failed loads;
- errors and their messages; 2,000 curves on all of Excel's threads; the forms in real ranges; a user's assembly;
- looping and its messages by cell, names for calendars and day counters, and objects released with their cells;
- repro export: a desk's swap exported in each format, the vector replayed, the C# compiled and run, the Python run
(with
--python, an interpreter with nanoql's Python package and NumPy), each to the cell's bits; every committed vector the build holds exported again from its workbook; the export's refusals; - the package's stable add-in, from its zip, priced;
- the example workbooks, each opened on the stable add-in and recalculated whole, its named results C#'s bits.
Its wrapper builds the add-ins first. It checks that the run leaves the machine's other Excel processes and Excel's settings as it found them; it puts back Excel's window position, which Excel rewrites when a test closes a second workbook, and says so.