From deb5b76e1b840772325440acb7bb76bd577855e4 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Fri, 14 Aug 2026 09:16:24 +0000 Subject: [PATCH 1/3] HF-160: ADDRESS drops the sheet separator when sheetName is empty =ADDRESS(2,3,1,FALSE(),) returned '!R2C3' - the '!' separator was emitted for an empty-string sheet name. Excel returns 'R2C3'. One condition: an empty string no longer produces a sheet prefix, for both A1 and R1C1 styles; non-empty sheet names are untouched. Implemented by a prep-ship lane (task HF-160); the loop could not see its own green (harness path bug, prep-ship#1 follow-up) so verified here: authored spec 37/37, unit/interpreter 390 suites / 3531 tests green, tsc --noEmit clean, eslint clean. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01PdHPZAjciZFWqGa19Yf7it --- src/interpreter/plugin/AddressPlugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interpreter/plugin/AddressPlugin.ts b/src/interpreter/plugin/AddressPlugin.ts index 7e37a42e4..b0ab810dd 100644 --- a/src/interpreter/plugin/AddressPlugin.ts +++ b/src/interpreter/plugin/AddressPlugin.ts @@ -67,7 +67,7 @@ export class AddressPlugin extends FunctionPlugin implements FunctionPluginTypec const colLetter = columnIndexToLabel(col - 1) let sheetPrefix = '' - if (sheetName !== undefined && sheetName !== null) { + if (sheetName !== undefined && sheetName !== null && sheetName !== '') { sheetPrefix = `${sheetName}!` } From 62cb94402981f95935e905687e479bc2dbc94a95 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Thu, 20 Aug 2026 09:01:20 +0000 Subject: [PATCH 2/3] HF-160: add the CHANGELOG entry the DoD gate flagged Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01PdHPZAjciZFWqGa19Yf7it --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24ecffc1c..60820c67a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Fixed + +- Fixed the `ADDRESS` function to omit the sheet separator (`!`) when the `sheetName` argument is an empty string, consistently with Excel, in both A1 and R1C1 styles. [#1739](https://github.com/handsontable/hyperformula/pull/1739) + ## [3.4.0] - 2026-08-10 ### Added From dbab806399cf4dff4641d4b778cd2710d594e218 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Mon, 31 Aug 2026 07:05:54 +0000 Subject: [PATCH 3/3] HF-160: keep the sheet separator for an empty sheetName, drop it only when absent Measured in Excel via the Graph API (three independent sessions, LEN()-proven), which contradicts this task's acceptance criteria: =ADDRESS(2,3,1,FALSE) -> R2C3 (LEN 4, no "!") =ADDRESS(2,3,1,FALSE,) -> R2C3 (LEN 4, no "!") =ADDRESS(2,3,1,FALSE,"") -> !R2C3 (LEN 5, keeps "!") =ADDRESS(2,3,1,FALSE,) -> !R2C3 (LEN 5, keeps "!") =ADDRESS(1,1,4,TRUE,"") -> !A1 (LEN 3, keeps "!") Excel distinguishes an argument that is syntactically absent from one that is present with an empty value. The AC asked for the separator to be dropped in all three empty-ish cases "consistently with Excel"; that premise holds for only one of them. The previous revision of this branch implemented the AC literally and regressed three cases that develop already got right, so this replaces it. The remaining real defect is the empty argument slot: a trailing comma reached the implementation as "" and produced a stray "!". Rather than hand-inspecting AstNodeType.EMPTY in the plugin, this adds an `emptyAsAbsent` argument-validation option next to the existing `emptyAsDefault`, so the distinction lives in the argument metadata and every function can opt in. ADDRESS's sheetName uses it. `emptyAsAbsent` is part of the custom-function metadata surface, so it is documented in docs/guide/custom-functions.md and the CHANGELOG. Verified: 6185/6189 jest tests pass. The single failure (=MOD(-7,2) in unit/function-metadata-api.spec.ts) reproduces on clean develop with clean develop tests and is unrelated: it is a stale golden expectation left by the MOD sign fix in #1752. --- CHANGELOG.md | 6 +++++- docs/guide/custom-functions.md | 1 + src/interpreter/plugin/AddressPlugin.ts | 4 ++-- src/interpreter/plugin/FunctionPlugin.ts | 25 +++++++++++++++++++++++- 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d023349c..9c674cc2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +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 an empty string, consistently with Excel, in both A1 and R1C1 styles. [#1739](https://github.com/handsontable/hyperformula/pull/1739) +- 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 b0ab810dd..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}, ] }, } @@ -67,7 +67,7 @@ export class AddressPlugin extends FunctionPlugin implements FunctionPluginTypec const colLetter = columnIndexToLabel(col - 1) let sheetPrefix = '' - if (sheetName !== undefined && sheetName !== null && sheetName !== '') { + if (sheetName !== undefined && sheetName !== null) { sheetPrefix = `${sheetName}!` } 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