diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e535ebc2..9c674cc2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/guide/custom-functions.md b/docs/guide/custom-functions.md index 5c68d8c11..5f14535bd 100644 --- a/docs/guide/custom-functions.md +++ b/docs/guide/custom-functions.md @@ -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`: diff --git a/src/interpreter/plugin/AddressPlugin.ts b/src/interpreter/plugin/AddressPlugin.ts index 7e37a42e4..2542bb1fb 100644 --- a/src/interpreter/plugin/AddressPlugin.ts +++ b/src/interpreter/plugin/AddressPlugin.ts @@ -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}, ] }, } diff --git a/src/interpreter/plugin/FunctionPlugin.ts b/src/interpreter/plugin/FunctionPlugin.ts index 810f14047..6ca652fd1 100644 --- a/src/interpreter/plugin/FunctionPlugin.ts +++ b/src/interpreter/plugin/FunctionPlugin.ts @@ -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 @@ -471,7 +491,10 @@ export abstract class FunctionPlugin implements FunctionPluginTypecheck