Skip to content
v3.3.0

Custom functions

Expand the function library of your application by adding custom functions.

As an example, let’s create a custom function GREET that accepts a person’s first name as a string argument and returns a personalized greeting.

Import FunctionPlugin, and extend it with a new class. For example:

import { FunctionPlugin } from 'hyperformula';
// let's call the function plugin `MyCustomPlugin`
export class MyCustomPlugin extends FunctionPlugin {}

2. Define your function’s ID, method, and metadata

Section titled “2. Define your function’s ID, method, and metadata”

In your function plugin, in the static implementedFunctions property, define an object that declares the functions provided by this plugin.

The name of that object becomes the ID by which translations, aliases, and other elements reference your function. Make the ID unique among all HyperFormula functions (built-in and custom).

In your function’s object, you can specify:

  • A method property (required), which maps your function to the implementation method (we’ll define it later on),
  • A parameters array that describes the arguments accepted by your function and validation options for each argument,
  • Other custom function options.
import { FunctionPlugin, FunctionArgumentType } from 'hyperformula';
MyCustomPlugin.implementedFunctions = {
// let's define the function's ID as `GREET`
GREET: {
method: 'greet',
parameters: [{ argumentType: FunctionArgumentType.STRING }],
},
};

In a separate object, define your function’s names in every language that you want to support.

export const MyCustomPluginTranslations = {
enGB: {
GREET: 'GREET',
},
enUS: {
GREET: 'GREET',
},
// repeat for all languages used in your system
};

In your function plugin, add a method that implements your function’s calculations. Your method needs to:

  • Take two optional arguments: ast and state.
  • Return the results of your calculations.

Wrap your implementation in the built-in runFunction() method, which:

export class MyCustomPlugin extends FunctionPlugin {
greet(ast, state) {
return this.runFunction(
ast.args,
state,
this.metadata('GREET'),
(firstName) => {
return `👋 Hello, ${firstName}!`;
}
);
}
}

Register your function plugin and its translations so that HyperFormula can recognize it. You need to do this before you create your HyperFormula instance.

Use the registerFunctionPlugin() method:

HyperFormula.registerFunctionPlugin(MyCustomPlugin, MyCustomPluginTranslations);

Now, you’re ready to use your GREET function in a formula.

// build a HyperFormula instance where you can use your function directly
const hfInstance = HyperFormula.buildFromArray([['Anthony', '=GREET(A1)']]);
// read the value of cell B1
const result = hfInstance.getCellValue({ sheet: 0, col: 1, row: 0 });
// cell B1 should evaluate to 'Anthony'
console.log(result);

The complete implementation of this custom function is also included in the demo.

import { FunctionPlugin, FunctionArgumentType } from 'hyperformula';
export class MyCustomPlugin extends FunctionPlugin {
greet(ast, state) {
return this.runFunction(
ast.args,
state,
this.metadata('GREET'),
(firstName) => {
return `👋 Hello, ${firstName}!`;
}
);
}
}
MyCustomPlugin.implementedFunctions = {
GREET: {
method: 'greet',
parameters: [{ argumentType: FunctionArgumentType.STRING }],
},
};
export const MyCustomPluginTranslations = {
enGB: {
GREET: 'GREET',
},
enUS: {
GREET: 'GREET',
},
};
HyperFormula.registerFunctionPlugin(MyCustomPlugin, MyCustomPluginTranslations);

In a more advanced example, we’ll create a custom function DOUBLE_RANGE that takes a range of numbers and returns the range of the same size with all the numbers doubled.

To accept a range argument, declare it in the parameters array:

MyCustomPlugin.implementedFunctions = {
DOUBLE_RANGE: {
method: 'doubleRange',
parameters: [{ argumentType: FunctionArgumentType.RANGE }],
},
};

The range arguments are passed to the implementation method as instances of the SimpleRangeValue class:

export class MyCustomPlugin extends FunctionPlugin {
doubleRange(ast, state) {
return this.runFunction(
ast.args,
state,
this.metadata('DOUBLE_RANGE'),
(range) => {
const rangeData = range.data;
// ...
}
);
}
}

A function can return multiple values in the form of an array. To do that, use SimpleRangeValue class:

export class MyCustomPlugin extends FunctionPlugin {
doubleRange(ast, state) {
return this.runFunction(
ast.args,
state,
this.metadata('DOUBLE_RANGE'),
(range) => {
const resultArray = //...
return SimpleRangeValue.onlyValues(resultArray);
},
);
}
}

