Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

- Fixed the MAXPOOL and MEDIANPOOL functions throwing an uncaught `TypeError` instead of returning the `#VALUE!` error when the range dimensions are not a whole multiple of the window size and the stride. [#1718](https://github.com/handsontable/hyperformula/pull/1718)
- Fixed the `MOD` function returning a remainder with the sign of the dividend instead of the sign of the divisor, which made the results differ from Excel and Google Sheets for arguments with opposite signs (e.g. `=MOD(-3, 12)` now returns `9` instead of `-3`). [#1747](https://github.com/handsontable/hyperformula/issues/1747)
- Fixed the `ADDRESS` function to omit the sheet separator (`!`) when the `sheetName` argument is left out of the formula — whether omitted entirely (`=ADDRESS(2,3,1,FALSE())`) or written as an empty argument slot (`=ADDRESS(2,3,1,FALSE(),)`) — in both A1 and R1C1 styles, matching Microsoft Excel. A `sheetName` that is present but empty (an explicit `""`, or a reference to an empty cell) keeps the separator, which also matches Excel. [#1739](https://github.com/handsontable/hyperformula/pull/1739)

### Added

- Added the `emptyAsAbsent` argument-validation option for [custom functions](https://hyperformula.handsontable.com/docs/guide/custom-functions.html): an empty argument slot is passed to the implementation as `undefined` instead of the zero-value for its type, which lets a function tell an absent argument apart from one whose value is empty. [#1739](https://github.com/handsontable/hyperformula/pull/1739)

## [3.4.0] - 2026-08-10

Expand Down
1 change: 1 addition & 0 deletions docs/guide/custom-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,7 @@ You can set the following argument validation options:
| `lessThan` | Number | If set: numerical argument needs to be less than `lessThan`. |
| `greaterThan` | Number | If set: numerical argument needs to be greater than `greaterThan`. |
| `emptyAsDefault` | Boolean | `true`: 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. |
| `emptyAsAbsent` | Boolean | `true`: an empty argument (e.g., `=FUNC(1,,3)`) is passed to the function as `undefined`, exactly as if it had not been written at all. Use it when the function must distinguish an absent argument from one whose value is empty — `ADDRESS` does, because Excel omits the `!` separator only for an absent sheet name. Applies only when `defaultValue` is unset, so it never competes with `emptyAsDefault`. |

In your function plugin, in the static `implementedFunctions` property, add an
array called `parameters`:
Expand Down
2 changes: 1 addition & 1 deletion src/interpreter/plugin/AddressPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class AddressPlugin extends FunctionPlugin implements FunctionPluginTypec
{argumentType: FunctionArgumentType.NUMBER},
{argumentType: FunctionArgumentType.NUMBER, optionalArg: true, defaultValue: 1, minValue: 1, maxValue: 4, emptyAsDefault: true},
{argumentType: FunctionArgumentType.BOOLEAN, optionalArg: true, defaultValue: true, emptyAsDefault: true},
{argumentType: FunctionArgumentType.STRING, optionalArg: true},
{argumentType: FunctionArgumentType.STRING, optionalArg: true, emptyAsAbsent: true},
]
},
}
Expand Down
25 changes: 24 additions & 1 deletion src/interpreter/plugin/FunctionPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,26 @@ export interface FunctionArgument {
* Requires `defaultValue` to be set.
*/
emptyAsDefault?: boolean,

/**
* If set to `true`, a syntactically empty argument is treated as if the argument had
* not been provided at all: the function implementation receives `undefined`.
*
* This differs from {@link emptyAsDefault}, which substitutes `defaultValue`. It is the
* only way to tell "no argument was given" apart from "an argument was given and its
* value is empty" — a distinction Microsoft Excel makes for `ADDRESS`, where an absent
* sheet name omits the `!` separator while an explicitly empty one keeps it.
*
* | Formula | `emptyAsAbsent: false` (default) | `emptyAsAbsent: true` |
* |------------------------------|----------------------------------|-------------------------|
* | `ADDRESS(2,3,1,FALSE())` | `undefined` for 5th arg | `undefined` for 5th arg |
* | `ADDRESS(2,3,1,FALSE(),)` | `""` for 5th arg | `undefined` for 5th arg |
* | `ADDRESS(2,3,1,FALSE(),"")` | `""` for 5th arg | `""` for 5th arg |
*
* Does not require `defaultValue`; it applies only when `defaultValue` is unset, so it
* never competes with {@link emptyAsDefault}.
*/
emptyAsAbsent?: boolean,
}

export type PluginFunctionType = (ast: ProcedureAst, state: InterpreterState) => InterpreterValue
Expand Down Expand Up @@ -471,7 +491,10 @@ export abstract class FunctionPlugin implements FunctionPluginTypecheck<Function
for (let i = 0; i < argumentsMetadata.length; i++) {
const argumentMetadata = argumentsMetadata[i]
const rawArg = vectorizedArguments[i]
const argumentValue = rawArg === undefined
const emptySlotIsAbsent = syntacticallyEmptyFlags[i]
&& argumentMetadata?.emptyAsAbsent === true
&& argumentMetadata?.defaultValue === undefined
const argumentValue = rawArg === undefined || emptySlotIsAbsent
? argumentMetadata?.defaultValue
: (syntacticallyEmptyFlags[i] && argumentMetadata?.emptyAsDefault && argumentMetadata?.defaultValue !== undefined)
? argumentMetadata.defaultValue
Expand Down
Loading