From 4abdae5830e02601bc2ca6acf583f68d9b9b22ea Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Mon, 15 Jun 2026 03:24:02 +0000 Subject: [PATCH 1/4] feat(functions): implement VSTACK and HSTACK (HF-71) Add the VSTACK and HSTACK array-manipulation functions to ArrayPlugin, mirroring the existing FILTER implementation pattern. - VSTACK stacks input arrays vertically: result height = sum of input heights, width = max input width. Narrower rows are padded on the right with #N/A. - HSTACK stacks input arrays horizontally: result width = sum of input widths, height = max input height. Shorter columns are padded at the bottom with #N/A. - Both are variadic (repeatLastArgs: 1) and accept ranges, array literals and scalars (FunctionArgumentType.RANGE, enableArrayArithmeticForArguments). - Result dimensions are declared at parse time via the sizeOfResultArrayMethod (vstackArraySize / hstackArraySize) so the result spills correctly, consistent with HyperFormula's parse-time array sizing. Padding uses ErrorType.NA (HyperFormula has no #CALC!). - Add translations for all 17 built-in language packs (English name in every locale, matching Excel/Sheets, which do not localize these functions). enUS inherits enGB. - Document both functions in the built-in functions guide and add a changelog entry. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 1 + docs/guide/built-in-functions.md | 2 + src/i18n/languages/csCZ.ts | 2 + src/i18n/languages/daDK.ts | 2 + src/i18n/languages/deDE.ts | 2 + src/i18n/languages/enGB.ts | 2 + src/i18n/languages/esES.ts | 2 + src/i18n/languages/fiFI.ts | 2 + src/i18n/languages/frFR.ts | 2 + src/i18n/languages/huHU.ts | 2 + src/i18n/languages/idID.ts | 2 + src/i18n/languages/itIT.ts | 2 + src/i18n/languages/nbNO.ts | 2 + src/i18n/languages/nlNL.ts | 2 + src/i18n/languages/plPL.ts | 2 + src/i18n/languages/ptPT.ts | 2 + src/i18n/languages/ruRU.ts | 2 + src/i18n/languages/svSE.ts | 2 + src/i18n/languages/trTR.ts | 2 + src/interpreter/plugin/ArrayPlugin.ts | 114 +++++++++++++++++++++++++- 20 files changed, 150 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 683e1da144..ca3f5a8e28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added +- Added new functions: VSTACK, HSTACK. [#1690](https://github.com/handsontable/hyperformula/issues/1690) - Added an Indonesian (Bahasa Indonesia) language pack. [#1674](https://github.com/handsontable/hyperformula/pull/1674) ## [3.3.0] - 2026-05-20 diff --git a/docs/guide/built-in-functions.md b/docs/guide/built-in-functions.md index 5798f0404d..de895e2dfd 100644 --- a/docs/guide/built-in-functions.md +++ b/docs/guide/built-in-functions.md @@ -65,6 +65,8 @@ Total number of functions: **{{ $page.functionsCount }}** | FILTER | Filters an array, based on multiple conditions (boolean arrays). | FILTER(SourceArray, BoolArray1, BoolArray2, ...BoolArrayN) | | ARRAY_CONSTRAIN | Truncates an array to given dimensions. | ARRAY_CONSTRAIN(Array, Height, Width) | | SEQUENCE | Returns an array of sequential numbers. | SEQUENCE(Rows, [Cols], [Start], [Step]) | +| VSTACK | Stacks arrays vertically into a single array. | VSTACK(Array1, [Array2], ...[ArrayN]) | +| HSTACK | Stacks arrays horizontally into a single array. | HSTACK(Array1, [Array2], ...[ArrayN]) | ### Date and time diff --git a/src/i18n/languages/csCZ.ts b/src/i18n/languages/csCZ.ts index 71ce56b148..8a79738b01 100644 --- a/src/i18n/languages/csCZ.ts +++ b/src/i18n/languages/csCZ.ts @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', + VSTACK: 'VSTACK', + HSTACK: 'HSTACK', ADDRESS: 'ODKAZ', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', diff --git a/src/i18n/languages/daDK.ts b/src/i18n/languages/daDK.ts index ea5a1e4ced..90cd9281d4 100644 --- a/src/i18n/languages/daDK.ts +++ b/src/i18n/languages/daDK.ts @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', + VSTACK: 'VSTACK', + HSTACK: 'HSTACK', ADDRESS: 'ADRESSE', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', diff --git a/src/i18n/languages/deDE.ts b/src/i18n/languages/deDE.ts index 24a8ec03be..1657fd9438 100644 --- a/src/i18n/languages/deDE.ts +++ b/src/i18n/languages/deDE.ts @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', + VSTACK: 'VSTACK', + HSTACK: 'HSTACK', ADDRESS: 'ADRESSE', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', diff --git a/src/i18n/languages/enGB.ts b/src/i18n/languages/enGB.ts index e878f897ae..bd5d922139 100644 --- a/src/i18n/languages/enGB.ts +++ b/src/i18n/languages/enGB.ts @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', + VSTACK: 'VSTACK', + HSTACK: 'HSTACK', ADDRESS: 'ADDRESS', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', diff --git a/src/i18n/languages/esES.ts b/src/i18n/languages/esES.ts index d36d14fd06..7287ada3a6 100644 --- a/src/i18n/languages/esES.ts +++ b/src/i18n/languages/esES.ts @@ -19,6 +19,8 @@ export const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', + VSTACK: 'VSTACK', + HSTACK: 'HSTACK', ADDRESS: 'DIRECCION', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', diff --git a/src/i18n/languages/fiFI.ts b/src/i18n/languages/fiFI.ts index 5735278d85..ea34d1c32d 100644 --- a/src/i18n/languages/fiFI.ts +++ b/src/i18n/languages/fiFI.ts @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', + VSTACK: 'VSTACK', + HSTACK: 'HSTACK', ADDRESS: 'OSOITE', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', diff --git a/src/i18n/languages/frFR.ts b/src/i18n/languages/frFR.ts index 044c70715a..43a0a2d318 100644 --- a/src/i18n/languages/frFR.ts +++ b/src/i18n/languages/frFR.ts @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', + VSTACK: 'VSTACK', + HSTACK: 'HSTACK', ADDRESS: 'ADRESSE', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', diff --git a/src/i18n/languages/huHU.ts b/src/i18n/languages/huHU.ts index 3a51192221..7a28720966 100644 --- a/src/i18n/languages/huHU.ts +++ b/src/i18n/languages/huHU.ts @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', + VSTACK: 'VSTACK', + HSTACK: 'HSTACK', ADDRESS: 'CÍM', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', diff --git a/src/i18n/languages/idID.ts b/src/i18n/languages/idID.ts index b07d9aad8e..4f2cc4b770 100644 --- a/src/i18n/languages/idID.ts +++ b/src/i18n/languages/idID.ts @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', + VSTACK: 'VSTACK', + HSTACK: 'HSTACK', ADDRESS: 'ALAMAT', 'ARRAY_CONSTRAIN': 'BATASAN.MATRIKS', ARRAYFORMULA: 'RUMUS.MATRIKS', diff --git a/src/i18n/languages/itIT.ts b/src/i18n/languages/itIT.ts index 2716d34714..cab8c97d50 100644 --- a/src/i18n/languages/itIT.ts +++ b/src/i18n/languages/itIT.ts @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', + VSTACK: 'VSTACK', + HSTACK: 'HSTACK', ADDRESS: 'INDIRIZZO', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', diff --git a/src/i18n/languages/nbNO.ts b/src/i18n/languages/nbNO.ts index 80d9948f6b..0d8028ab76 100644 --- a/src/i18n/languages/nbNO.ts +++ b/src/i18n/languages/nbNO.ts @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', + VSTACK: 'VSTACK', + HSTACK: 'HSTACK', ADDRESS: 'ADRESSE', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', diff --git a/src/i18n/languages/nlNL.ts b/src/i18n/languages/nlNL.ts index 57d6fddb92..bc0ac0e2b8 100644 --- a/src/i18n/languages/nlNL.ts +++ b/src/i18n/languages/nlNL.ts @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', + VSTACK: 'VSTACK', + HSTACK: 'HSTACK', ADDRESS: 'ADRES', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', diff --git a/src/i18n/languages/plPL.ts b/src/i18n/languages/plPL.ts index 251a35bbae..dfe6c40b54 100644 --- a/src/i18n/languages/plPL.ts +++ b/src/i18n/languages/plPL.ts @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', + VSTACK: 'VSTACK', + HSTACK: 'HSTACK', ADDRESS: 'ADRES', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', diff --git a/src/i18n/languages/ptPT.ts b/src/i18n/languages/ptPT.ts index 3195612f02..fddb84e8a7 100644 --- a/src/i18n/languages/ptPT.ts +++ b/src/i18n/languages/ptPT.ts @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', + VSTACK: 'VSTACK', + HSTACK: 'HSTACK', ADDRESS: 'ENDEREÇO', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', diff --git a/src/i18n/languages/ruRU.ts b/src/i18n/languages/ruRU.ts index cfbf51e59c..9c76254190 100644 --- a/src/i18n/languages/ruRU.ts +++ b/src/i18n/languages/ruRU.ts @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', + VSTACK: 'VSTACK', + HSTACK: 'HSTACK', ADDRESS: 'АДРЕС', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', diff --git a/src/i18n/languages/svSE.ts b/src/i18n/languages/svSE.ts index bce2c4cf86..ecc15d4639 100644 --- a/src/i18n/languages/svSE.ts +++ b/src/i18n/languages/svSE.ts @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', + VSTACK: 'VSTACK', + HSTACK: 'HSTACK', ADDRESS: 'ADRESS', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', diff --git a/src/i18n/languages/trTR.ts b/src/i18n/languages/trTR.ts index af2c253907..eee1f4dd39 100644 --- a/src/i18n/languages/trTR.ts +++ b/src/i18n/languages/trTR.ts @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', + VSTACK: 'VSTACK', + HSTACK: 'HSTACK', ADDRESS: 'ADRES', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', diff --git a/src/interpreter/plugin/ArrayPlugin.ts b/src/interpreter/plugin/ArrayPlugin.ts index d9915f7f42..3cff70ddbd 100644 --- a/src/interpreter/plugin/ArrayPlugin.ts +++ b/src/interpreter/plugin/ArrayPlugin.ts @@ -42,7 +42,25 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche {argumentType: FunctionArgumentType.RANGE}, ], repeatLastArgs: 1, - } + }, + 'VSTACK': { + method: 'vstack', + sizeOfResultArrayMethod: 'vstackArraySize', + enableArrayArithmeticForArguments: true, + parameters: [ + {argumentType: FunctionArgumentType.RANGE}, + ], + repeatLastArgs: 1, + }, + 'HSTACK': { + method: 'hstack', + sizeOfResultArrayMethod: 'hstackArraySize', + enableArrayArithmeticForArguments: true, + parameters: [ + {argumentType: FunctionArgumentType.RANGE}, + ], + repeatLastArgs: 1, + }, } public arrayformula(ast: ProcedureAst, state: InterpreterState): InterpreterValue { @@ -147,4 +165,98 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche const height = Math.max(...(subChecks).map(val => val.height)) return new ArraySize(width, height) } + + /** + * Corresponds to VSTACK(array1, [array2], ...) + * + * Stacks the input arrays vertically, one on top of another, into a single array. + * The result has as many rows as the inputs combined and as many columns as the + * widest input. Cells of narrower inputs are padded on the right with the #N/A + * error, matching the behaviour of Excel and Google Sheets. + * + * @param ast + * @param state + */ + public vstack(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('VSTACK'), (...ranges: SimpleRangeValue[]) => { + const width = Math.max(...ranges.map(range => range.width())) + const result: InternalScalarValue[][] = [] + + for (const range of ranges) { + for (const row of range.data) { + result.push(this.padRowToWidth(row, width)) + } + } + + return SimpleRangeValue.onlyValues(result) + }) + } + + public vstackArraySize(ast: ProcedureAst, state: InterpreterState): ArraySize { + if (ast.args.length < 1) { + return ArraySize.error() + } + + const subChecks = this.stackSubChecks(ast, state, 'VSTACK') + const width = Math.max(...subChecks.map(size => size.width)) + const height = subChecks.reduce((total, size) => total + size.height, 0) + return new ArraySize(width, height) + } + + /** + * Corresponds to HSTACK(array1, [array2], ...) + * + * Stacks the input arrays horizontally, side by side, into a single array. + * The result has as many columns as the inputs combined and as many rows as the + * tallest input. Cells of shorter inputs are padded at the bottom with the #N/A + * error, matching the behaviour of Excel and Google Sheets. + * + * @param ast + * @param state + */ + public hstack(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + return this.runFunction(ast.args, state, this.metadata('HSTACK'), (...ranges: SimpleRangeValue[]) => { + const height = Math.max(...ranges.map(range => range.height())) + const result: InternalScalarValue[][] = [...Array(height).keys()].map(() => []) + + for (const range of ranges) { + const data = range.data + for (let row = 0; row < height; row++) { + const sourceRow = row < data.length ? data[row] : undefined + for (let col = 0; col < range.width(); col++) { + result[row].push(sourceRow !== undefined ? sourceRow[col] : new CellError(ErrorType.NA, ErrorMessage.ValueNotFound)) + } + } + } + + return SimpleRangeValue.onlyValues(result) + }) + } + + public hstackArraySize(ast: ProcedureAst, state: InterpreterState): ArraySize { + if (ast.args.length < 1) { + return ArraySize.error() + } + + const subChecks = this.stackSubChecks(ast, state, 'HSTACK') + const width = subChecks.reduce((total, size) => total + size.width, 0) + const height = Math.max(...subChecks.map(size => size.height)) + return new ArraySize(width, height) + } + + private stackSubChecks(ast: ProcedureAst, state: InterpreterState, functionName: 'VSTACK' | 'HSTACK'): ArraySize[] { + const metadata = this.metadata(functionName) + return ast.args.map((arg) => this.arraySizeForAst(arg, new InterpreterState(state.formulaAddress, state.arraysFlag || (metadata?.enableArrayArithmeticForArguments ?? false)))) + } + + private padRowToWidth(row: InternalScalarValue[], width: number): InternalScalarValue[] { + if (row.length >= width) { + return row.slice(0, width) + } + const padded = row.slice() + while (padded.length < width) { + padded.push(new CellError(ErrorType.NA, ErrorMessage.ValueNotFound)) + } + return padded + } } From 8b7191b3ebda9695801167423ddae7ddc3d997f9 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Thu, 25 Jun 2026 15:03:37 +0000 Subject: [PATCH 2/4] fix(hstack): pad short rows via padRowToWidth, symmetric with VSTACK (HF-71) HSTACK indexed sourceRow[col] directly, trusting every row to be as wide as the range. A jagged SimpleRangeValue (a row shorter than range.width(), reachable via a custom function) produced an empty cell where VSTACK produces #N/A. Reuse padRowToWidth so both functions pad identically. Reported by Cursor Bugbot on PR #1698. Co-Authored-By: Claude Opus 4.8 --- src/interpreter/plugin/ArrayPlugin.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/interpreter/plugin/ArrayPlugin.ts b/src/interpreter/plugin/ArrayPlugin.ts index 3cff70ddbd..33431ef534 100644 --- a/src/interpreter/plugin/ArrayPlugin.ts +++ b/src/interpreter/plugin/ArrayPlugin.ts @@ -222,10 +222,8 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche for (const range of ranges) { const data = range.data for (let row = 0; row < height; row++) { - const sourceRow = row < data.length ? data[row] : undefined - for (let col = 0; col < range.width(); col++) { - result[row].push(sourceRow !== undefined ? sourceRow[col] : new CellError(ErrorType.NA, ErrorMessage.ValueNotFound)) - } + const sourceRow = row < data.length ? data[row] : [] + result[row].push(...this.padRowToWidth(sourceRow, range.width())) } } From a61573becbd58e8f159bffe7b0d3ef03b1ecd444 Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Fri, 26 Jun 2026 15:34:05 +0000 Subject: [PATCH 3/4] docs(changelog): point VSTACK/HSTACK entry at PR #1698 (HF-71) The entry linked #1690 ("Fix docs AGENTS standards link"), an unrelated PR, via /issues/. Correct it to this feature's PR #1698. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca3f5a8e28..44e0716424 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added -- Added new functions: VSTACK, HSTACK. [#1690](https://github.com/handsontable/hyperformula/issues/1690) +- Added new functions: VSTACK, HSTACK. [#1698](https://github.com/handsontable/hyperformula/pull/1698) - Added an Indonesian (Bahasa Indonesia) language pack. [#1674](https://github.com/handsontable/hyperformula/pull/1674) ## [3.3.0] - 2026-05-20 From 52d9993f79428fd38e205e6e5696c05a94c6a17b Mon Sep 17 00:00:00 2001 From: marcin-kordas-hoc Date: Mon, 29 Jun 2026 14:43:18 +0000 Subject: [PATCH 4/4] refactor(stack): add JSDoc to stacking helpers; pad HSTACK inline (HF-71) - Add JSDoc to the new vstackArraySize/hstackArraySize/stackSubChecks/ padRowToWidth methods (sequba: JSDoc required on new public surface). - HSTACK pads rows inline with an index guard instead of building an intermediate padded array via padRowToWidth, avoiding a per-row slice + spread allocation in the common rectangular path. Behaviour is unchanged (covered by the jagged-input and empty-array regression tests); VSTACK keeps padRowToWidth, whose copy it relies on per result row. Co-Authored-By: Claude Opus 4.8 --- src/interpreter/plugin/ArrayPlugin.ts | 41 +++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/src/interpreter/plugin/ArrayPlugin.ts b/src/interpreter/plugin/ArrayPlugin.ts index 33431ef534..27b47096e0 100644 --- a/src/interpreter/plugin/ArrayPlugin.ts +++ b/src/interpreter/plugin/ArrayPlugin.ts @@ -192,6 +192,13 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche }) } + /** + * Calculates the spilled array size of VSTACK: the width is the widest input + * and the height is the sum of all input heights. + * + * @param ast + * @param state + */ public vstackArraySize(ast: ProcedureAst, state: InterpreterState): ArraySize { if (ast.args.length < 1) { return ArraySize.error() @@ -221,9 +228,16 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche for (const range of ranges) { const data = range.data + const width = range.width() for (let row = 0; row < height; row++) { - const sourceRow = row < data.length ? data[row] : [] - result[row].push(...this.padRowToWidth(sourceRow, range.width())) + const sourceRow = row < data.length ? data[row] : undefined + for (let col = 0; col < width; col++) { + // Pad both missing rows (sourceRow === undefined) and short rows + // (col beyond the row's length) with #N/A, exactly as VSTACK does. + result[row].push(sourceRow !== undefined && col < sourceRow.length + ? sourceRow[col] + : new CellError(ErrorType.NA, ErrorMessage.ValueNotFound)) + } } } @@ -231,6 +245,13 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche }) } + /** + * Calculates the spilled array size of HSTACK: the width is the sum of all + * input widths and the height is the tallest input. + * + * @param ast + * @param state + */ public hstackArraySize(ast: ProcedureAst, state: InterpreterState): ArraySize { if (ast.args.length < 1) { return ArraySize.error() @@ -242,11 +263,27 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche return new ArraySize(width, height) } + /** + * Resolves the array size of every argument of a stacking function, enabling + * array arithmetic for the arguments when the function's metadata requests it. + * + * @param ast + * @param state + * @param functionName - the stacking function whose metadata drives the array-arithmetic flag + */ private stackSubChecks(ast: ProcedureAst, state: InterpreterState, functionName: 'VSTACK' | 'HSTACK'): ArraySize[] { const metadata = this.metadata(functionName) return ast.args.map((arg) => this.arraySizeForAst(arg, new InterpreterState(state.formulaAddress, state.arraysFlag || (metadata?.enableArrayArithmeticForArguments ?? false)))) } + /** + * Returns a copy of the given row resized to exactly `width` cells: longer + * rows are truncated and shorter rows are padded on the right with #N/A. Used + * by VSTACK to align every stacked row to the widest input. + * + * @param row - the source row to resize + * @param width - the target number of cells + */ private padRowToWidth(row: InternalScalarValue[], width: number): InternalScalarValue[] { if (row.length >= width) { return row.slice(0, width)