A function that returns an array will cause the VALUE! error unless you also declare a companion method for the array size. To do that, provide the sizeOfResultArrayMethod that calculates the size of the result array based on the function arguments and returns an instance of the ArraySize class.

export class MyCustomPlugin extends FunctionPlugin {
doubleRangeResultArraySize(ast, state) {
const arg = ast?.args?.[0];
if (arg?.start == null || arg?.end == null) {
return ArraySize.scalar();
}
const width = arg.end.col - arg.start.col + 1;
const height = arg.end.row - arg.start.row + 1;
return new ArraySize(width, height);
}
}
MyCustomPlugin.implementedFunctions = {
DOUBLE_RANGE: {
method: 'doubleRange',
sizeOfResultArrayMethod: 'doubleRangeResultArraySize',
parameters: [{ argumentType: FunctionArgumentType.RANGE }],
},
};

Validate the arguments and return an error

Section titled “Validate the arguments and return an error”

To handle invalid inputs, the custom function should return an instance of the CellError class with the relevant error type. Errors are localized according to your language settings.

if (rangeData.some((row) => row.some((val) => typeof rawValue !== 'number'))) {
return new CellError(
'VALUE',
'Function DOUBLE_RANGE operates only on numbers.'
);
}

To make sure your function works correctly, add unit tests. Use a JavaScript testing library of your choice.

it('works for a range of numbers', () => {
HyperFormula.registerFunctionPlugin(
MyCustomPlugin,
MyCustomPluginTranslations
);
const engine = HyperFormula.buildFromArray(
[[1, '=DOUBLE_RANGE(A1:A3)'], [2], [3]],
{ licenseKey: 'gpl-v3' }
);
expect(engine.getCellValue({ sheet: 0, row: 0, col: 1 })).toEqual(2);
expect(engine.getCellValue({ sheet: 0, row: 1, col: 1 })).toEqual(4);
expect(engine.getCellValue({ sheet: 0, row: 2, col: 1 })).toEqual(6);
});
it('returns a VALUE error if the range argument contains a string', () => {
HyperFormula.registerFunctionPlugin(
MyCustomPlugin,
MyCustomPluginTranslations
);
const engine = HyperFormula.buildFromArray(
[[1, '=DOUBLE_RANGE(A1:A3)'], ['I should not be here'], [3]],
{ licenseKey: 'gpl-v3' }
);
expect(engine.getCellValueType({ sheet: 0, row: 0, col: 1 })).toEqual(
'ERROR'
);
expect(engine.getCellValue({ sheet: 0, row: 0, col: 1 }).value).toEqual(
'#VALUE!'
);
});

Explore the full working example on Stackblitz.

This demo contains the implementation of both the GREET and DOUBLE_RANGE custom functions.

You can set the following options for your function:

OptionTypeDescription
method (required)StringName of the method that implements the custom function logic.
parametersArraySpecification of the arguments accepted by the function and their validation options.
sizeOfResultArrayMethodStringName of the method that calculates the size of the result array. Not required for functions that never return an array.
returnNumberTypeStringIf the function returns a numeric value, this option indicates how to interpret the returned number.
Possible values: NUMBER_RAW, NUMBER_DATE, NUMBER_TIME, NUMBER_DATETIME, NUMBER_CURRENCY, NUMBER_PERCENT.
Default: NUMBER_RAW
repeatLastArgsNumberFor functions with a variable number of arguments: sets how many last arguments can be repeated indefinitely.
Default: 0
expandRangesBooleantrue: ranges in the function’s arguments are inlined to (possibly multiple) scalar arguments.
Default: false
isVolatileBooleantrue: the function is volatile.
Default: false
isDependentOnSheetStructureChangeBooleantrue: the function gets recalculated with each sheet shape change (e.g., when adding/removing rows or columns).
Default: false
doesNotNeedArgumentsToBeComputedBooleantrue: the function treats reference or range arguments as arguments that don’t create dependency (other arguments are properly evaluated).
Default: false
enableArrayArithmeticForArgumentsBooleantrue: the function enables the array arithmetic mode in its arguments and nested expressions.
Default: false
vectorizationForbiddenBooleantrue: the function will never get vectorized.
Default: false
arraySizeMethodStringDeprecated; Use sizeOfResultArrayMethod instead.
arrayFunctionBooleanDeprecated; Use enableArrayArithmeticForArguments instead.

