Custom functions
Functions written in C# that formulas call by name, in place of VBA functions or add-ins that a workbook expects.
Adding a function
workbook.Functions.Add("VAT", args =>
{
CellValue amount = args.Number(0); // converted as Excel converts: "100" is 100
if (amount.IsError)
return amount; // pass #VALUE! and other errors on
return amount.AsNumber() * 0.2;
}, new FunctionOptions { MinArguments = 1, MaxArguments = 1, ScalarParameters = [0] });
sheet["A1"].Value = 100;
sheet["A2"].Value = 250;
sheet["B1"].Formula = "=VAT(A1)"; // 20
sheet["C1"].Formula = "=VAT(A1:A2)"; // a scalar parameter given a range: spills 20 and 50
workbook.Recalculate();
Workbook.Functions.Add takes a name, a body and options. Formulas call the function ignoring
case, and can pass it as a value where a LAMBDA is expected, as in MAP(A1:A9, VAT). Formulas that were #NAME? because the function was missing are calculated again by the
next Recalculate; Remove turns its calls back into #NAME?. Each workbook has its own
functions, and Workbook.Clone copies them.
The name has letters, digits, . and _, and starts with a letter or _. The name of a function
SharpCell implements is refused: their results are checked against Excel. A name Excel knows but
SharpCell does not implement, such as WEBSERVICE, can be added. In files, Excel writes calls of
add-in functions as _xll.NAME(...) and calls of functions it does not know as
_xludf.NAME(...); both find the function NAME.
Arguments
| Option | Meaning |
|---|---|
MinArguments, MaxArguments |
How many arguments a call may have, 0 to 255. A call outside the range is #VALUE!, as with Excel's functions. |
ScalarParameters |
Positions (from 0) of parameters that take one value. Given a range or an array, the function runs once per element and the results spill, as LEN(A1:A10) does. |
IsVolatile |
The function runs on every Recalculate, like NOW, not only when its arguments change. |
FunctionArguments gives the body the call's arguments:
args[i]is the value: a single cell's value, a range as aCellValueKind.Array, an error as an error value (the function decides what to do with it), andCellValue.Missingfor an argument left out, as the middle one ofF(1,,2).Number(i),Text(i)andBoolean(i)convert the argument as Excel does and return either the converted value or the error to return, such as#VALUE!for"abc"read as a number.Calleris the cell whose formula makes the call; null forWorkbook.Evaluate.DateSystemandCultureare the workbook's, for dates and text.
A parameter that is not scalar receives a range as an array of its values:
workbook.Functions.Add("SUMSQUARES", args =>
{
double total = 0;
foreach (CellValue value in args[0].Kind == CellValueKind.Array ? args[0].AsArray() : new[,] { { args[0] } })
{
if (value.Kind == CellValueKind.Number)
total += value.AsNumber() * value.AsNumber();
}
return total;
}, new FunctionOptions { MinArguments = 1, MaxArguments = 1 });
sheet["B1"].Formula = "=SUMSQUARES(A1:A3)"; // the range arrives as an array: 25
Results and failures
The body returns a CellValue. An array result spills like any dynamic array. Missing and
lambda results are #VALUE!.
An exception thrown by the body makes the cell #VALUE! and adds a diagnostic of kind
DiagnosticKind.CustomFunctionFailure, naming the function and the exception. The rest of the
workbook calculates as usual. An OperationCanceledException cancels the calculation only when
its token was cancelled; otherwise it is a failure like any other exception.
Rules for the body
- Read cells only through the arguments. SharpCell records which cells a formula reads, to
know what to calculate again. A cell the body reads on its own, such as
args.Caller!.Worksheet["Z1"].Value, is not recorded: the formula is not updated when it changes. Pass it as an argument, or make the function volatile. - Do not change the workbook. Setting cells, defining names, adding sheets or functions, and
calling
RecalculateorEvaluatefrom inside a body throwInvalidOperationException, which turns into#VALUE!and a diagnostic. - Keep the arguments to the call.
FunctionArgumentsis valid only while the body runs. An array it gives is a copy, which the body may change. - Expect extra calls. When a formula's inputs are out of date, SharpCell may call the body before calculating them, with stand-in values, and throw that result away. A body without side effects does not notice; one that calls a service should cache or tolerate it.
- Stop when cancelled.
args.CancellationTokenis the token given toRecalculate; a slow body can throwOperationCanceledExceptionthrough it to cancel the calculation. - Clones share the body.
Workbook.Clonecopies the delegate, not what it captures: a body used by clones on several threads must be thread-safe.
A workbook whose formulas use custom functions saves with XlsxWriter.Save like any other: the
results are calculated by SharpCell, so KeepUncalculated is not needed.