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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docs/guide/built-in-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions src/i18n/languages/csCZ.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = {
},
functions: {
FILTER: 'FILTER',
VSTACK: 'VSTACK',
HSTACK: 'HSTACK',
ADDRESS: 'ODKAZ',
'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN',
ARRAYFORMULA: 'ARRAYFORMULA',
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/languages/daDK.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = {
},
functions: {
FILTER: 'FILTER',
VSTACK: 'VSTACK',
HSTACK: 'HSTACK',
ADDRESS: 'ADRESSE',
'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN',
ARRAYFORMULA: 'ARRAYFORMULA',
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/languages/deDE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = {
},
functions: {
FILTER: 'FILTER',
VSTACK: 'VSTACK',
HSTACK: 'HSTACK',
ADDRESS: 'ADRESSE',
'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN',
ARRAYFORMULA: 'ARRAYFORMULA',
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/languages/enGB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = {
},
functions: {
FILTER: 'FILTER',
VSTACK: 'VSTACK',
HSTACK: 'HSTACK',
ADDRESS: 'ADDRESS',
'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN',
ARRAYFORMULA: 'ARRAYFORMULA',
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/languages/esES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export const dictionary: RawTranslationPackage = {
},
functions: {
FILTER: 'FILTER',
VSTACK: 'VSTACK',
HSTACK: 'HSTACK',
ADDRESS: 'DIRECCION',
'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN',
ARRAYFORMULA: 'ARRAYFORMULA',
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/languages/fiFI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = {
},
functions: {
FILTER: 'FILTER',
VSTACK: 'VSTACK',
HSTACK: 'HSTACK',
ADDRESS: 'OSOITE',
'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN',
ARRAYFORMULA: 'ARRAYFORMULA',
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/languages/frFR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = {
},
functions: {
FILTER: 'FILTER',
VSTACK: 'VSTACK',
HSTACK: 'HSTACK',
ADDRESS: 'ADRESSE',
'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN',
ARRAYFORMULA: 'ARRAYFORMULA',
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/languages/huHU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = {
},
functions: {
FILTER: 'FILTER',
VSTACK: 'VSTACK',
HSTACK: 'HSTACK',
ADDRESS: 'CÍM',
'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN',
ARRAYFORMULA: 'ARRAYFORMULA',
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/languages/idID.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = {
},
functions: {
FILTER: 'FILTER',
VSTACK: 'VSTACK',
HSTACK: 'HSTACK',
ADDRESS: 'ALAMAT',
'ARRAY_CONSTRAIN': 'BATASAN.MATRIKS',
ARRAYFORMULA: 'RUMUS.MATRIKS',
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/languages/itIT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = {
},
functions: {
FILTER: 'FILTER',
VSTACK: 'VSTACK',
HSTACK: 'HSTACK',
ADDRESS: 'INDIRIZZO',
'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN',
ARRAYFORMULA: 'ARRAYFORMULA',
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/languages/nbNO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = {
},
functions: {
FILTER: 'FILTER',
VSTACK: 'VSTACK',
HSTACK: 'HSTACK',
ADDRESS: 'ADRESSE',
'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN',
ARRAYFORMULA: 'ARRAYFORMULA',
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/languages/nlNL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = {
},
functions: {
FILTER: 'FILTER',
VSTACK: 'VSTACK',
HSTACK: 'HSTACK',
ADDRESS: 'ADRES',
'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN',
ARRAYFORMULA: 'ARRAYFORMULA',
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/languages/plPL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = {
},
functions: {
FILTER: 'FILTER',
VSTACK: 'VSTACK',
HSTACK: 'HSTACK',
ADDRESS: 'ADRES',
'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN',
ARRAYFORMULA: 'ARRAYFORMULA',
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/languages/ptPT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = {
},
functions: {
FILTER: 'FILTER',
VSTACK: 'VSTACK',
HSTACK: 'HSTACK',
ADDRESS: 'ENDEREÇO',
'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN',
ARRAYFORMULA: 'ARRAYFORMULA',
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/languages/ruRU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = {
},
functions: {
FILTER: 'FILTER',
VSTACK: 'VSTACK',
HSTACK: 'HSTACK',
ADDRESS: 'АДРЕС',
'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN',
ARRAYFORMULA: 'ARRAYFORMULA',
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/languages/svSE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = {
},
functions: {
FILTER: 'FILTER',
VSTACK: 'VSTACK',
HSTACK: 'HSTACK',
ADDRESS: 'ADRESS',
'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN',
ARRAYFORMULA: 'ARRAYFORMULA',
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/languages/trTR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const dictionary: RawTranslationPackage = {
},
functions: {
FILTER: 'FILTER',
VSTACK: 'VSTACK',
HSTACK: 'HSTACK',
ADDRESS: 'ADRES',
'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN',
ARRAYFORMULA: 'ARRAYFORMULA',
Expand Down
114 changes: 113 additions & 1 deletion src/interpreter/plugin/ArrayPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HSTACK omits NA row padding

Medium Severity

In hstack, when a row exists but has fewer columns than the range width (including an empty row array), cells beyond the row length are taken from sourceRow[col] and become undefined instead of #N/A. vstack pads short rows via padRowToWidth, so stacked results can disagree with Excel and with VSTACK on jagged inputs such as FILTER output.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4abdae5. Configure here.

}
}

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
}
}
Loading