You can set the options in the static implementedFunctions property of your function plugin:

MyCustomPlugin.implementedFunctions = {
MY_FUNCTION: {
method: 'myFunctionMethod',
parameters: [
{
// your argument validation options
},
],
sizeOfResultArrayMethod: 'myArraySizeMethod',
returnNumberType: 'NUMBER_RAW',
repeatLastArgs: 0,
expandRanges: false,
isVolatile: false,
isDependentOnSheetStructureChange: false,
doesNotNeedArgumentsToBeComputed: false,
enableArrayArithmeticForArguments: false,
vectorizationForbidden: false,
},
};

You can set the following argument validation options:

OptionTypeDescription
argumentType (required)FunctionArgumentTypeExpected type of the function argument. See possible values.
defaultValueInternalScalarValue or RawScalarValueIf set: if an argument is missing, its value defaults to defaultValue.
passSubtypeBooleantrue: arguments are passed with full type information (e.g., for numbers: Date or DateTime or Time or Currency or Percentage).
Default: false
optionalArgBooleantrue: if an argument is missing, and no defaultValue is set, the argument defaults to undefined (instead of throwing an error).
Default: false
Setting this option to true is the same as setting defaultValue to undefined.
minValueNumberIf set: numerical arguments need to be greater than or equal to minValue.
maxValueNumberIf set: numerical arguments need to be less than or equal to maxValue.
lessThanNumberIf set: numerical argument needs to be less than lessThan.
greaterThanNumberIf set: numerical argument needs to be greater than greaterThan.
emptyAsDefaultBooleantrue: an empty argument (e.g., =FUNC(1,,3)) is treated as missing and falls back to defaultValue. By default (false), empty arguments are coerced to the zero-value for their type (0, FALSE, or ""). Requires defaultValue to be set.

In your function plugin, in the static implementedFunctions property, add an array called parameters:

MyCustomPlugin.implementedFunctions = {
MY_FUNCTION: {
method: 'myFunctionMethod',
parameters: [
{
argumentType: FunctionArgumentType.STRING,
defaultValue: 10,
passSubtype: false,
optionalArg: false,
minValue: 5,
maxValue: 15,
lessThan: 15,
greaterThan: 5,
},
],
},
};
TypeDescriptionExample
NUMBERA general numeric value such as floating-point number, date/time value, currency value or percent value.3, 3.14, $100, 1939/09/01, 4:45 AM
INTEGERAn integer.42
COMPLEXA text representing a complex value."-3+4i"
STRINGA text value."aaa"
BOOLEANA logical value.=TRUE()
NOERRORAny non-range and non-error value.All of the above
SCALARAny non-range value.All of the above
RANGEMultiple values as a range of cells or an inline array.A1:B100, {1, 2}
ANYAny value.All of the above

Both the defaultValue and optionalArg options let you decide what happens when a user doesn’t pass enough valid arguments to your custom function.

Setting a defaultValue for an argument always makes that argument optional. But, the defaultValue option automatically replaces any missing arguments with defaultValue, so your custom function is unaware of the actual number of valid arguments passed.

If you don’t want to set any defaultValue (because, for example, your function’s behavior depends on the number of valid arguments passed), use the optionalArg setting instead.

You can add translations of your function’s name in multiple languages. Your end users use the translated names to call your function inside formulas.

In a separate object, define the translations of your custom functions’ names in every language you want to support. Function names are case-insensitive, as they are all normalized to uppercase.

export const MyCustomPluginTranslations = {
enGB: {
// formula in English: `=MY_FUNCTION()`
MY_FUNCTION: 'MY_FUNCTION',
},
deDE: {
// formula in German: `=MEINE_FUNKTION()`
MY_FUNCTION: 'MEINE_FUNKTION',
},
// repeat for all languages used in your system
};
// register your function plugin and translations
HyperFormula.registerFunctionPlugin(MyCustomPlugin, MyCustomPluginTranslations);

You can also assign multiple aliases to a single custom function.

In your function plugin, in the static aliases property, add aliases for your function:

MyCustomPlugin.aliases = {
// `=MY_ALIAS()` will work the same as `=MY_FUNCTION()`
MY_ALIAS: 'MY_FUNCTION',
};