From 5c3df0caa1bf84e6fdb07018cb2a2472d013a524 Mon Sep 17 00:00:00 2001 From: tobiadefami Date: Tue, 4 Aug 2026 18:18:40 +0100 Subject: [PATCH 01/20] feat: add TAKE function --- CHANGELOG.md | 4 + docs/guide/known-limitations.md | 4 + src/i18n/languages/csCZ.ts | 1 + src/i18n/languages/daDK.ts | 1 + src/i18n/languages/deDE.ts | 1 + src/i18n/languages/enGB.ts | 1 + src/i18n/languages/esES.ts | 1 + src/i18n/languages/fiFI.ts | 1 + src/i18n/languages/frFR.ts | 1 + src/i18n/languages/huHU.ts | 1 + src/i18n/languages/idID.ts | 1 + src/i18n/languages/itIT.ts | 1 + src/i18n/languages/nbNO.ts | 1 + src/i18n/languages/nlNL.ts | 1 + src/i18n/languages/plPL.ts | 1 + src/i18n/languages/ptPT.ts | 1 + src/i18n/languages/ruRU.ts | 1 + src/i18n/languages/svSE.ts | 1 + src/i18n/languages/trTR.ts | 1 + .../categories/lookup-and-reference.ts | 7 ++ src/interpreter/plugin/ArrayPlugin.ts | 74 +++++++++++++++++++ 21 files changed, 106 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24ecffc1c..212e9e2fe 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] +### Added + +- Added the `TAKE` dynamic-array function. + ## [3.4.0] - 2026-08-10 ### Added diff --git a/docs/guide/known-limitations.md b/docs/guide/known-limitations.md index 7f0e2361d..aeb932772 100644 --- a/docs/guide/known-limitations.md +++ b/docs/guide/known-limitations.md @@ -51,6 +51,10 @@ a circular reference. * Ordering (including mixed types, empty cells, and text collation) follows HyperFormula's own comparison rules, which honor the `caseSensitive` and `accentSensitive` configuration options. Numbers sort before text, and text before logical values. +### TAKE function + +* When a non-empty `rows` or `columns` argument evaluates to 0, or when the source has no effective rows or columns, `TAKE` returns `#N/A` with the "Empty range not allowed" message. Microsoft Excel returns `#CALC!` for a zero-count empty-array result, but HyperFormula does not support the `#CALC!` error type. + ### OFFSET function HyperFormula resolves the OFFSET function at parse time rather than during evaluation. The parser inspects the arguments and rewrites the expression into a plain cell reference or range. This keeps the dependency graph accurate but imposes several restrictions. diff --git a/src/i18n/languages/csCZ.ts b/src/i18n/languages/csCZ.ts index 830b1c196..254d65049 100644 --- a/src/i18n/languages/csCZ.ts +++ b/src/i18n/languages/csCZ.ts @@ -225,6 +225,7 @@ const dictionary: RawTranslationPackage = { SWITCH: '', SYD: 'ODPIS.NELIN', T: 'T', + TAKE: 'TAKE', TAN: 'TG', TANH: 'TGH', TBILLEQ: 'TBILLEQ', diff --git a/src/i18n/languages/daDK.ts b/src/i18n/languages/daDK.ts index 90296fd42..0f5ed9234 100644 --- a/src/i18n/languages/daDK.ts +++ b/src/i18n/languages/daDK.ts @@ -225,6 +225,7 @@ const dictionary: RawTranslationPackage = { SWITCH: '', SYD: 'ÅRSAFSKRIVNING', T: 'T', + TAKE: 'TAG', TAN: 'TAN', TANH: 'TANH', TBILLEQ: 'STATSOBLIGATION', diff --git a/src/i18n/languages/deDE.ts b/src/i18n/languages/deDE.ts index daaaa0bf7..9aa3b3cb6 100644 --- a/src/i18n/languages/deDE.ts +++ b/src/i18n/languages/deDE.ts @@ -225,6 +225,7 @@ const dictionary: RawTranslationPackage = { SWITCH: '', SYD: 'DIA', T: 'T', + TAKE: 'ÜBERNEHMEN', TAN: 'TAN', TANH: 'TANHYP', TBILLEQ: 'TBILLÄQUIV', diff --git a/src/i18n/languages/enGB.ts b/src/i18n/languages/enGB.ts index d271b732c..e8286f5da 100644 --- a/src/i18n/languages/enGB.ts +++ b/src/i18n/languages/enGB.ts @@ -227,6 +227,7 @@ const dictionary: RawTranslationPackage = { SWITCH: 'SWITCH', SYD: 'SYD', T: 'T', + TAKE: 'TAKE', TAN: 'TAN', TANH: 'TANH', TBILLEQ: 'TBILLEQ', diff --git a/src/i18n/languages/esES.ts b/src/i18n/languages/esES.ts index 6fea52e4e..b41794de1 100644 --- a/src/i18n/languages/esES.ts +++ b/src/i18n/languages/esES.ts @@ -225,6 +225,7 @@ export const dictionary: RawTranslationPackage = { SWITCH: '', SYD: 'SYD', T: 'T', + TAKE: 'TOMAR', TAN: 'TAN', TANH: 'TANH', TBILLEQ: 'LETRA.DE.TEST.EQV.A.BONO', diff --git a/src/i18n/languages/fiFI.ts b/src/i18n/languages/fiFI.ts index a3b319af2..c80e18186 100644 --- a/src/i18n/languages/fiFI.ts +++ b/src/i18n/languages/fiFI.ts @@ -225,6 +225,7 @@ const dictionary: RawTranslationPackage = { SWITCH: '', SYD: 'VUOSIPOISTO', T: 'T', + TAKE: 'TAKE', TAN: 'TAN', TANH: 'TANH', TBILLEQ: 'OBLIG.TUOTTOPROS', diff --git a/src/i18n/languages/frFR.ts b/src/i18n/languages/frFR.ts index a29109053..19f957351 100644 --- a/src/i18n/languages/frFR.ts +++ b/src/i18n/languages/frFR.ts @@ -225,6 +225,7 @@ const dictionary: RawTranslationPackage = { SWITCH: '', SYD: 'SYD', T: 'T', + TAKE: 'TAKE', TAN: 'TAN', TANH: 'TANH', TBILLEQ: 'TAUX.ESCOMPTE.R', diff --git a/src/i18n/languages/huHU.ts b/src/i18n/languages/huHU.ts index fafc2c4b9..b8c175e4f 100644 --- a/src/i18n/languages/huHU.ts +++ b/src/i18n/languages/huHU.ts @@ -225,6 +225,7 @@ const dictionary: RawTranslationPackage = { SWITCH: '', SYD: 'ÉSZÖ', T: 'T', + TAKE: 'TAKE', TAN: 'TAN', TANH: 'TANH', TBILLEQ: 'KJEGY.EGYENÉRT', diff --git a/src/i18n/languages/idID.ts b/src/i18n/languages/idID.ts index 719cb11db..337e32d57 100644 --- a/src/i18n/languages/idID.ts +++ b/src/i18n/languages/idID.ts @@ -227,6 +227,7 @@ const dictionary: RawTranslationPackage = { SWITCH: 'BERALIH', SYD: 'SYD', T: 'T', + TAKE: 'TAKE', TAN: 'TAN', TANH: 'TANH', TBILLEQ: 'TBILL.SAMA', diff --git a/src/i18n/languages/itIT.ts b/src/i18n/languages/itIT.ts index 40ec9cf41..64dc5afa9 100644 --- a/src/i18n/languages/itIT.ts +++ b/src/i18n/languages/itIT.ts @@ -225,6 +225,7 @@ const dictionary: RawTranslationPackage = { SWITCH: '', SYD: 'AMMORT.ANNUO', T: 'T', + TAKE: 'TAKE', TAN: 'TAN', TANH: 'TANH', TBILLEQ: 'BOT.EQUIV', diff --git a/src/i18n/languages/nbNO.ts b/src/i18n/languages/nbNO.ts index 89c0f85f7..721d096f3 100644 --- a/src/i18n/languages/nbNO.ts +++ b/src/i18n/languages/nbNO.ts @@ -225,6 +225,7 @@ const dictionary: RawTranslationPackage = { SWITCH: '', SYD: 'ÅRSAVS', T: 'T', + TAKE: 'TAKE', TAN: 'TAN', TANH: 'TANH', TBILLEQ: 'TBILLEKV', diff --git a/src/i18n/languages/nlNL.ts b/src/i18n/languages/nlNL.ts index 73ef99687..d63edfccd 100644 --- a/src/i18n/languages/nlNL.ts +++ b/src/i18n/languages/nlNL.ts @@ -225,6 +225,7 @@ const dictionary: RawTranslationPackage = { SWITCH: '', SYD: 'SYD', T: 'T', + TAKE: 'NEMEN', TAN: 'TAN', TANH: 'TANH', TBILLEQ: 'SCHATK.OBL', diff --git a/src/i18n/languages/plPL.ts b/src/i18n/languages/plPL.ts index 003b0fd64..22e181175 100644 --- a/src/i18n/languages/plPL.ts +++ b/src/i18n/languages/plPL.ts @@ -225,6 +225,7 @@ const dictionary: RawTranslationPackage = { SWITCH: 'PRZEŁĄCZ', SYD: 'SYD', T: 'T', + TAKE: 'TAKE', TAN: 'TAN', TANH: 'TANH', TBILLEQ: 'RENT.EKW.BS', diff --git a/src/i18n/languages/ptPT.ts b/src/i18n/languages/ptPT.ts index 408d7f772..e58a1ffc8 100644 --- a/src/i18n/languages/ptPT.ts +++ b/src/i18n/languages/ptPT.ts @@ -225,6 +225,7 @@ const dictionary: RawTranslationPackage = { SWITCH: '', SYD: 'SDA', T: 'T', + TAKE: 'TAKE', TAN: 'TAN', TANH: 'TANH', TBILLEQ: 'OTN', diff --git a/src/i18n/languages/ruRU.ts b/src/i18n/languages/ruRU.ts index 657b0db37..d2213035c 100644 --- a/src/i18n/languages/ruRU.ts +++ b/src/i18n/languages/ruRU.ts @@ -225,6 +225,7 @@ const dictionary: RawTranslationPackage = { SWITCH: '', SYD: 'АСЧ', T: 'Т', + TAKE: 'ВЗЯТЬ', TAN: 'TAN', TANH: 'TANH', TBILLEQ: 'РАВНОКЧЕК', diff --git a/src/i18n/languages/svSE.ts b/src/i18n/languages/svSE.ts index 5f8758008..ef7d34069 100644 --- a/src/i18n/languages/svSE.ts +++ b/src/i18n/languages/svSE.ts @@ -225,6 +225,7 @@ const dictionary: RawTranslationPackage = { SWITCH: '', SYD: 'ÅRSAVSKR', T: 'T', + TAKE: 'TAKE', TAN: 'TAN', TANH: 'TANH', TBILLEQ: 'SSVXEKV', diff --git a/src/i18n/languages/trTR.ts b/src/i18n/languages/trTR.ts index dc243600d..79c25f806 100644 --- a/src/i18n/languages/trTR.ts +++ b/src/i18n/languages/trTR.ts @@ -225,6 +225,7 @@ const dictionary: RawTranslationPackage = { SWITCH: '', SYD: 'YAT', T: 'T', + TAKE: 'TAKE', TAN: 'TAN', TANH: 'TANH', TBILLEQ: 'HTAHEŞ', diff --git a/src/interpreter/functionMetadata/categories/lookup-and-reference.ts b/src/interpreter/functionMetadata/categories/lookup-and-reference.ts index ef6b848d7..d6089cd0d 100644 --- a/src/interpreter/functionMetadata/categories/lookup-and-reference.ts +++ b/src/interpreter/functionMetadata/categories/lookup-and-reference.ts @@ -121,6 +121,13 @@ export const LOOKUP_AND_REFERENCE_DOCS: Record = { documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', examples: ['=SORT(A1:A10)', '=SORT(A1:B10, 2, -1)'], }, + TAKE: { + category: 'Lookup and reference', + shortDescription: 'Returns specified rows or columns from the beginning or end of an array.', + parameters: [{name: 'array', description: 'The array or range from which to take rows or columns.'}, {name: 'rows', description: 'The number of rows to take; a negative value takes rows from the end. An empty argument keeps all rows.'}, {name: 'columns', description: 'The number of columns to take; a negative value takes columns from the end. When omitted or empty, all columns are kept.'}], + documentationUrl: 'https://hyperformula.handsontable.com/docs/guide/built-in-functions.html', + examples: ['=TAKE(A1:C5, 2)', '=TAKE(A1:C5, -2, -1)', '=TAKE(A1:C5, , 2)'], + }, TRANSPOSE: { category: 'Lookup and reference', shortDescription: 'Transposes the rows and columns of an array.', diff --git a/src/interpreter/plugin/ArrayPlugin.ts b/src/interpreter/plugin/ArrayPlugin.ts index 27b47096e..b392dea54 100644 --- a/src/interpreter/plugin/ArrayPlugin.ts +++ b/src/interpreter/plugin/ArrayPlugin.ts @@ -43,6 +43,17 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche ], repeatLastArgs: 1, }, + 'TAKE': { + method: 'take', + sizeOfResultArrayMethod: 'takeArraySize', + enableArrayArithmeticForArguments: true, + parameters: [ + {argumentType: FunctionArgumentType.RANGE}, + {argumentType: FunctionArgumentType.NUMBER}, + {argumentType: FunctionArgumentType.NUMBER, optionalArg: true}, + ], + vectorizationForbidden: true, + }, 'VSTACK': { method: 'vstack', sizeOfResultArrayMethod: 'vstackArraySize', @@ -166,6 +177,69 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche return new ArraySize(width, height) } + /** + * Corresponds to TAKE(array, rows, [columns]). + * + * Returns the requested number of rows and columns from the beginning or end + * of the source array. Syntactically empty dimensions keep the corresponding + * source dimension, while evaluated zero counts produce an empty-range error. + * + * @param {ProcedureAst} ast - The parsed function-call AST node. + * @param {InterpreterState} state - The current interpreter state. + */ + public take(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + const rowsArg = ast.args[1] + const columnsArg = ast.args[2] + const rowsIsEmpty = rowsArg?.type === AstNodeType.EMPTY + const columnsIsMissingOrEmpty = columnsArg === undefined || columnsArg.type === AstNodeType.EMPTY + + return this.runFunction(ast.args, state, this.metadata('TAKE'), + (range: SimpleRangeValue, rows: number, columns: number | undefined) => { + const sourceHeight = range.height() + const sourceWidth = range.width() + const requestedRows = rowsIsEmpty ? sourceHeight : Math.trunc(rows) + const requestedColumns = columnsIsMissingOrEmpty || columns === undefined ? sourceWidth : Math.trunc(columns) + + if (requestedRows === 0 || requestedColumns === 0 || sourceHeight === 0 || sourceWidth === 0) { + return new CellError(ErrorType.NA, ErrorMessage.EmptyRange) + } + + const rowsToTake = Math.min(Math.abs(requestedRows), sourceHeight) + const columnsToTake = Math.min(Math.abs(requestedColumns), sourceWidth) + const startRow = requestedRows > 0 ? 0 : sourceHeight - rowsToTake + const startColumn = requestedColumns > 0 ? 0 : sourceWidth - columnsToTake + const result = range.data + .slice(startRow, startRow + rowsToTake) + .map(row => row.slice(startColumn, startColumn + columnsToTake)) + + return SimpleRangeValue.onlyValues(result) + } + ) + } + + /** + * Predicts TAKE's maximum result size from the source array. + * + * The requested dimensions can depend on evaluated expressions, so the source + * dimensions are used as a safe upper bound for the spill area. + * + * @param {ProcedureAst} ast - The parsed function-call AST node. + * @param {InterpreterState} state - The current interpreter state. + */ + public takeArraySize(ast: ProcedureAst, state: InterpreterState): ArraySize { + if (ast.args.length < 2 || ast.args.length > 3) { + return ArraySize.error() + } + + const metadata = this.metadata('TAKE') + const sourceSize = this.arraySizeForAst( + ast.args[0], + new InterpreterState(state.formulaAddress, state.arraysFlag || (metadata?.enableArrayArithmeticForArguments ?? false)), + ) + + return new ArraySize(sourceSize.width, sourceSize.height) + } + /** * Corresponds to VSTACK(array1, [array2], ...) * From efde710f2ad2ba8f1563cbe21c334dfb2b3e6148 Mon Sep 17 00:00:00 2001 From: tobiadefami Date: Tue, 4 Aug 2026 21:17:27 +0100 Subject: [PATCH 02/20] docs: align TAKE comments with array plugin style --- src/interpreter/plugin/ArrayPlugin.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/interpreter/plugin/ArrayPlugin.ts b/src/interpreter/plugin/ArrayPlugin.ts index b392dea54..402ace8a4 100644 --- a/src/interpreter/plugin/ArrayPlugin.ts +++ b/src/interpreter/plugin/ArrayPlugin.ts @@ -180,12 +180,11 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche /** * Corresponds to TAKE(array, rows, [columns]). * - * Returns the requested number of rows and columns from the beginning or end - * of the source array. Syntactically empty dimensions keep the corresponding - * source dimension, while evaluated zero counts produce an empty-range error. + * Returns rows and columns from the beginning or end of the source array. + * Empty dimensions keep all rows or columns. * - * @param {ProcedureAst} ast - The parsed function-call AST node. - * @param {InterpreterState} state - The current interpreter state. + * @param ast + * @param state */ public take(ast: ProcedureAst, state: InterpreterState): InterpreterValue { const rowsArg = ast.args[1] @@ -218,13 +217,11 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche } /** - * Predicts TAKE's maximum result size from the source array. - * - * The requested dimensions can depend on evaluated expressions, so the source - * dimensions are used as a safe upper bound for the spill area. + * Calculates the spilled array size of TAKE using the source dimensions as + * the upper bound. * - * @param {ProcedureAst} ast - The parsed function-call AST node. - * @param {InterpreterState} state - The current interpreter state. + * @param ast + * @param state */ public takeArraySize(ast: ProcedureAst, state: InterpreterState): ArraySize { if (ast.args.length < 2 || ast.args.length > 3) { From d76e2b4262b9edc1c5d52c9dbcfb0a4a2bec7c29 Mon Sep 17 00:00:00 2001 From: tobiadefami Date: Wed, 5 Aug 2026 12:11:38 +0100 Subject: [PATCH 03/20] fix: return CALC for empty TAKE results --- src/Cell.ts | 3 +++ src/i18n/languages/csCZ.ts | 1 + src/i18n/languages/daDK.ts | 1 + src/i18n/languages/deDE.ts | 1 + src/i18n/languages/enGB.ts | 1 + src/i18n/languages/esES.ts | 1 + src/i18n/languages/fiFI.ts | 1 + src/i18n/languages/frFR.ts | 1 + src/i18n/languages/huHU.ts | 1 + src/i18n/languages/idID.ts | 1 + src/i18n/languages/itIT.ts | 1 + src/i18n/languages/nbNO.ts | 1 + src/i18n/languages/nlNL.ts | 1 + src/i18n/languages/plPL.ts | 1 + src/i18n/languages/ptPT.ts | 1 + src/i18n/languages/ruRU.ts | 1 + src/i18n/languages/svSE.ts | 1 + src/i18n/languages/trTR.ts | 1 + src/interpreter/plugin/ArrayPlugin.ts | 5 +++-- 19 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/Cell.ts b/src/Cell.ts index faab24bab..6f460e9f0 100644 --- a/src/Cell.ts +++ b/src/Cell.ts @@ -23,6 +23,9 @@ import {AddressWithSheet} from './parser/Address' * Possible errors returned by our interpreter. */ export enum ErrorType { + /** Calculation error. */ + CALC = 'CALC', + /** Division by zero. */ DIV_BY_ZERO = 'DIV_BY_ZERO', diff --git a/src/i18n/languages/csCZ.ts b/src/i18n/languages/csCZ.ts index 254d65049..fa310523d 100644 --- a/src/i18n/languages/csCZ.ts +++ b/src/i18n/languages/csCZ.ts @@ -7,6 +7,7 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { + CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#DĚLENÍ_NULOU!', ERROR: '#ERROR!', diff --git a/src/i18n/languages/daDK.ts b/src/i18n/languages/daDK.ts index 0f5ed9234..587817507 100644 --- a/src/i18n/languages/daDK.ts +++ b/src/i18n/languages/daDK.ts @@ -7,6 +7,7 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { + CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#DIVISION/0!', ERROR: '#ERROR!', diff --git a/src/i18n/languages/deDE.ts b/src/i18n/languages/deDE.ts index 9aa3b3cb6..b21329940 100644 --- a/src/i18n/languages/deDE.ts +++ b/src/i18n/languages/deDE.ts @@ -7,6 +7,7 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { + CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#DIV/0!', ERROR: '#ERROR!', diff --git a/src/i18n/languages/enGB.ts b/src/i18n/languages/enGB.ts index e8286f5da..c133d912d 100644 --- a/src/i18n/languages/enGB.ts +++ b/src/i18n/languages/enGB.ts @@ -7,6 +7,7 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { + CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#DIV/0!', ERROR: '#ERROR!', diff --git a/src/i18n/languages/esES.ts b/src/i18n/languages/esES.ts index b41794de1..2d918fab1 100644 --- a/src/i18n/languages/esES.ts +++ b/src/i18n/languages/esES.ts @@ -7,6 +7,7 @@ import {RawTranslationPackage} from '..' export const dictionary: RawTranslationPackage = { errors: { + CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#¡DIV/0!', ERROR: '#ERROR!', diff --git a/src/i18n/languages/fiFI.ts b/src/i18n/languages/fiFI.ts index c80e18186..26685ab18 100644 --- a/src/i18n/languages/fiFI.ts +++ b/src/i18n/languages/fiFI.ts @@ -7,6 +7,7 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { + CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#JAKO/0!', ERROR: '#ERROR!', diff --git a/src/i18n/languages/frFR.ts b/src/i18n/languages/frFR.ts index 19f957351..00151e2b4 100644 --- a/src/i18n/languages/frFR.ts +++ b/src/i18n/languages/frFR.ts @@ -7,6 +7,7 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { + CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#DIV/0!', ERROR: '#ERROR!', diff --git a/src/i18n/languages/huHU.ts b/src/i18n/languages/huHU.ts index b8c175e4f..f9a95e083 100644 --- a/src/i18n/languages/huHU.ts +++ b/src/i18n/languages/huHU.ts @@ -7,6 +7,7 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { + CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#ZÉRÓOSZTÓ!', ERROR: '#ERROR!', diff --git a/src/i18n/languages/idID.ts b/src/i18n/languages/idID.ts index 337e32d57..afabf7e2b 100644 --- a/src/i18n/languages/idID.ts +++ b/src/i18n/languages/idID.ts @@ -7,6 +7,7 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { + CALC: '#CALC!', CYCLE: '#SIKLUS!', DIV_BY_ZERO: '#BAGI0!', ERROR: '#GALAT!', diff --git a/src/i18n/languages/itIT.ts b/src/i18n/languages/itIT.ts index 64dc5afa9..3c8257f40 100644 --- a/src/i18n/languages/itIT.ts +++ b/src/i18n/languages/itIT.ts @@ -7,6 +7,7 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { + CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#DIV/0!', ERROR: '#ERROR!', diff --git a/src/i18n/languages/nbNO.ts b/src/i18n/languages/nbNO.ts index 721d096f3..20e8a8fd8 100644 --- a/src/i18n/languages/nbNO.ts +++ b/src/i18n/languages/nbNO.ts @@ -7,6 +7,7 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { + CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#DIV/0!', ERROR: '#ERROR!', diff --git a/src/i18n/languages/nlNL.ts b/src/i18n/languages/nlNL.ts index d63edfccd..1f3aa00cf 100644 --- a/src/i18n/languages/nlNL.ts +++ b/src/i18n/languages/nlNL.ts @@ -7,6 +7,7 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { + CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#DELING.DOOR.0!', ERROR: '#ERROR!', diff --git a/src/i18n/languages/plPL.ts b/src/i18n/languages/plPL.ts index 22e181175..2df2445dd 100644 --- a/src/i18n/languages/plPL.ts +++ b/src/i18n/languages/plPL.ts @@ -7,6 +7,7 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { + CALC: '#CALC!', CYCLE: '#CYKL!', DIV_BY_ZERO: '#DZIEL/0!', ERROR: '#BŁĄD!', diff --git a/src/i18n/languages/ptPT.ts b/src/i18n/languages/ptPT.ts index e58a1ffc8..b376ebe03 100644 --- a/src/i18n/languages/ptPT.ts +++ b/src/i18n/languages/ptPT.ts @@ -7,6 +7,7 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { + CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#DIV/0!', ERROR: '#ERROR!', diff --git a/src/i18n/languages/ruRU.ts b/src/i18n/languages/ruRU.ts index d2213035c..2134e4e4b 100644 --- a/src/i18n/languages/ruRU.ts +++ b/src/i18n/languages/ruRU.ts @@ -7,6 +7,7 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { + CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#ДЕЛ/0!', ERROR: '#ERROR!', diff --git a/src/i18n/languages/svSE.ts b/src/i18n/languages/svSE.ts index ef7d34069..765a9d301 100644 --- a/src/i18n/languages/svSE.ts +++ b/src/i18n/languages/svSE.ts @@ -7,6 +7,7 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { + CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#DIVISION/0!', ERROR: '#ERROR!', diff --git a/src/i18n/languages/trTR.ts b/src/i18n/languages/trTR.ts index 79c25f806..b09020d34 100644 --- a/src/i18n/languages/trTR.ts +++ b/src/i18n/languages/trTR.ts @@ -7,6 +7,7 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { + CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#SAYI/0!', ERROR: '#ERROR!', diff --git a/src/interpreter/plugin/ArrayPlugin.ts b/src/interpreter/plugin/ArrayPlugin.ts index 402ace8a4..7942680ea 100644 --- a/src/interpreter/plugin/ArrayPlugin.ts +++ b/src/interpreter/plugin/ArrayPlugin.ts @@ -181,7 +181,8 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche * Corresponds to TAKE(array, rows, [columns]). * * Returns rows and columns from the beginning or end of the source array. - * Empty dimensions keep all rows or columns. + * Syntactically empty dimensions keep all rows or columns. Counts that + * evaluate to zero return a #CALC! error. * * @param ast * @param state @@ -200,7 +201,7 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche const requestedColumns = columnsIsMissingOrEmpty || columns === undefined ? sourceWidth : Math.trunc(columns) if (requestedRows === 0 || requestedColumns === 0 || sourceHeight === 0 || sourceWidth === 0) { - return new CellError(ErrorType.NA, ErrorMessage.EmptyRange) + return new CellError(ErrorType.CALC, ErrorMessage.EmptyRange) } const rowsToTake = Math.min(Math.abs(requestedRows), sourceHeight) From 2174419574321482dbb316fb295f66af960787c5 Mon Sep 17 00:00:00 2001 From: tobiadefami Date: Wed, 5 Aug 2026 12:11:43 +0100 Subject: [PATCH 04/20] docs: document TAKE CALC behavior --- CHANGELOG.md | 2 +- docs/guide/known-limitations.md | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 212e9e2fe..bd72cf8de 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 the `TAKE` dynamic-array function. +- Added the `TAKE` dynamic-array function and the `CALC` error type used for empty-array results. ## [3.4.0] - 2026-08-10 diff --git a/docs/guide/known-limitations.md b/docs/guide/known-limitations.md index aeb932772..7f0e2361d 100644 --- a/docs/guide/known-limitations.md +++ b/docs/guide/known-limitations.md @@ -51,10 +51,6 @@ a circular reference. * Ordering (including mixed types, empty cells, and text collation) follows HyperFormula's own comparison rules, which honor the `caseSensitive` and `accentSensitive` configuration options. Numbers sort before text, and text before logical values. -### TAKE function - -* When a non-empty `rows` or `columns` argument evaluates to 0, or when the source has no effective rows or columns, `TAKE` returns `#N/A` with the "Empty range not allowed" message. Microsoft Excel returns `#CALC!` for a zero-count empty-array result, but HyperFormula does not support the `#CALC!` error type. - ### OFFSET function HyperFormula resolves the OFFSET function at parse time rather than during evaluation. The parser inspects the arguments and rewrites the expression into a plain cell reference or range. This keeps the dependency graph accurate but imposes several restrictions. From 1f3e435ba3781daa41bc1dbe82286e8589a46bbc Mon Sep 17 00:00:00 2001 From: tobiadefami Date: Thu, 6 Aug 2026 22:38:21 +0100 Subject: [PATCH 05/20] docs: link TAKE PR in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd72cf8de..92a47f304 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 the `TAKE` dynamic-array function and the `CALC` error type used for empty-array results. +- Added the `TAKE` dynamic-array function and the `CALC` error type used for empty-array results. [#1722](https://github.com/handsontable/hyperformula/pull/1722) ## [3.4.0] - 2026-08-10 From a66b330b6edb3e3f5c3d110ad770b7064711cb62 Mon Sep 17 00:00:00 2001 From: tobiadefami Date: Fri, 7 Aug 2026 16:49:29 +0100 Subject: [PATCH 06/20] fix: refine TAKE argument and error handling --- docs/guide/types-of-errors.md | 3 +- src/error-message.ts | 1 + .../buildFunctionDescriptions.ts | 8 +++-- src/interpreter/plugin/ArrayPlugin.ts | 21 ++++++------ src/interpreter/plugin/FunctionPlugin.ts | 34 +++++++++++++++---- 5 files changed, 45 insertions(+), 22 deletions(-) diff --git a/docs/guide/types-of-errors.md b/docs/guide/types-of-errors.md index 7f8546a90..038415c2c 100644 --- a/docs/guide/types-of-errors.md +++ b/docs/guide/types-of-errors.md @@ -21,6 +21,7 @@ according to the language settings. | #NUM! | Invalid number | This error arises when your formula contains an invalid number. | | #REF! | Invalid reference | It occurs when a formula contains an invalid reference. It is one of the most common errors users encounter when working with spreadsheets. | | #VALUE! | Wrong type of argument | It occurs when a formula tries to improperly use different types of data. For example, you will see this error when you will try to add a string to a number. | +| #CALC! | Calculation error | It occurs when a formula cannot produce a valid array result, such as when TAKE requests zero rows or columns. | | #CYCLE! | Circular reference | It occurs when a formula refers to its own cell, both directly and indirectly. | | #ERROR! | An error occurred | It indicates that there is an unknown error in a formula. | -| #LIC! | Invalid license key | It occurs when the license key is invalid, expired, or missing. | \ No newline at end of file +| #LIC! | Invalid license key | It occurs when the license key is invalid, expired, or missing. | diff --git a/src/error-message.ts b/src/error-message.ts index 5e3afdbea..0d9dfb7b9 100644 --- a/src/error-message.ts +++ b/src/error-message.ts @@ -66,6 +66,7 @@ export class ErrorMessage { public static IncorrectDateTime = 'String does not represent correct DateTime.' public static CharacterCodeBounds = 'Character code out of bounds.' public static NonZero = 'Argument cannot be 0.' + public static ZeroRowOrColumnCount = 'Row or column count cannot be zero.' public static LessThanOne = 'Argument cannot be less than 1.' public static WeekendString = 'Incorrect weekend bitmask string.' public static InvalidRoman = 'Invalid roman numeral.' diff --git a/src/interpreter/functionMetadata/buildFunctionDescriptions.ts b/src/interpreter/functionMetadata/buildFunctionDescriptions.ts index 8d1cdde77..176f2e1e6 100644 --- a/src/interpreter/functionMetadata/buildFunctionDescriptions.ts +++ b/src/interpreter/functionMetadata/buildFunctionDescriptions.ts @@ -23,7 +23,7 @@ * placeholder. */ -import {FunctionMetadata, FunctionArgument} from '../plugin/FunctionPlugin' +import {FunctionMetadata, FunctionArgument, isFunctionArgumentOptional} from '../plugin/FunctionPlugin' import {CUSTOM_FUNCTION_CATEGORY, FunctionDoc, FunctionListEntry, FunctionDetails, FunctionParameterDescription} from './FunctionDescription' /** Resolves a function's display name: the translation for the active language, or the canonical id as fallback. */ @@ -33,12 +33,14 @@ type TranslateName = (canonicalName: string) => string | undefined export type StructuralMetadata = Pick /** - * Returns whether a parameter may be omitted: it declares `optionalArg`, or it has a `defaultValue`. + * Returns whether a parameter may be omitted. An explicit `optionalArg` + * declaration takes precedence; otherwise, a `defaultValue` makes the + * parameter optional. * * @param {FunctionArgument | undefined} arg - the structural argument metadata, or `undefined` */ export function isParameterOptional(arg: FunctionArgument | undefined): boolean { - return arg?.optionalArg === true || arg?.defaultValue !== undefined + return isFunctionArgumentOptional(arg) } /** diff --git a/src/interpreter/plugin/ArrayPlugin.ts b/src/interpreter/plugin/ArrayPlugin.ts index 7942680ea..3fe058433 100644 --- a/src/interpreter/plugin/ArrayPlugin.ts +++ b/src/interpreter/plugin/ArrayPlugin.ts @@ -49,8 +49,8 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche enableArrayArithmeticForArguments: true, parameters: [ {argumentType: FunctionArgumentType.RANGE}, - {argumentType: FunctionArgumentType.NUMBER}, - {argumentType: FunctionArgumentType.NUMBER, optionalArg: true}, + {argumentType: FunctionArgumentType.NUMBER, optionalArg: false, defaultValue: Number.POSITIVE_INFINITY, emptyAsDefault: true}, + {argumentType: FunctionArgumentType.NUMBER, optionalArg: true, defaultValue: Number.POSITIVE_INFINITY, emptyAsDefault: true}, ], vectorizationForbidden: true, }, @@ -188,19 +188,18 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche * @param state */ public take(ast: ProcedureAst, state: InterpreterState): InterpreterValue { - const rowsArg = ast.args[1] - const columnsArg = ast.args[2] - const rowsIsEmpty = rowsArg?.type === AstNodeType.EMPTY - const columnsIsMissingOrEmpty = columnsArg === undefined || columnsArg.type === AstNodeType.EMPTY - return this.runFunction(ast.args, state, this.metadata('TAKE'), - (range: SimpleRangeValue, rows: number, columns: number | undefined) => { + (range: SimpleRangeValue, rows: number, columns: number) => { const sourceHeight = range.height() const sourceWidth = range.width() - const requestedRows = rowsIsEmpty ? sourceHeight : Math.trunc(rows) - const requestedColumns = columnsIsMissingOrEmpty || columns === undefined ? sourceWidth : Math.trunc(columns) + const requestedRows = Math.trunc(rows) + const requestedColumns = Math.trunc(columns) + + if (requestedRows === 0 || requestedColumns === 0) { + return new CellError(ErrorType.CALC, ErrorMessage.ZeroRowOrColumnCount) + } - if (requestedRows === 0 || requestedColumns === 0 || sourceHeight === 0 || sourceWidth === 0) { + if (sourceHeight === 0 || sourceWidth === 0) { return new CellError(ErrorType.CALC, ErrorMessage.EmptyRange) } diff --git a/src/interpreter/plugin/FunctionPlugin.ts b/src/interpreter/plugin/FunctionPlugin.ts index 810f14047..b1e0b1fa6 100644 --- a/src/interpreter/plugin/FunctionPlugin.ts +++ b/src/interpreter/plugin/FunctionPlugin.ts @@ -200,15 +200,18 @@ export interface FunctionArgument { passSubtype?: boolean, /** - * If an argument is missing, its value defaults to `defaultValue`. + * If an argument is missing and omission is allowed, its value defaults to + * `defaultValue`. Declaring a default makes the argument optional unless + * `optionalArg` is explicitly set to `false`. */ defaultValue?: InternalScalarValue | RawScalarValue, /** - * If set to `true`: - * if an argument is missing, and no `defaultValue` is set, the argument is `undefined` (instead of throwing an error). + * Explicitly controls whether the argument may be omitted. When omitted, + * the presence of `defaultValue` makes the argument optional. * - * This is logically equivalent to setting `defaultValue` to `undefined`. + * If set to `true` without a `defaultValue`, a missing argument is passed as + * `undefined` instead of producing an error. */ optionalArg?: boolean, @@ -233,8 +236,9 @@ export interface FunctionArgument { greaterThan?: number, /** - * If set to `true`, an empty argument is treated as if the argument was not - * provided at all — that is, it falls back to `defaultValue`. + * If set to `true`, a syntactically empty argument falls back to + * `defaultValue`. This does not make the argument optional; `optionalArg` + * controls whether the argument may be omitted. * * By default (`false`), an empty argument is coerced to the zero-value for its * type (`0` for numbers, `FALSE` for booleans, `""` for strings). @@ -249,6 +253,22 @@ export interface FunctionArgument { emptyAsDefault?: boolean, } +/** + * Returns whether a function argument may be omitted. + * + * An explicit `optionalArg` declaration takes precedence. Otherwise, the + * presence of a default value preserves the existing implicit-optional + * behavior. + * + * @param {FunctionArgument | undefined} argument - The argument metadata to inspect. + */ +export function isFunctionArgumentOptional(argument: FunctionArgument | undefined): boolean { + if (argument?.optionalArg !== undefined) { + return argument.optionalArg + } + return argument?.defaultValue !== undefined +} + export type PluginFunctionType = (ast: ProcedureAst, state: InterpreterState) => InterpreterValue export type PluginArraySizeFunctionType = (ast: ProcedureAst, state: InterpreterState) => ArraySize @@ -539,7 +559,7 @@ export abstract class FunctionPlugin implements FunctionPluginTypecheck argMetadata?.optionalArg || argMetadata?.defaultValue !== undefined) + const areMissingArgumentsOptional = metadataForMissingArguments.every(isFunctionArgumentOptional) return areMissingArgumentsOptional } From 14da55aff0ebbb3119bf774f0133e2d0c0c19337 Mon Sep 17 00:00:00 2001 From: tobiadefami Date: Fri, 7 Aug 2026 17:57:03 +0100 Subject: [PATCH 07/20] fix: predict TAKE size from literal counts --- src/interpreter/plugin/ArrayPlugin.ts | 34 +++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/interpreter/plugin/ArrayPlugin.ts b/src/interpreter/plugin/ArrayPlugin.ts index 3fe058433..46e0dca35 100644 --- a/src/interpreter/plugin/ArrayPlugin.ts +++ b/src/interpreter/plugin/ArrayPlugin.ts @@ -6,7 +6,7 @@ import {ArraySize} from '../../ArraySize' import {CellError, ErrorType} from '../../Cell' import {ErrorMessage} from '../../error-message' -import {AstNodeType, ProcedureAst} from '../../parser' +import {Ast, AstNodeType, ProcedureAst} from '../../parser' import {coerceScalarToBoolean} from '../ArithmeticHelper' import {InterpreterState} from '../InterpreterState' import {InternalScalarValue, InterpreterValue} from '../InterpreterValue' @@ -14,6 +14,27 @@ import {SimpleRangeValue} from '../../SimpleRangeValue' import {FunctionArgumentType, FunctionPlugin, FunctionPluginTypecheck, ImplementedFunctions} from './FunctionPlugin' export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypecheck { + /** + * Converts a numeric TAKE count literal into its predicted result dimension. + * + * @param {Ast | undefined} argument - The count argument to inspect before evaluation. + * @returns {number | undefined} The truncated absolute count, or `undefined` when it is not a numeric literal. + */ + private static parseTakeLiteralDimension(argument: Ast | undefined): number | undefined { + if (argument?.type === AstNodeType.NUMBER) { + return Math.abs(Math.trunc(argument.value)) + } + + if ( + (argument?.type === AstNodeType.PLUS_UNARY_OP || argument?.type === AstNodeType.MINUS_UNARY_OP) + && argument.value.type === AstNodeType.NUMBER + ) { + return Math.abs(Math.trunc(argument.value.value)) + } + + return undefined + } + public static implementedFunctions: ImplementedFunctions = { 'ARRAYFORMULA': { method: 'arrayformula', @@ -234,7 +255,16 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche new InterpreterState(state.formulaAddress, state.arraysFlag || (metadata?.enableArrayArithmeticForArguments ?? false)), ) - return new ArraySize(sourceSize.width, sourceSize.height) + const literalRows = ArrayPlugin.parseTakeLiteralDimension(ast.args[1]) + const literalColumns = ArrayPlugin.parseTakeLiteralDimension(ast.args[2]) + const height = literalRows === undefined ? sourceSize.height : Math.min(sourceSize.height, literalRows) + const width = literalColumns === undefined ? sourceSize.width : Math.min(sourceSize.width, literalColumns) + + if (height < 1 || width < 1) { + return ArraySize.error() + } + + return new ArraySize(width, height) } /** From 7ed72560ce6858650f214f4b247cba2d8d914df4 Mon Sep 17 00:00:00 2001 From: tobiadefami Date: Fri, 7 Aug 2026 20:46:44 +0100 Subject: [PATCH 08/20] perf: avoid materializing unused TAKE cells --- src/interpreter/plugin/ArrayPlugin.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/interpreter/plugin/ArrayPlugin.ts b/src/interpreter/plugin/ArrayPlugin.ts index 46e0dca35..b406a3796 100644 --- a/src/interpreter/plugin/ArrayPlugin.ts +++ b/src/interpreter/plugin/ArrayPlugin.ts @@ -3,6 +3,7 @@ * Copyright (c) 2025 Handsoncode. All rights reserved. */ +import {AbsoluteCellRange} from '../../AbsoluteCellRange' import {ArraySize} from '../../ArraySize' import {CellError, ErrorType} from '../../Cell' import {ErrorMessage} from '../../error-message' @@ -228,6 +229,18 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche const columnsToTake = Math.min(Math.abs(requestedColumns), sourceWidth) const startRow = requestedRows > 0 ? 0 : sourceHeight - rowsToTake const startColumn = requestedColumns > 0 ? 0 : sourceWidth - columnsToTake + const sourceRange = range.range + + // Keep address-backed ranges lazy to avoid materializing cells outside the TAKE result. + if (sourceRange !== undefined) { + const resultRange = AbsoluteCellRange.spanFrom( + sourceRange.getAddress(startColumn, startRow), + columnsToTake, + rowsToTake, + ) + return SimpleRangeValue.onlyRange(resultRange, this.dependencyGraph) + } + const result = range.data .slice(startRow, startRow + rowsToTake) .map(row => row.slice(startColumn, startColumn + columnsToTake)) From b902d53bcf31521de73739c9bc7d0000140eee09 Mon Sep 17 00:00:00 2001 From: Kuba Sekowski Date: Mon, 10 Aug 2026 17:40:29 +0200 Subject: [PATCH 09/20] i18n: use Excel's official names for TAKE, VSTACK and HSTACK TAKE shipped with the English name in 10 of 16 packs while 6 carried a translation, and VSTACK/HSTACK were English in all 16. Microsoft localizes all three in most locales, so a user could not type the name their Excel uses. Names taken from Microsoft's localized "Excel functions (alphabetical)" page, one locale at a time. Each row there links to the function's own page using the English slug in the href while the link text is the localized name, so the lookup is exact: WYCINEK Left as English where Microsoft itself does not translate: TAKE, VSTACK and HSTACK in Indonesian, and VSTACK/HSTACK in Swedish. Note that a function's own localized page is not a usable source: for several locales its syntax block still shows the English name even though the prose and argument names are translated (the French page shows "=TAKE(tableau, lignes,[colonnes])" while the product uses PRENDRE). DEV_DOCS records the lookup method and adds the governing policy: ship a localized name only when it can be confirmed against the product, and keep the English name otherwise, since an invented name matches nothing, reads plausibly enough to be typed first, and fails as #NAME?. No changelog entry: TAKE, VSTACK and HSTACK are all still in [Unreleased], so no wrong name has been released. Co-Authored-By: Claude Opus 5 --- DEV_DOCS.md | 36 +++++++++++++++++++++++++++++++++++- src/i18n/languages/csCZ.ts | 6 +++--- src/i18n/languages/daDK.ts | 4 ++-- src/i18n/languages/deDE.ts | 4 ++-- src/i18n/languages/esES.ts | 4 ++-- src/i18n/languages/fiFI.ts | 6 +++--- src/i18n/languages/frFR.ts | 6 +++--- src/i18n/languages/huHU.ts | 6 +++--- src/i18n/languages/itIT.ts | 6 +++--- src/i18n/languages/nbNO.ts | 6 +++--- src/i18n/languages/nlNL.ts | 4 ++-- src/i18n/languages/plPL.ts | 6 +++--- src/i18n/languages/ptPT.ts | 6 +++--- src/i18n/languages/ruRU.ts | 4 ++-- src/i18n/languages/svSE.ts | 2 +- src/i18n/languages/trTR.ts | 6 +++--- 16 files changed, 73 insertions(+), 39 deletions(-) diff --git a/DEV_DOCS.md b/DEV_DOCS.md index 13630fd4c..431433ed1 100644 --- a/DEV_DOCS.md +++ b/DEV_DOCS.md @@ -159,12 +159,46 @@ Descriptions must describe **HyperFormula's** behaviour, not Excel's. Much of th HyperFormula supports internationalization and provides localized function names for all built-in languages. Translation files live in `src/i18n/languages/`. New functions must include translations for all built-in languages. +### Policy: only ship a name you can source + +Add a localized function name **only when you have confirmed that Microsoft Excel ships that exact name in the product**. If you cannot confirm it, keep the English name. + +An unverified translation is worse than the English fallback. The English name at least matches what Excel uses in the locales Microsoft leaves untranslated, and users can look it up; an invented one matches nothing, is what users will type first because it reads plausibly, and silently produces `#NAME?`. It is also hard to spot in review — a made-up name looks exactly like a real one to anyone who does not speak the language. + +Two things this rules out: + +- Translating the English word yourself, or having a tool do it. A dictionary rendering is not what Excel ships: `TAKE` is `WYCINEK` in Polish (not "WEŹ"), `INCLUDI` in Italian, and `ÁTHELYEZ` in Hungarian — none of which a translator would produce from the word "take". +- Guessing from a sibling function. Names within one release batch are not translated consistently: Swedish localizes `TAKE` as `TA` but keeps `VSTACK`/`HSTACK` in English. + +Keeping the English name is a legitimate, complete answer, and it is what Microsoft itself does in several locales. Prefer it over a guess. + When looking for the valid translations for new functions, try these sources: - https://support.microsoft.com/en-us/office/excel-functions-translator-f262d0c0-991c-485b-89b6-32cc8d326889 - http://dolf.trieschnigg.nl/excel/index.php -For languages not officially supported by Microsoft Excel, the two sources above do not apply. For these languages, use Google Sheets as the reference. Switch the `hl` query parameter to the target locale, for example: +### Reading the translations off Microsoft's localized function list + +The two sources above are a manual add-in and a third-party table that only covers older functions, so neither answers "what is `TAKE` called in Czech?". The **alphabetical function list** does, for every locale at once: + +``` +https://support.microsoft.com//office/excel-functions-alphabetical-b3944572-255d-4efb-bb96-c6d90033e188 +``` + +Each row links to the function's own page using the **English slug** in the `href` while the link text is the **localized name**, which makes the lookup exact rather than a guess: + +```html +WYCINEK
(2024) +``` + +So `grep`ping the page for `functions/-function` and reading the anchor text gives the authoritative name. Do this per locale (`pl-pl`, `cs-cz`, `fi-fi`, …) rather than trusting a single translated page. + +Two traps this avoids: + +- **A function's own localized page is not a reliable source.** For several locales the syntax block is still the English one even though the surrounding prose and the argument names are translated — the French page shows `=TAKE(tableau, lignes,[colonnes])` and the Hungarian one `=TAKE(tömb, sorok,[oszlopok])`, while the product actually uses `PRENDRE` and `ÁTHELYEZ`. Reading the function page alone concludes "not translated" and is wrong. +- **Do not translate the name yourself.** A dictionary rendering of the English word is not what Excel ships: `TAKE` is `WYCINEK` in Polish (not "WEŹ"), `INCLUDI` in Italian, `ÁTHELYEZ` in Hungarian, and `TA` in both Swedish and Norwegian. Where the list genuinely keeps the English name — `VSTACK`/`HSTACK` in Swedish and Indonesian, `TAKE` in Indonesian — keep it too. + +For languages not officially supported by Microsoft Excel, none of the sources above apply. For these languages, use Google Sheets as the reference. Switch the `hl` query parameter to the target locale, for example: - https://support.google.com/docs/table/25273?hl=id (Indonesian) diff --git a/src/i18n/languages/csCZ.ts b/src/i18n/languages/csCZ.ts index fa310523d..e4a62b922 100644 --- a/src/i18n/languages/csCZ.ts +++ b/src/i18n/languages/csCZ.ts @@ -20,8 +20,8 @@ const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', - VSTACK: 'VSTACK', - HSTACK: 'HSTACK', + VSTACK: 'SROVNAT.SVISLE', + HSTACK: 'SROVNAT.VODOROVNĚ', ADDRESS: 'ODKAZ', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', @@ -226,7 +226,7 @@ const dictionary: RawTranslationPackage = { SWITCH: '', SYD: 'ODPIS.NELIN', T: 'T', - TAKE: 'TAKE', + TAKE: 'VZÍT', TAN: 'TG', TANH: 'TGH', TBILLEQ: 'TBILLEQ', diff --git a/src/i18n/languages/daDK.ts b/src/i18n/languages/daDK.ts index 587817507..60de6b246 100644 --- a/src/i18n/languages/daDK.ts +++ b/src/i18n/languages/daDK.ts @@ -20,8 +20,8 @@ const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', - VSTACK: 'VSTACK', - HSTACK: 'HSTACK', + VSTACK: 'LSTAK', + HSTACK: 'VSTAK', ADDRESS: 'ADRESSE', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', diff --git a/src/i18n/languages/deDE.ts b/src/i18n/languages/deDE.ts index b21329940..2a267fba5 100644 --- a/src/i18n/languages/deDE.ts +++ b/src/i18n/languages/deDE.ts @@ -20,8 +20,8 @@ const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', - VSTACK: 'VSTACK', - HSTACK: 'HSTACK', + VSTACK: 'VSTAPELN', + HSTACK: 'HSTAPELN', ADDRESS: 'ADRESSE', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', diff --git a/src/i18n/languages/esES.ts b/src/i18n/languages/esES.ts index 2d918fab1..f9047cbdd 100644 --- a/src/i18n/languages/esES.ts +++ b/src/i18n/languages/esES.ts @@ -20,8 +20,8 @@ export const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', - VSTACK: 'VSTACK', - HSTACK: 'HSTACK', + VSTACK: 'APILARV', + HSTACK: 'APILARH', ADDRESS: 'DIRECCION', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', diff --git a/src/i18n/languages/fiFI.ts b/src/i18n/languages/fiFI.ts index 26685ab18..029df4a8f 100644 --- a/src/i18n/languages/fiFI.ts +++ b/src/i18n/languages/fiFI.ts @@ -20,8 +20,8 @@ const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', - VSTACK: 'VSTACK', - HSTACK: 'HSTACK', + VSTACK: 'VPINO', + HSTACK: 'HPINO', ADDRESS: 'OSOITE', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', @@ -226,7 +226,7 @@ const dictionary: RawTranslationPackage = { SWITCH: '', SYD: 'VUOSIPOISTO', T: 'T', - TAKE: 'TAKE', + TAKE: 'OTA', TAN: 'TAN', TANH: 'TANH', TBILLEQ: 'OBLIG.TUOTTOPROS', diff --git a/src/i18n/languages/frFR.ts b/src/i18n/languages/frFR.ts index 00151e2b4..0ca1d7a0e 100644 --- a/src/i18n/languages/frFR.ts +++ b/src/i18n/languages/frFR.ts @@ -20,8 +20,8 @@ const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', - VSTACK: 'VSTACK', - HSTACK: 'HSTACK', + VSTACK: 'ASSEMB.V', + HSTACK: 'ASSEMB.H', ADDRESS: 'ADRESSE', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', @@ -226,7 +226,7 @@ const dictionary: RawTranslationPackage = { SWITCH: '', SYD: 'SYD', T: 'T', - TAKE: 'TAKE', + TAKE: 'PRENDRE', TAN: 'TAN', TANH: 'TANH', TBILLEQ: 'TAUX.ESCOMPTE.R', diff --git a/src/i18n/languages/huHU.ts b/src/i18n/languages/huHU.ts index f9a95e083..e5a46d082 100644 --- a/src/i18n/languages/huHU.ts +++ b/src/i18n/languages/huHU.ts @@ -20,8 +20,8 @@ const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', - VSTACK: 'VSTACK', - HSTACK: 'HSTACK', + VSTACK: 'FÜGG.HALMOZÁS', + HSTACK: 'VÍZSZ.HALMOZÁS', ADDRESS: 'CÍM', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', @@ -226,7 +226,7 @@ const dictionary: RawTranslationPackage = { SWITCH: '', SYD: 'ÉSZÖ', T: 'T', - TAKE: 'TAKE', + TAKE: 'ÁTHELYEZ', TAN: 'TAN', TANH: 'TANH', TBILLEQ: 'KJEGY.EGYENÉRT', diff --git a/src/i18n/languages/itIT.ts b/src/i18n/languages/itIT.ts index 3c8257f40..789a758bf 100644 --- a/src/i18n/languages/itIT.ts +++ b/src/i18n/languages/itIT.ts @@ -20,8 +20,8 @@ const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', - VSTACK: 'VSTACK', - HSTACK: 'HSTACK', + VSTACK: 'STACK.VERT', + HSTACK: 'STACK.ORIZ', ADDRESS: 'INDIRIZZO', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', @@ -226,7 +226,7 @@ const dictionary: RawTranslationPackage = { SWITCH: '', SYD: 'AMMORT.ANNUO', T: 'T', - TAKE: 'TAKE', + TAKE: 'INCLUDI', TAN: 'TAN', TANH: 'TANH', TBILLEQ: 'BOT.EQUIV', diff --git a/src/i18n/languages/nbNO.ts b/src/i18n/languages/nbNO.ts index 20e8a8fd8..28f594b15 100644 --- a/src/i18n/languages/nbNO.ts +++ b/src/i18n/languages/nbNO.ts @@ -20,8 +20,8 @@ const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', - VSTACK: 'VSTACK', - HSTACK: 'HSTACK', + VSTACK: 'VSTAKK', + HSTACK: 'HSTAKK', ADDRESS: 'ADRESSE', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', @@ -226,7 +226,7 @@ const dictionary: RawTranslationPackage = { SWITCH: '', SYD: 'ÅRSAVS', T: 'T', - TAKE: 'TAKE', + TAKE: 'TA', TAN: 'TAN', TANH: 'TANH', TBILLEQ: 'TBILLEKV', diff --git a/src/i18n/languages/nlNL.ts b/src/i18n/languages/nlNL.ts index 1f3aa00cf..838e58305 100644 --- a/src/i18n/languages/nlNL.ts +++ b/src/i18n/languages/nlNL.ts @@ -20,8 +20,8 @@ const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', - VSTACK: 'VSTACK', - HSTACK: 'HSTACK', + VSTACK: 'VERT.STAPELEN', + HSTACK: 'HOR.STAPELEN', ADDRESS: 'ADRES', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', diff --git a/src/i18n/languages/plPL.ts b/src/i18n/languages/plPL.ts index 2df2445dd..7275a746d 100644 --- a/src/i18n/languages/plPL.ts +++ b/src/i18n/languages/plPL.ts @@ -20,8 +20,8 @@ const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', - VSTACK: 'VSTACK', - HSTACK: 'HSTACK', + VSTACK: 'STOS.PION', + HSTACK: 'STOS.POZ', ADDRESS: 'ADRES', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', @@ -226,7 +226,7 @@ const dictionary: RawTranslationPackage = { SWITCH: 'PRZEŁĄCZ', SYD: 'SYD', T: 'T', - TAKE: 'TAKE', + TAKE: 'WYCINEK', TAN: 'TAN', TANH: 'TANH', TBILLEQ: 'RENT.EKW.BS', diff --git a/src/i18n/languages/ptPT.ts b/src/i18n/languages/ptPT.ts index b376ebe03..d8e46ad2e 100644 --- a/src/i18n/languages/ptPT.ts +++ b/src/i18n/languages/ptPT.ts @@ -20,8 +20,8 @@ const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', - VSTACK: 'VSTACK', - HSTACK: 'HSTACK', + VSTACK: 'JUNTARV', + HSTACK: 'JUNTARH', ADDRESS: 'ENDEREÇO', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', @@ -226,7 +226,7 @@ const dictionary: RawTranslationPackage = { SWITCH: '', SYD: 'SDA', T: 'T', - TAKE: 'TAKE', + TAKE: 'INCLUIR', TAN: 'TAN', TANH: 'TANH', TBILLEQ: 'OTN', diff --git a/src/i18n/languages/ruRU.ts b/src/i18n/languages/ruRU.ts index 2134e4e4b..6dce4df00 100644 --- a/src/i18n/languages/ruRU.ts +++ b/src/i18n/languages/ruRU.ts @@ -20,8 +20,8 @@ const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', - VSTACK: 'VSTACK', - HSTACK: 'HSTACK', + VSTACK: 'ВСТОЛБИК', + HSTACK: 'ГСТОЛБИК', ADDRESS: 'АДРЕС', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', diff --git a/src/i18n/languages/svSE.ts b/src/i18n/languages/svSE.ts index 765a9d301..4190105aa 100644 --- a/src/i18n/languages/svSE.ts +++ b/src/i18n/languages/svSE.ts @@ -226,7 +226,7 @@ const dictionary: RawTranslationPackage = { SWITCH: '', SYD: 'ÅRSAVSKR', T: 'T', - TAKE: 'TAKE', + TAKE: 'TA', TAN: 'TAN', TANH: 'TANH', TBILLEQ: 'SSVXEKV', diff --git a/src/i18n/languages/trTR.ts b/src/i18n/languages/trTR.ts index b09020d34..b490bc201 100644 --- a/src/i18n/languages/trTR.ts +++ b/src/i18n/languages/trTR.ts @@ -20,8 +20,8 @@ const dictionary: RawTranslationPackage = { }, functions: { FILTER: 'FILTER', - VSTACK: 'VSTACK', - HSTACK: 'HSTACK', + VSTACK: 'DÜŞEYYIĞ', + HSTACK: 'YATAYYIĞ', ADDRESS: 'ADRES', 'ARRAY_CONSTRAIN': 'ARRAY_CONSTRAIN', ARRAYFORMULA: 'ARRAYFORMULA', @@ -226,7 +226,7 @@ const dictionary: RawTranslationPackage = { SWITCH: '', SYD: 'YAT', T: 'T', - TAKE: 'TAKE', + TAKE: 'AL', TAN: 'TAN', TANH: 'TANH', TBILLEQ: 'HTAHEŞ', From 417202387b4a9b95e3d11b70968252a83f1df9d5 Mon Sep 17 00:00:00 2001 From: tobiadefami Date: Tue, 11 Aug 2026 15:26:33 +0100 Subject: [PATCH 10/20] fix: avoid breaking language packs for TAKE --- CHANGELOG.md | 2 +- docs/guide/list-of-differences.md | 1 + docs/guide/types-of-errors.md | 1 - src/Cell.ts | 3 --- src/i18n/languages/csCZ.ts | 1 - src/i18n/languages/daDK.ts | 1 - src/i18n/languages/deDE.ts | 1 - src/i18n/languages/enGB.ts | 1 - src/i18n/languages/esES.ts | 1 - src/i18n/languages/fiFI.ts | 1 - src/i18n/languages/frFR.ts | 1 - src/i18n/languages/huHU.ts | 1 - src/i18n/languages/idID.ts | 1 - src/i18n/languages/itIT.ts | 1 - src/i18n/languages/nbNO.ts | 1 - src/i18n/languages/nlNL.ts | 1 - src/i18n/languages/plPL.ts | 1 - src/i18n/languages/ptPT.ts | 1 - src/i18n/languages/ruRU.ts | 1 - src/i18n/languages/svSE.ts | 1 - src/i18n/languages/trTR.ts | 1 - src/interpreter/plugin/ArrayPlugin.ts | 6 +++--- 22 files changed, 5 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92a47f304..5851706f4 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 the `TAKE` dynamic-array function and the `CALC` error type used for empty-array results. [#1722](https://github.com/handsontable/hyperformula/pull/1722) +- Added the `TAKE` dynamic-array function. [#1722](https://github.com/handsontable/hyperformula/pull/1722) ## [3.4.0] - 2026-08-10 diff --git a/docs/guide/list-of-differences.md b/docs/guide/list-of-differences.md index fae264e4b..e4df0258c 100644 --- a/docs/guide/list-of-differences.md +++ b/docs/guide/list-of-differences.md @@ -106,6 +106,7 @@ To remove the differences, create [custom implementations](custom-functions.md) | NORMSDIST | =NORMSDIST(0, TRUE()) | 0.5 | Wrong number | Wrong number | | ADDRESS | =ADDRESS(1,1,4, TRUE(), "") | !A1 | ''!A1 | !A1 | | SEQUENCE | =SEQUENCE(0) | VALUE | N/A | CALC | +| TAKE | =TAKE(A1:A3, 0) | N/A | No such function. | CALC | | INT | =INT(-8.9) | -8 | -9 | -9 | | MOD | =MOD(-10, 3) | -1 | 2 | 2 | | ISEVEN | =ISEVEN(2.5) | FALSE | TRUE | TRUE | diff --git a/docs/guide/types-of-errors.md b/docs/guide/types-of-errors.md index 038415c2c..92defde20 100644 --- a/docs/guide/types-of-errors.md +++ b/docs/guide/types-of-errors.md @@ -21,7 +21,6 @@ according to the language settings. | #NUM! | Invalid number | This error arises when your formula contains an invalid number. | | #REF! | Invalid reference | It occurs when a formula contains an invalid reference. It is one of the most common errors users encounter when working with spreadsheets. | | #VALUE! | Wrong type of argument | It occurs when a formula tries to improperly use different types of data. For example, you will see this error when you will try to add a string to a number. | -| #CALC! | Calculation error | It occurs when a formula cannot produce a valid array result, such as when TAKE requests zero rows or columns. | | #CYCLE! | Circular reference | It occurs when a formula refers to its own cell, both directly and indirectly. | | #ERROR! | An error occurred | It indicates that there is an unknown error in a formula. | | #LIC! | Invalid license key | It occurs when the license key is invalid, expired, or missing. | diff --git a/src/Cell.ts b/src/Cell.ts index 6f460e9f0..faab24bab 100644 --- a/src/Cell.ts +++ b/src/Cell.ts @@ -23,9 +23,6 @@ import {AddressWithSheet} from './parser/Address' * Possible errors returned by our interpreter. */ export enum ErrorType { - /** Calculation error. */ - CALC = 'CALC', - /** Division by zero. */ DIV_BY_ZERO = 'DIV_BY_ZERO', diff --git a/src/i18n/languages/csCZ.ts b/src/i18n/languages/csCZ.ts index e4a62b922..2e132b159 100644 --- a/src/i18n/languages/csCZ.ts +++ b/src/i18n/languages/csCZ.ts @@ -7,7 +7,6 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { - CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#DĚLENÍ_NULOU!', ERROR: '#ERROR!', diff --git a/src/i18n/languages/daDK.ts b/src/i18n/languages/daDK.ts index 60de6b246..d349a1449 100644 --- a/src/i18n/languages/daDK.ts +++ b/src/i18n/languages/daDK.ts @@ -7,7 +7,6 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { - CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#DIVISION/0!', ERROR: '#ERROR!', diff --git a/src/i18n/languages/deDE.ts b/src/i18n/languages/deDE.ts index 2a267fba5..43e1362a3 100644 --- a/src/i18n/languages/deDE.ts +++ b/src/i18n/languages/deDE.ts @@ -7,7 +7,6 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { - CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#DIV/0!', ERROR: '#ERROR!', diff --git a/src/i18n/languages/enGB.ts b/src/i18n/languages/enGB.ts index c133d912d..e8286f5da 100644 --- a/src/i18n/languages/enGB.ts +++ b/src/i18n/languages/enGB.ts @@ -7,7 +7,6 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { - CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#DIV/0!', ERROR: '#ERROR!', diff --git a/src/i18n/languages/esES.ts b/src/i18n/languages/esES.ts index f9047cbdd..48df21d5e 100644 --- a/src/i18n/languages/esES.ts +++ b/src/i18n/languages/esES.ts @@ -7,7 +7,6 @@ import {RawTranslationPackage} from '..' export const dictionary: RawTranslationPackage = { errors: { - CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#¡DIV/0!', ERROR: '#ERROR!', diff --git a/src/i18n/languages/fiFI.ts b/src/i18n/languages/fiFI.ts index 029df4a8f..57491a6fb 100644 --- a/src/i18n/languages/fiFI.ts +++ b/src/i18n/languages/fiFI.ts @@ -7,7 +7,6 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { - CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#JAKO/0!', ERROR: '#ERROR!', diff --git a/src/i18n/languages/frFR.ts b/src/i18n/languages/frFR.ts index 0ca1d7a0e..a99ffa240 100644 --- a/src/i18n/languages/frFR.ts +++ b/src/i18n/languages/frFR.ts @@ -7,7 +7,6 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { - CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#DIV/0!', ERROR: '#ERROR!', diff --git a/src/i18n/languages/huHU.ts b/src/i18n/languages/huHU.ts index e5a46d082..6095f001b 100644 --- a/src/i18n/languages/huHU.ts +++ b/src/i18n/languages/huHU.ts @@ -7,7 +7,6 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { - CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#ZÉRÓOSZTÓ!', ERROR: '#ERROR!', diff --git a/src/i18n/languages/idID.ts b/src/i18n/languages/idID.ts index afabf7e2b..337e32d57 100644 --- a/src/i18n/languages/idID.ts +++ b/src/i18n/languages/idID.ts @@ -7,7 +7,6 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { - CALC: '#CALC!', CYCLE: '#SIKLUS!', DIV_BY_ZERO: '#BAGI0!', ERROR: '#GALAT!', diff --git a/src/i18n/languages/itIT.ts b/src/i18n/languages/itIT.ts index 789a758bf..43fb67409 100644 --- a/src/i18n/languages/itIT.ts +++ b/src/i18n/languages/itIT.ts @@ -7,7 +7,6 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { - CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#DIV/0!', ERROR: '#ERROR!', diff --git a/src/i18n/languages/nbNO.ts b/src/i18n/languages/nbNO.ts index 28f594b15..9cc2f3c48 100644 --- a/src/i18n/languages/nbNO.ts +++ b/src/i18n/languages/nbNO.ts @@ -7,7 +7,6 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { - CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#DIV/0!', ERROR: '#ERROR!', diff --git a/src/i18n/languages/nlNL.ts b/src/i18n/languages/nlNL.ts index 838e58305..34e27f436 100644 --- a/src/i18n/languages/nlNL.ts +++ b/src/i18n/languages/nlNL.ts @@ -7,7 +7,6 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { - CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#DELING.DOOR.0!', ERROR: '#ERROR!', diff --git a/src/i18n/languages/plPL.ts b/src/i18n/languages/plPL.ts index 7275a746d..e24b4f8e4 100644 --- a/src/i18n/languages/plPL.ts +++ b/src/i18n/languages/plPL.ts @@ -7,7 +7,6 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { - CALC: '#CALC!', CYCLE: '#CYKL!', DIV_BY_ZERO: '#DZIEL/0!', ERROR: '#BŁĄD!', diff --git a/src/i18n/languages/ptPT.ts b/src/i18n/languages/ptPT.ts index d8e46ad2e..3d45eb9d5 100644 --- a/src/i18n/languages/ptPT.ts +++ b/src/i18n/languages/ptPT.ts @@ -7,7 +7,6 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { - CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#DIV/0!', ERROR: '#ERROR!', diff --git a/src/i18n/languages/ruRU.ts b/src/i18n/languages/ruRU.ts index 6dce4df00..b8b137821 100644 --- a/src/i18n/languages/ruRU.ts +++ b/src/i18n/languages/ruRU.ts @@ -7,7 +7,6 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { - CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#ДЕЛ/0!', ERROR: '#ERROR!', diff --git a/src/i18n/languages/svSE.ts b/src/i18n/languages/svSE.ts index 4190105aa..7d4ba7b46 100644 --- a/src/i18n/languages/svSE.ts +++ b/src/i18n/languages/svSE.ts @@ -7,7 +7,6 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { - CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#DIVISION/0!', ERROR: '#ERROR!', diff --git a/src/i18n/languages/trTR.ts b/src/i18n/languages/trTR.ts index b490bc201..9524f793f 100644 --- a/src/i18n/languages/trTR.ts +++ b/src/i18n/languages/trTR.ts @@ -7,7 +7,6 @@ import {RawTranslationPackage} from '..' const dictionary: RawTranslationPackage = { errors: { - CALC: '#CALC!', CYCLE: '#CYCLE!', DIV_BY_ZERO: '#SAYI/0!', ERROR: '#ERROR!', diff --git a/src/interpreter/plugin/ArrayPlugin.ts b/src/interpreter/plugin/ArrayPlugin.ts index b406a3796..d6d512127 100644 --- a/src/interpreter/plugin/ArrayPlugin.ts +++ b/src/interpreter/plugin/ArrayPlugin.ts @@ -204,7 +204,7 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche * * Returns rows and columns from the beginning or end of the source array. * Syntactically empty dimensions keep all rows or columns. Counts that - * evaluate to zero return a #CALC! error. + * evaluate to zero return a #N/A error. * * @param ast * @param state @@ -218,11 +218,11 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche const requestedColumns = Math.trunc(columns) if (requestedRows === 0 || requestedColumns === 0) { - return new CellError(ErrorType.CALC, ErrorMessage.ZeroRowOrColumnCount) + return new CellError(ErrorType.NA, ErrorMessage.ZeroRowOrColumnCount) } if (sourceHeight === 0 || sourceWidth === 0) { - return new CellError(ErrorType.CALC, ErrorMessage.EmptyRange) + return new CellError(ErrorType.NA, ErrorMessage.EmptyRange) } const rowsToTake = Math.min(Math.abs(requestedRows), sourceHeight) From d6708dc7e8f0046a1b376f6084f001b4d90a4ad2 Mon Sep 17 00:00:00 2001 From: tobiadefami Date: Tue, 11 Aug 2026 15:59:23 +0100 Subject: [PATCH 11/20] fix: enforce TAKE arity locally --- src/interpreter/plugin/ArrayPlugin.ts | 8 +++++++- src/interpreter/plugin/FunctionPlugin.ts | 10 +++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/interpreter/plugin/ArrayPlugin.ts b/src/interpreter/plugin/ArrayPlugin.ts index d6d512127..181714426 100644 --- a/src/interpreter/plugin/ArrayPlugin.ts +++ b/src/interpreter/plugin/ArrayPlugin.ts @@ -71,7 +71,7 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche enableArrayArithmeticForArguments: true, parameters: [ {argumentType: FunctionArgumentType.RANGE}, - {argumentType: FunctionArgumentType.NUMBER, optionalArg: false, defaultValue: Number.POSITIVE_INFINITY, emptyAsDefault: true}, + {argumentType: FunctionArgumentType.NUMBER, defaultValue: Number.POSITIVE_INFINITY, emptyAsDefault: true}, {argumentType: FunctionArgumentType.NUMBER, optionalArg: true, defaultValue: Number.POSITIVE_INFINITY, emptyAsDefault: true}, ], vectorizationForbidden: true, @@ -210,6 +210,12 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche * @param state */ public take(ast: ProcedureAst, state: InterpreterState): InterpreterValue { + // The default supports TAKE(array, , columns), but the rows argument + // position must still be present. + if (ast.args.length < 2) { + return new CellError(ErrorType.NA, ErrorMessage.WrongArgNumber) + } + return this.runFunction(ast.args, state, this.metadata('TAKE'), (range: SimpleRangeValue, rows: number, columns: number) => { const sourceHeight = range.height() diff --git a/src/interpreter/plugin/FunctionPlugin.ts b/src/interpreter/plugin/FunctionPlugin.ts index b1e0b1fa6..79de6453b 100644 --- a/src/interpreter/plugin/FunctionPlugin.ts +++ b/src/interpreter/plugin/FunctionPlugin.ts @@ -256,17 +256,13 @@ export interface FunctionArgument { /** * Returns whether a function argument may be omitted. * - * An explicit `optionalArg` declaration takes precedence. Otherwise, the - * presence of a default value preserves the existing implicit-optional - * behavior. + * Arguments explicitly marked as optional and arguments with default values + * may be omitted. * * @param {FunctionArgument | undefined} argument - The argument metadata to inspect. */ export function isFunctionArgumentOptional(argument: FunctionArgument | undefined): boolean { - if (argument?.optionalArg !== undefined) { - return argument.optionalArg - } - return argument?.defaultValue !== undefined + return argument?.optionalArg || argument?.defaultValue !== undefined } export type PluginFunctionType = (ast: ProcedureAst, state: InterpreterState) => InterpreterValue From 3335d993a8a3f010ef488a805eb4cb354e33e407 Mon Sep 17 00:00:00 2001 From: tobiadefami Date: Tue, 11 Aug 2026 17:21:37 +0100 Subject: [PATCH 12/20] docs: remove unrelated translation policy --- DEV_DOCS.md | 36 +----------------------------------- 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/DEV_DOCS.md b/DEV_DOCS.md index 431433ed1..13630fd4c 100644 --- a/DEV_DOCS.md +++ b/DEV_DOCS.md @@ -159,46 +159,12 @@ Descriptions must describe **HyperFormula's** behaviour, not Excel's. Much of th HyperFormula supports internationalization and provides localized function names for all built-in languages. Translation files live in `src/i18n/languages/`. New functions must include translations for all built-in languages. -### Policy: only ship a name you can source - -Add a localized function name **only when you have confirmed that Microsoft Excel ships that exact name in the product**. If you cannot confirm it, keep the English name. - -An unverified translation is worse than the English fallback. The English name at least matches what Excel uses in the locales Microsoft leaves untranslated, and users can look it up; an invented one matches nothing, is what users will type first because it reads plausibly, and silently produces `#NAME?`. It is also hard to spot in review — a made-up name looks exactly like a real one to anyone who does not speak the language. - -Two things this rules out: - -- Translating the English word yourself, or having a tool do it. A dictionary rendering is not what Excel ships: `TAKE` is `WYCINEK` in Polish (not "WEŹ"), `INCLUDI` in Italian, and `ÁTHELYEZ` in Hungarian — none of which a translator would produce from the word "take". -- Guessing from a sibling function. Names within one release batch are not translated consistently: Swedish localizes `TAKE` as `TA` but keeps `VSTACK`/`HSTACK` in English. - -Keeping the English name is a legitimate, complete answer, and it is what Microsoft itself does in several locales. Prefer it over a guess. - When looking for the valid translations for new functions, try these sources: - https://support.microsoft.com/en-us/office/excel-functions-translator-f262d0c0-991c-485b-89b6-32cc8d326889 - http://dolf.trieschnigg.nl/excel/index.php -### Reading the translations off Microsoft's localized function list - -The two sources above are a manual add-in and a third-party table that only covers older functions, so neither answers "what is `TAKE` called in Czech?". The **alphabetical function list** does, for every locale at once: - -``` -https://support.microsoft.com//office/excel-functions-alphabetical-b3944572-255d-4efb-bb96-c6d90033e188 -``` - -Each row links to the function's own page using the **English slug** in the `href` while the link text is the **localized name**, which makes the lookup exact rather than a guess: - -```html -WYCINEK
(2024) -``` - -So `grep`ping the page for `functions/-function` and reading the anchor text gives the authoritative name. Do this per locale (`pl-pl`, `cs-cz`, `fi-fi`, …) rather than trusting a single translated page. - -Two traps this avoids: - -- **A function's own localized page is not a reliable source.** For several locales the syntax block is still the English one even though the surrounding prose and the argument names are translated — the French page shows `=TAKE(tableau, lignes,[colonnes])` and the Hungarian one `=TAKE(tömb, sorok,[oszlopok])`, while the product actually uses `PRENDRE` and `ÁTHELYEZ`. Reading the function page alone concludes "not translated" and is wrong. -- **Do not translate the name yourself.** A dictionary rendering of the English word is not what Excel ships: `TAKE` is `WYCINEK` in Polish (not "WEŹ"), `INCLUDI` in Italian, `ÁTHELYEZ` in Hungarian, and `TA` in both Swedish and Norwegian. Where the list genuinely keeps the English name — `VSTACK`/`HSTACK` in Swedish and Indonesian, `TAKE` in Indonesian — keep it too. - -For languages not officially supported by Microsoft Excel, none of the sources above apply. For these languages, use Google Sheets as the reference. Switch the `hl` query parameter to the target locale, for example: +For languages not officially supported by Microsoft Excel, the two sources above do not apply. For these languages, use Google Sheets as the reference. Switch the `hl` query parameter to the target locale, for example: - https://support.google.com/docs/table/25273?hl=id (Indonesian) From 86a6a937de0701a2e13f2a5852ee56f7f628f74f Mon Sep 17 00:00:00 2001 From: tobiadefami Date: Tue, 11 Aug 2026 17:58:32 +0100 Subject: [PATCH 13/20] fix: preserve TAKE result identity --- src/interpreter/plugin/ArrayPlugin.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/interpreter/plugin/ArrayPlugin.ts b/src/interpreter/plugin/ArrayPlugin.ts index 181714426..85e2815f5 100644 --- a/src/interpreter/plugin/ArrayPlugin.ts +++ b/src/interpreter/plugin/ArrayPlugin.ts @@ -244,7 +244,8 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche columnsToTake, rowsToTake, ) - return SimpleRangeValue.onlyRange(resultRange, this.dependencyGraph) + const result = SimpleRangeValue.onlyRange(resultRange, this.dependencyGraph).data + return SimpleRangeValue.onlyValues(result) } const result = range.data From d7c87b8862f80cc572dc7a17dc212973677a1837 Mon Sep 17 00:00:00 2001 From: tobiadefami Date: Tue, 11 Aug 2026 23:21:12 +0100 Subject: [PATCH 14/20] fix: reject unbounded TAKE results --- docs/guide/list-of-differences.md | 1 + src/interpreter/plugin/ArrayPlugin.ts | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/guide/list-of-differences.md b/docs/guide/list-of-differences.md index e4df0258c..bd98d2eac 100644 --- a/docs/guide/list-of-differences.md +++ b/docs/guide/list-of-differences.md @@ -107,6 +107,7 @@ To remove the differences, create [custom implementations](custom-functions.md) | ADDRESS | =ADDRESS(1,1,4, TRUE(), "") | !A1 | ''!A1 | !A1 | | SEQUENCE | =SEQUENCE(0) | VALUE | N/A | CALC | | TAKE | =TAKE(A1:A3, 0) | N/A | No such function. | CALC | +| TAKE | =TAKE(Data!A:A, , 1) | VALUE because HyperFormula cannot allocate an unbounded direct whole-column result. | No such function. | Spills the whole column from row 1; returns SPILL below row 1. | | INT | =INT(-8.9) | -8 | -9 | -9 | | MOD | =MOD(-10, 3) | -1 | 2 | 2 | | ISEVEN | =ISEVEN(2.5) | FALSE | TRUE | TRUE | diff --git a/src/interpreter/plugin/ArrayPlugin.ts b/src/interpreter/plugin/ArrayPlugin.ts index 85e2815f5..bf0033296 100644 --- a/src/interpreter/plugin/ArrayPlugin.ts +++ b/src/interpreter/plugin/ArrayPlugin.ts @@ -259,7 +259,8 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche /** * Calculates the spilled array size of TAKE using the source dimensions as - * the upper bound. + * the upper bound. Unbounded results are rejected so the dependency graph + * never reserves an infinite spill range. * * @param ast * @param state @@ -280,7 +281,7 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche const height = literalRows === undefined ? sourceSize.height : Math.min(sourceSize.height, literalRows) const width = literalColumns === undefined ? sourceSize.width : Math.min(sourceSize.width, literalColumns) - if (height < 1 || width < 1) { + if (!Number.isFinite(height) || !Number.isFinite(width) || height < 1 || width < 1) { return ArraySize.error() } From f34f6a8a79fa2c9c46a82694e0cf66be5b64010f Mon Sep 17 00:00:00 2001 From: tobiadefami Date: Wed, 12 Aug 2026 13:22:04 +0100 Subject: [PATCH 15/20] fix: size TAKE from numeric text literals --- src/interpreter/plugin/ArrayPlugin.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/interpreter/plugin/ArrayPlugin.ts b/src/interpreter/plugin/ArrayPlugin.ts index bf0033296..f9050960f 100644 --- a/src/interpreter/plugin/ArrayPlugin.ts +++ b/src/interpreter/plugin/ArrayPlugin.ts @@ -10,22 +10,30 @@ import {ErrorMessage} from '../../error-message' import {Ast, AstNodeType, ProcedureAst} from '../../parser' import {coerceScalarToBoolean} from '../ArithmeticHelper' import {InterpreterState} from '../InterpreterState' -import {InternalScalarValue, InterpreterValue} from '../InterpreterValue' +import {getRawValue, InternalScalarValue, InterpreterValue} from '../InterpreterValue' import {SimpleRangeValue} from '../../SimpleRangeValue' import {FunctionArgumentType, FunctionPlugin, FunctionPluginTypecheck, ImplementedFunctions} from './FunctionPlugin' export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypecheck { /** - * Converts a numeric TAKE count literal into its predicted result dimension. + * Converts a numeric or numeric-text TAKE count literal into its predicted result dimension. * * @param {Ast | undefined} argument - The count argument to inspect before evaluation. - * @returns {number | undefined} The truncated absolute count, or `undefined` when it is not a numeric literal. + * @returns {number | undefined} The truncated absolute count, or `undefined` when it is not a supported literal. */ - private static parseTakeLiteralDimension(argument: Ast | undefined): number | undefined { + private parseTakeLiteralDimension(argument: Ast | undefined): number | undefined { if (argument?.type === AstNodeType.NUMBER) { return Math.abs(Math.trunc(argument.value)) } + if (argument?.type === AstNodeType.STRING) { + const coercedValue = this.arithmeticHelper.coerceToMaybeNumber(argument.value) + if (coercedValue === undefined) { + return undefined + } + return Math.abs(Math.trunc(getRawValue(coercedValue))) + } + if ( (argument?.type === AstNodeType.PLUS_UNARY_OP || argument?.type === AstNodeType.MINUS_UNARY_OP) && argument.value.type === AstNodeType.NUMBER @@ -276,8 +284,8 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche new InterpreterState(state.formulaAddress, state.arraysFlag || (metadata?.enableArrayArithmeticForArguments ?? false)), ) - const literalRows = ArrayPlugin.parseTakeLiteralDimension(ast.args[1]) - const literalColumns = ArrayPlugin.parseTakeLiteralDimension(ast.args[2]) + const literalRows = this.parseTakeLiteralDimension(ast.args[1]) + const literalColumns = this.parseTakeLiteralDimension(ast.args[2]) const height = literalRows === undefined ? sourceSize.height : Math.min(sourceSize.height, literalRows) const width = literalColumns === undefined ? sourceSize.width : Math.min(sourceSize.width, literalColumns) From a18429c5805c6f8dbcc6c97257fc71546702e11d Mon Sep 17 00:00:00 2001 From: tobiadefami Date: Wed, 12 Aug 2026 14:22:42 +0100 Subject: [PATCH 16/20] fix: preserve TAKE coercion errors --- src/interpreter/plugin/ArrayPlugin.ts | 34 +++++++++++++++++---------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/interpreter/plugin/ArrayPlugin.ts b/src/interpreter/plugin/ArrayPlugin.ts index f9050960f..a10a8e4fd 100644 --- a/src/interpreter/plugin/ArrayPlugin.ts +++ b/src/interpreter/plugin/ArrayPlugin.ts @@ -14,34 +14,39 @@ import {getRawValue, InternalScalarValue, InterpreterValue} from '../Interpreter import {SimpleRangeValue} from '../../SimpleRangeValue' import {FunctionArgumentType, FunctionPlugin, FunctionPluginTypecheck, ImplementedFunctions} from './FunctionPlugin' +type TakeLiteralDimension = + | {kind: 'value', value: number} + | {kind: 'invalid'} + | {kind: 'unresolved'} + export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypecheck { /** - * Converts a numeric or numeric-text TAKE count literal into its predicted result dimension. + * Classifies a TAKE count for static result-size prediction. * * @param {Ast | undefined} argument - The count argument to inspect before evaluation. - * @returns {number | undefined} The truncated absolute count, or `undefined` when it is not a supported literal. + * @returns {TakeLiteralDimension} The literal value, an invalid-literal marker, or an unresolved marker. */ - private parseTakeLiteralDimension(argument: Ast | undefined): number | undefined { + private parseTakeLiteralDimension(argument: Ast | undefined): TakeLiteralDimension { if (argument?.type === AstNodeType.NUMBER) { - return Math.abs(Math.trunc(argument.value)) + return {kind: 'value', value: Math.abs(Math.trunc(argument.value))} } if (argument?.type === AstNodeType.STRING) { const coercedValue = this.arithmeticHelper.coerceToMaybeNumber(argument.value) if (coercedValue === undefined) { - return undefined + return {kind: 'invalid'} } - return Math.abs(Math.trunc(getRawValue(coercedValue))) + return {kind: 'value', value: Math.abs(Math.trunc(getRawValue(coercedValue)))} } if ( (argument?.type === AstNodeType.PLUS_UNARY_OP || argument?.type === AstNodeType.MINUS_UNARY_OP) && argument.value.type === AstNodeType.NUMBER ) { - return Math.abs(Math.trunc(argument.value.value)) + return {kind: 'value', value: Math.abs(Math.trunc(argument.value.value))} } - return undefined + return {kind: 'unresolved'} } public static implementedFunctions: ImplementedFunctions = { @@ -284,10 +289,15 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche new InterpreterState(state.formulaAddress, state.arraysFlag || (metadata?.enableArrayArithmeticForArguments ?? false)), ) - const literalRows = this.parseTakeLiteralDimension(ast.args[1]) - const literalColumns = this.parseTakeLiteralDimension(ast.args[2]) - const height = literalRows === undefined ? sourceSize.height : Math.min(sourceSize.height, literalRows) - const width = literalColumns === undefined ? sourceSize.width : Math.min(sourceSize.width, literalColumns) + const rowDimension = this.parseTakeLiteralDimension(ast.args[1]) + const columnDimension = this.parseTakeLiteralDimension(ast.args[2]) + + if (rowDimension.kind === 'invalid' || columnDimension.kind === 'invalid') { + return ArraySize.error() + } + + const height = rowDimension.kind === 'value' ? Math.min(sourceSize.height, rowDimension.value) : sourceSize.height + const width = columnDimension.kind === 'value' ? Math.min(sourceSize.width, columnDimension.value) : sourceSize.width if (!Number.isFinite(height) || !Number.isFinite(width) || height < 1 || width < 1) { return ArraySize.error() From 4459034087d68290cdcc639aa9e2241131b9b943 Mon Sep 17 00:00:00 2001 From: tobiadefami Date: Mon, 17 Aug 2026 14:11:23 +0100 Subject: [PATCH 17/20] fix: align TAKE sizing with Excel --- docs/guide/known-limitations.md | 7 ++ docs/guide/list-of-differences.md | 2 +- src/interpreter/plugin/ArrayPlugin.ts | 111 ++++++++++++++++++++++---- 3 files changed, 105 insertions(+), 15 deletions(-) diff --git a/docs/guide/known-limitations.md b/docs/guide/known-limitations.md index 7f0e2361d..1aaa78461 100644 --- a/docs/guide/known-limitations.md +++ b/docs/guide/known-limitations.md @@ -43,6 +43,13 @@ a circular reference. * Comparison of values follows HyperFormula's own equality rules, which honor the `caseSensitive` and `accentSensitive` configuration options. By default comparison is case-insensitive. * When `ExactlyOnce` is TRUE and no row or column occurs exactly once, `UNIQUE` returns a `#N/A` error (the result would otherwise be empty). + +### TAKE function + +* A whole-column result can spill only from the first row of the source sheet, and a whole-row result can spill only from its first column. Cross-sheet whole-column and whole-row results return `#SPILL!`, matching Excel. + +* When a row or column count depends on a cell or another non-constant expression, TAKE reserves the corresponding source dimension as an upper bound. The reserved footprint can therefore be larger than the values returned at runtime; an error result, including a count that evaluates to zero, fills that footprint. + ### SORT function * The `SortIndex` argument accepts a single key only. Multi-key sorting through an array constant (for example `=SORT(A1:B9, {1,2})`) is not supported; sort by one column or row at a time. diff --git a/docs/guide/list-of-differences.md b/docs/guide/list-of-differences.md index bd98d2eac..a30792ac1 100644 --- a/docs/guide/list-of-differences.md +++ b/docs/guide/list-of-differences.md @@ -107,7 +107,7 @@ To remove the differences, create [custom implementations](custom-functions.md) | ADDRESS | =ADDRESS(1,1,4, TRUE(), "") | !A1 | ''!A1 | !A1 | | SEQUENCE | =SEQUENCE(0) | VALUE | N/A | CALC | | TAKE | =TAKE(A1:A3, 0) | N/A | No such function. | CALC | -| TAKE | =TAKE(Data!A:A, , 1) | VALUE because HyperFormula cannot allocate an unbounded direct whole-column result. | No such function. | Spills the whole column from row 1; returns SPILL below row 1. | +| TAKE | =TAKE(Data!A:A, , 1) | SPILL | No such function. | SPILL | | INT | =INT(-8.9) | -8 | -9 | -9 | | MOD | =MOD(-10, 3) | -1 | 2 | 2 | | ISEVEN | =ISEVEN(2.5) | FALSE | TRUE | TRUE | diff --git a/src/interpreter/plugin/ArrayPlugin.ts b/src/interpreter/plugin/ArrayPlugin.ts index a10a8e4fd..a0aaef68f 100644 --- a/src/interpreter/plugin/ArrayPlugin.ts +++ b/src/interpreter/plugin/ArrayPlugin.ts @@ -21,14 +21,15 @@ type TakeLiteralDimension = export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypecheck { /** - * Classifies a TAKE count for static result-size prediction. + * Evaluates the constant subset of TAKE count expressions needed for static + * result-size prediction. * - * @param {Ast | undefined} argument - The count argument to inspect before evaluation. - * @returns {TakeLiteralDimension} The literal value, an invalid-literal marker, or an unresolved marker. + * @param {Ast | undefined} argument - The count expression to inspect before evaluation. + * @returns {TakeLiteralDimension} The constant value, an invalid-literal marker, or an unresolved marker. */ - private parseTakeLiteralDimension(argument: Ast | undefined): TakeLiteralDimension { + private parseTakeLiteralNumber(argument: Ast | undefined): TakeLiteralDimension { if (argument?.type === AstNodeType.NUMBER) { - return {kind: 'value', value: Math.abs(Math.trunc(argument.value))} + return {kind: 'value', value: argument.value} } if (argument?.type === AstNodeType.STRING) { @@ -36,19 +37,74 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche if (coercedValue === undefined) { return {kind: 'invalid'} } - return {kind: 'value', value: Math.abs(Math.trunc(getRawValue(coercedValue)))} + return {kind: 'value', value: getRawValue(coercedValue)} } - if ( - (argument?.type === AstNodeType.PLUS_UNARY_OP || argument?.type === AstNodeType.MINUS_UNARY_OP) - && argument.value.type === AstNodeType.NUMBER - ) { - return {kind: 'value', value: Math.abs(Math.trunc(argument.value.value))} + if (argument?.type === AstNodeType.PLUS_UNARY_OP || argument?.type === AstNodeType.MINUS_UNARY_OP) { + const dimension = this.parseTakeLiteralNumber(argument.value) + if (dimension.kind !== 'value') { + return dimension + } + return {kind: 'value', value: argument.type === AstNodeType.MINUS_UNARY_OP ? -dimension.value : dimension.value} + } + + if (argument?.type === AstNodeType.PARENTHESIS) { + return this.parseTakeLiteralNumber(argument.expression) + } + + if (argument?.type === AstNodeType.FUNCTION_CALL && argument.args.length === 0) { + if (argument.procedureName === 'TRUE') { + return {kind: 'value', value: 1} + } + if (argument.procedureName === 'FALSE') { + return {kind: 'value', value: 0} + } + } + + if (argument?.type === AstNodeType.PLUS_OP) { + const left = this.parseTakeLiteralNumber(argument.left) + const right = this.parseTakeLiteralNumber(argument.right) + if (left.kind === 'invalid' || right.kind === 'invalid') { + return {kind: 'invalid'} + } + if (left.kind === 'value' && right.kind === 'value') { + return {kind: 'value', value: left.value + right.value} + } } return {kind: 'unresolved'} } + /** + * Converts a statically resolved TAKE count into its output dimension. + * + * @param {Ast | undefined} argument - The count expression to classify. + * @returns {TakeLiteralDimension} The non-negative truncated dimension or the unresolved classification. + */ + private parseTakeLiteralDimension(argument: Ast | undefined): TakeLiteralDimension { + const dimension = this.parseTakeLiteralNumber(argument) + return dimension.kind === 'value' + ? {kind: 'value', value: Math.abs(Math.trunc(dimension.value))} + : dimension + } + + /** + * Resolves a direct TAKE source reference without evaluating its values. + * + * @param {Ast} argument - The source expression to inspect. + * @param {InterpreterState} state - The formula state used to resolve relative addresses. + * @returns {AbsoluteCellRange | undefined} The source range, or `undefined` for a computed array. + */ + private takeSourceRange(argument: Ast, state: InterpreterState): AbsoluteCellRange | undefined { + if (argument.type === AstNodeType.PARENTHESIS) { + return this.takeSourceRange(argument.expression, state) + } + if (argument.type === AstNodeType.CELL_RANGE || argument.type === AstNodeType.COLUMN_RANGE || argument.type === AstNodeType.ROW_RANGE) { + return AbsoluteCellRange.fromAstOrUndef(argument, state.formulaAddress) + } + return undefined + } + public static implementedFunctions: ImplementedFunctions = { 'ARRAYFORMULA': { method: 'arrayformula', @@ -250,6 +306,18 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche const startColumn = requestedColumns > 0 ? 0 : sourceWidth - columnsToTake const sourceRange = range.range + if (sourceRange !== undefined) { + const leavesHeightUnbounded = !Number.isFinite(sourceRange.height()) && !Number.isFinite(requestedRows) + const leavesWidthUnbounded = !Number.isFinite(sourceRange.width()) && !Number.isFinite(requestedColumns) + const crossesSheets = sourceRange.sheet !== state.formulaAddress.sheet + const startsBelowFirstRow = leavesHeightUnbounded && state.formulaAddress.row !== 0 + const startsRightOfFirstColumn = leavesWidthUnbounded && state.formulaAddress.col !== 0 + + if ((leavesHeightUnbounded || leavesWidthUnbounded) && (crossesSheets || startsBelowFirstRow || startsRightOfFirstColumn)) { + return new CellError(ErrorType.SPILL, ErrorMessage.NoSpaceForArrayResult) + } + } + // Keep address-backed ranges lazy to avoid materializing cells outside the TAKE result. if (sourceRange !== undefined) { const resultRange = AbsoluteCellRange.spanFrom( @@ -272,8 +340,9 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche /** * Calculates the spilled array size of TAKE using the source dimensions as - * the upper bound. Unbounded results are rejected so the dependency graph - * never reserves an infinite spill range. + * the upper bound. A same-sheet whole-column or whole-row result can retain + * its unbounded dimension when its formula starts at the corresponding sheet + * edge; other unbounded results are evaluated as scalar errors. * * @param ast * @param state @@ -298,8 +367,22 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche const height = rowDimension.kind === 'value' ? Math.min(sourceSize.height, rowDimension.value) : sourceSize.height const width = columnDimension.kind === 'value' ? Math.min(sourceSize.width, columnDimension.value) : sourceSize.width + const sourceRange = this.takeSourceRange(ast.args[0], state) + const canSpillUnboundedHeight = !Number.isFinite(height) + && sourceRange !== undefined + && sourceRange.sheet === state.formulaAddress.sheet + && state.formulaAddress.row === 0 + const canSpillUnboundedWidth = !Number.isFinite(width) + && sourceRange !== undefined + && sourceRange.sheet === state.formulaAddress.sheet + && state.formulaAddress.col === 0 - if (!Number.isFinite(height) || !Number.isFinite(width) || height < 1 || width < 1) { + if ( + (!Number.isFinite(height) && !canSpillUnboundedHeight) + || (!Number.isFinite(width) && !canSpillUnboundedWidth) + || height < 1 + || width < 1 + ) { return ArraySize.error() } From 5ca25fcded973098073327d63bf830ab5c42b6ee Mon Sep 17 00:00:00 2001 From: tobiadefami Date: Mon, 17 Aug 2026 14:35:01 +0100 Subject: [PATCH 18/20] Revert "fix: align TAKE sizing with Excel" This reverts commit 4459034087d68290cdcc639aa9e2241131b9b943. --- docs/guide/known-limitations.md | 7 -- docs/guide/list-of-differences.md | 2 +- src/interpreter/plugin/ArrayPlugin.ts | 111 ++++---------------------- 3 files changed, 15 insertions(+), 105 deletions(-) diff --git a/docs/guide/known-limitations.md b/docs/guide/known-limitations.md index 1aaa78461..7f0e2361d 100644 --- a/docs/guide/known-limitations.md +++ b/docs/guide/known-limitations.md @@ -43,13 +43,6 @@ a circular reference. * Comparison of values follows HyperFormula's own equality rules, which honor the `caseSensitive` and `accentSensitive` configuration options. By default comparison is case-insensitive. * When `ExactlyOnce` is TRUE and no row or column occurs exactly once, `UNIQUE` returns a `#N/A` error (the result would otherwise be empty). - -### TAKE function - -* A whole-column result can spill only from the first row of the source sheet, and a whole-row result can spill only from its first column. Cross-sheet whole-column and whole-row results return `#SPILL!`, matching Excel. - -* When a row or column count depends on a cell or another non-constant expression, TAKE reserves the corresponding source dimension as an upper bound. The reserved footprint can therefore be larger than the values returned at runtime; an error result, including a count that evaluates to zero, fills that footprint. - ### SORT function * The `SortIndex` argument accepts a single key only. Multi-key sorting through an array constant (for example `=SORT(A1:B9, {1,2})`) is not supported; sort by one column or row at a time. diff --git a/docs/guide/list-of-differences.md b/docs/guide/list-of-differences.md index a30792ac1..bd98d2eac 100644 --- a/docs/guide/list-of-differences.md +++ b/docs/guide/list-of-differences.md @@ -107,7 +107,7 @@ To remove the differences, create [custom implementations](custom-functions.md) | ADDRESS | =ADDRESS(1,1,4, TRUE(), "") | !A1 | ''!A1 | !A1 | | SEQUENCE | =SEQUENCE(0) | VALUE | N/A | CALC | | TAKE | =TAKE(A1:A3, 0) | N/A | No such function. | CALC | -| TAKE | =TAKE(Data!A:A, , 1) | SPILL | No such function. | SPILL | +| TAKE | =TAKE(Data!A:A, , 1) | VALUE because HyperFormula cannot allocate an unbounded direct whole-column result. | No such function. | Spills the whole column from row 1; returns SPILL below row 1. | | INT | =INT(-8.9) | -8 | -9 | -9 | | MOD | =MOD(-10, 3) | -1 | 2 | 2 | | ISEVEN | =ISEVEN(2.5) | FALSE | TRUE | TRUE | diff --git a/src/interpreter/plugin/ArrayPlugin.ts b/src/interpreter/plugin/ArrayPlugin.ts index a0aaef68f..a10a8e4fd 100644 --- a/src/interpreter/plugin/ArrayPlugin.ts +++ b/src/interpreter/plugin/ArrayPlugin.ts @@ -21,15 +21,14 @@ type TakeLiteralDimension = export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypecheck { /** - * Evaluates the constant subset of TAKE count expressions needed for static - * result-size prediction. + * Classifies a TAKE count for static result-size prediction. * - * @param {Ast | undefined} argument - The count expression to inspect before evaluation. - * @returns {TakeLiteralDimension} The constant value, an invalid-literal marker, or an unresolved marker. + * @param {Ast | undefined} argument - The count argument to inspect before evaluation. + * @returns {TakeLiteralDimension} The literal value, an invalid-literal marker, or an unresolved marker. */ - private parseTakeLiteralNumber(argument: Ast | undefined): TakeLiteralDimension { + private parseTakeLiteralDimension(argument: Ast | undefined): TakeLiteralDimension { if (argument?.type === AstNodeType.NUMBER) { - return {kind: 'value', value: argument.value} + return {kind: 'value', value: Math.abs(Math.trunc(argument.value))} } if (argument?.type === AstNodeType.STRING) { @@ -37,74 +36,19 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche if (coercedValue === undefined) { return {kind: 'invalid'} } - return {kind: 'value', value: getRawValue(coercedValue)} + return {kind: 'value', value: Math.abs(Math.trunc(getRawValue(coercedValue)))} } - if (argument?.type === AstNodeType.PLUS_UNARY_OP || argument?.type === AstNodeType.MINUS_UNARY_OP) { - const dimension = this.parseTakeLiteralNumber(argument.value) - if (dimension.kind !== 'value') { - return dimension - } - return {kind: 'value', value: argument.type === AstNodeType.MINUS_UNARY_OP ? -dimension.value : dimension.value} - } - - if (argument?.type === AstNodeType.PARENTHESIS) { - return this.parseTakeLiteralNumber(argument.expression) - } - - if (argument?.type === AstNodeType.FUNCTION_CALL && argument.args.length === 0) { - if (argument.procedureName === 'TRUE') { - return {kind: 'value', value: 1} - } - if (argument.procedureName === 'FALSE') { - return {kind: 'value', value: 0} - } - } - - if (argument?.type === AstNodeType.PLUS_OP) { - const left = this.parseTakeLiteralNumber(argument.left) - const right = this.parseTakeLiteralNumber(argument.right) - if (left.kind === 'invalid' || right.kind === 'invalid') { - return {kind: 'invalid'} - } - if (left.kind === 'value' && right.kind === 'value') { - return {kind: 'value', value: left.value + right.value} - } + if ( + (argument?.type === AstNodeType.PLUS_UNARY_OP || argument?.type === AstNodeType.MINUS_UNARY_OP) + && argument.value.type === AstNodeType.NUMBER + ) { + return {kind: 'value', value: Math.abs(Math.trunc(argument.value.value))} } return {kind: 'unresolved'} } - /** - * Converts a statically resolved TAKE count into its output dimension. - * - * @param {Ast | undefined} argument - The count expression to classify. - * @returns {TakeLiteralDimension} The non-negative truncated dimension or the unresolved classification. - */ - private parseTakeLiteralDimension(argument: Ast | undefined): TakeLiteralDimension { - const dimension = this.parseTakeLiteralNumber(argument) - return dimension.kind === 'value' - ? {kind: 'value', value: Math.abs(Math.trunc(dimension.value))} - : dimension - } - - /** - * Resolves a direct TAKE source reference without evaluating its values. - * - * @param {Ast} argument - The source expression to inspect. - * @param {InterpreterState} state - The formula state used to resolve relative addresses. - * @returns {AbsoluteCellRange | undefined} The source range, or `undefined` for a computed array. - */ - private takeSourceRange(argument: Ast, state: InterpreterState): AbsoluteCellRange | undefined { - if (argument.type === AstNodeType.PARENTHESIS) { - return this.takeSourceRange(argument.expression, state) - } - if (argument.type === AstNodeType.CELL_RANGE || argument.type === AstNodeType.COLUMN_RANGE || argument.type === AstNodeType.ROW_RANGE) { - return AbsoluteCellRange.fromAstOrUndef(argument, state.formulaAddress) - } - return undefined - } - public static implementedFunctions: ImplementedFunctions = { 'ARRAYFORMULA': { method: 'arrayformula', @@ -306,18 +250,6 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche const startColumn = requestedColumns > 0 ? 0 : sourceWidth - columnsToTake const sourceRange = range.range - if (sourceRange !== undefined) { - const leavesHeightUnbounded = !Number.isFinite(sourceRange.height()) && !Number.isFinite(requestedRows) - const leavesWidthUnbounded = !Number.isFinite(sourceRange.width()) && !Number.isFinite(requestedColumns) - const crossesSheets = sourceRange.sheet !== state.formulaAddress.sheet - const startsBelowFirstRow = leavesHeightUnbounded && state.formulaAddress.row !== 0 - const startsRightOfFirstColumn = leavesWidthUnbounded && state.formulaAddress.col !== 0 - - if ((leavesHeightUnbounded || leavesWidthUnbounded) && (crossesSheets || startsBelowFirstRow || startsRightOfFirstColumn)) { - return new CellError(ErrorType.SPILL, ErrorMessage.NoSpaceForArrayResult) - } - } - // Keep address-backed ranges lazy to avoid materializing cells outside the TAKE result. if (sourceRange !== undefined) { const resultRange = AbsoluteCellRange.spanFrom( @@ -340,9 +272,8 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche /** * Calculates the spilled array size of TAKE using the source dimensions as - * the upper bound. A same-sheet whole-column or whole-row result can retain - * its unbounded dimension when its formula starts at the corresponding sheet - * edge; other unbounded results are evaluated as scalar errors. + * the upper bound. Unbounded results are rejected so the dependency graph + * never reserves an infinite spill range. * * @param ast * @param state @@ -367,22 +298,8 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche const height = rowDimension.kind === 'value' ? Math.min(sourceSize.height, rowDimension.value) : sourceSize.height const width = columnDimension.kind === 'value' ? Math.min(sourceSize.width, columnDimension.value) : sourceSize.width - const sourceRange = this.takeSourceRange(ast.args[0], state) - const canSpillUnboundedHeight = !Number.isFinite(height) - && sourceRange !== undefined - && sourceRange.sheet === state.formulaAddress.sheet - && state.formulaAddress.row === 0 - const canSpillUnboundedWidth = !Number.isFinite(width) - && sourceRange !== undefined - && sourceRange.sheet === state.formulaAddress.sheet - && state.formulaAddress.col === 0 - if ( - (!Number.isFinite(height) && !canSpillUnboundedHeight) - || (!Number.isFinite(width) && !canSpillUnboundedWidth) - || height < 1 - || width < 1 - ) { + if (!Number.isFinite(height) || !Number.isFinite(width) || height < 1 || width < 1) { return ArraySize.error() } From cb5bc3c95351f4eaf39c2c7145c9879178c85919 Mon Sep 17 00:00:00 2001 From: tobiadefami Date: Mon, 17 Aug 2026 19:54:49 +0100 Subject: [PATCH 19/20] fix: align TAKE result sizing with Excel --- docs/guide/list-of-differences.md | 2 +- src/interpreter/plugin/ArrayPlugin.ts | 124 +++++++++++++++++++++----- 2 files changed, 104 insertions(+), 22 deletions(-) diff --git a/docs/guide/list-of-differences.md b/docs/guide/list-of-differences.md index bd98d2eac..11433559d 100644 --- a/docs/guide/list-of-differences.md +++ b/docs/guide/list-of-differences.md @@ -107,7 +107,7 @@ To remove the differences, create [custom implementations](custom-functions.md) | ADDRESS | =ADDRESS(1,1,4, TRUE(), "") | !A1 | ''!A1 | !A1 | | SEQUENCE | =SEQUENCE(0) | VALUE | N/A | CALC | | TAKE | =TAKE(A1:A3, 0) | N/A | No such function. | CALC | -| TAKE | =TAKE(Data!A:A, , 1) | VALUE because HyperFormula cannot allocate an unbounded direct whole-column result. | No such function. | Spills the whole column from row 1; returns SPILL below row 1. | +| TAKE | =TAKE(Data!A:A, , 1) | Spills the whole column from row 1; returns SPILL below row 1. | No such function. | Spills the whole column from row 1; returns SPILL below row 1. | | INT | =INT(-8.9) | -8 | -9 | -9 | | MOD | =MOD(-10, 3) | -1 | 2 | 2 | | ISEVEN | =ISEVEN(2.5) | FALSE | TRUE | TRUE | diff --git a/src/interpreter/plugin/ArrayPlugin.ts b/src/interpreter/plugin/ArrayPlugin.ts index a10a8e4fd..13876bc65 100644 --- a/src/interpreter/plugin/ArrayPlugin.ts +++ b/src/interpreter/plugin/ArrayPlugin.ts @@ -21,14 +21,16 @@ type TakeLiteralDimension = export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypecheck { /** - * Classifies a TAKE count for static result-size prediction. + * Evaluates the dependency-free subset of TAKE count expressions used for + * static result-size prediction. * - * @param {Ast | undefined} argument - The count argument to inspect before evaluation. - * @returns {TakeLiteralDimension} The literal value, an invalid-literal marker, or an unresolved marker. + * @param {Ast | undefined} argument - The count expression to inspect before evaluation. + * @returns {TakeLiteralDimension} The constant value, an invalid-literal marker, or an unresolved marker. + * @internal */ - private parseTakeLiteralDimension(argument: Ast | undefined): TakeLiteralDimension { + private parseTakeLiteralNumber(argument: Ast | undefined): TakeLiteralDimension { if (argument?.type === AstNodeType.NUMBER) { - return {kind: 'value', value: Math.abs(Math.trunc(argument.value))} + return {kind: 'value', value: argument.value} } if (argument?.type === AstNodeType.STRING) { @@ -36,19 +38,78 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche if (coercedValue === undefined) { return {kind: 'invalid'} } - return {kind: 'value', value: Math.abs(Math.trunc(getRawValue(coercedValue)))} + return {kind: 'value', value: getRawValue(coercedValue)} + } + + if (argument?.type === AstNodeType.PLUS_UNARY_OP || argument?.type === AstNodeType.MINUS_UNARY_OP) { + const dimension = this.parseTakeLiteralNumber(argument.value) + if (dimension.kind !== 'value') { + return dimension + } + return {kind: 'value', value: argument.type === AstNodeType.MINUS_UNARY_OP ? -dimension.value : dimension.value} + } + + if (argument?.type === AstNodeType.PARENTHESIS) { + return this.parseTakeLiteralNumber(argument.expression) + } + + if (argument?.type === AstNodeType.FUNCTION_CALL && argument.args.length === 0) { + if (argument.procedureName === 'TRUE') { + return {kind: 'value', value: 1} + } + if (argument.procedureName === 'FALSE') { + return {kind: 'value', value: 0} + } } - if ( - (argument?.type === AstNodeType.PLUS_UNARY_OP || argument?.type === AstNodeType.MINUS_UNARY_OP) - && argument.value.type === AstNodeType.NUMBER - ) { - return {kind: 'value', value: Math.abs(Math.trunc(argument.value.value))} + if (argument?.type === AstNodeType.PLUS_OP) { + const left = this.parseTakeLiteralNumber(argument.left) + const right = this.parseTakeLiteralNumber(argument.right) + if (left.kind === 'invalid' || right.kind === 'invalid') { + return {kind: 'invalid'} + } + if (left.kind === 'value' && right.kind === 'value') { + return {kind: 'value', value: left.value + right.value} + } } return {kind: 'unresolved'} } + /** + * Converts a statically resolved TAKE count into its output dimension. + * + * @param {Ast | undefined} argument - The count expression to classify. + * @returns {TakeLiteralDimension} The non-negative truncated dimension or its unresolved classification. + * @internal + */ + private parseTakeLiteralDimension(argument: Ast | undefined): TakeLiteralDimension { + const dimension = this.parseTakeLiteralNumber(argument) + return dimension.kind === 'value' + ? {kind: 'value', value: Math.abs(Math.trunc(dimension.value))} + : dimension + } + + /** + * Resolves a direct TAKE source reference without evaluating its values. + * The range supplies materialized dimensions only; spill placement never + * depends on its sheet. + * + * @param {Ast} argument - The source expression to inspect. + * @param {InterpreterState} state - The formula state used to resolve relative addresses. + * @returns {AbsoluteCellRange | undefined} The source range, or `undefined` for a computed array. + * @internal + */ + private takeSourceRange(argument: Ast, state: InterpreterState): AbsoluteCellRange | undefined { + if (argument.type === AstNodeType.PARENTHESIS) { + return this.takeSourceRange(argument.expression, state) + } + if (argument.type === AstNodeType.CELL_RANGE || argument.type === AstNodeType.COLUMN_RANGE || argument.type === AstNodeType.ROW_RANGE) { + return AbsoluteCellRange.fromAstOrUndef(argument, state.formulaAddress) + } + return undefined + } + public static implementedFunctions: ImplementedFunctions = { 'ARRAYFORMULA': { method: 'arrayformula', @@ -219,8 +280,9 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche * Syntactically empty dimensions keep all rows or columns. Counts that * evaluate to zero return a #N/A error. * - * @param ast - * @param state + * @param {ProcedureAst} ast - The parsed TAKE call. + * @param {InterpreterState} state - The current formula evaluation state. + * @returns {InterpreterValue} The selected source values or a spreadsheet error. */ public take(ast: ProcedureAst, state: InterpreterState): InterpreterValue { // The default supports TAKE(array, , columns), but the rows argument @@ -250,8 +312,17 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche const startColumn = requestedColumns > 0 ? 0 : sourceWidth - columnsToTake const sourceRange = range.range - // Keep address-backed ranges lazy to avoid materializing cells outside the TAKE result. if (sourceRange !== undefined) { + const leavesHeightUnbounded = !Number.isFinite(sourceRange.height()) && !Number.isFinite(requestedRows) + const leavesWidthUnbounded = !Number.isFinite(sourceRange.width()) && !Number.isFinite(requestedColumns) + const startsBelowFirstRow = leavesHeightUnbounded && state.formulaAddress.row !== 0 + const startsRightOfFirstColumn = leavesWidthUnbounded && state.formulaAddress.col !== 0 + + if (startsBelowFirstRow || startsRightOfFirstColumn) { + return new CellError(ErrorType.SPILL, ErrorMessage.NoSpaceForArrayResult) + } + + // Keep address-backed ranges lazy to avoid materializing cells outside the TAKE result. const resultRange = AbsoluteCellRange.spanFrom( sourceRange.getAddress(startColumn, startRow), columnsToTake, @@ -272,11 +343,13 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche /** * Calculates the spilled array size of TAKE using the source dimensions as - * the upper bound. Unbounded results are rejected so the dependency graph - * never reserves an infinite spill range. + * the upper bound. Unbounded dimensions are valid only at the corresponding + * output-sheet edge, then direct references use their effective dimensions + * to register the materialized spill footprint. * - * @param ast - * @param state + * @param {ProcedureAst} ast - The parsed TAKE call. + * @param {InterpreterState} state - The formula state whose address anchors the spill. + * @returns {ArraySize} The predicted result dimensions or an invalid size. */ public takeArraySize(ast: ProcedureAst, state: InterpreterState): ArraySize { if (ast.args.length < 2 || ast.args.length > 3) { @@ -298,12 +371,21 @@ export class ArrayPlugin extends FunctionPlugin implements FunctionPluginTypeche const height = rowDimension.kind === 'value' ? Math.min(sourceSize.height, rowDimension.value) : sourceSize.height const width = columnDimension.kind === 'value' ? Math.min(sourceSize.width, columnDimension.value) : sourceSize.width - - if (!Number.isFinite(height) || !Number.isFinite(width) || height < 1 || width < 1) { + const startsBelowFirstRow = !Number.isFinite(height) && state.formulaAddress.row !== 0 + const startsRightOfFirstColumn = !Number.isFinite(width) && state.formulaAddress.col !== 0 + const sourceRange = this.takeSourceRange(ast.args[0], state) + const effectiveHeight = !Number.isFinite(height) && sourceRange !== undefined + ? sourceRange.effectiveHeight(this.dependencyGraph) + : height + const effectiveWidth = !Number.isFinite(width) && sourceRange !== undefined + ? sourceRange.effectiveWidth(this.dependencyGraph) + : width + + if (startsBelowFirstRow || startsRightOfFirstColumn || effectiveHeight < 1 || effectiveWidth < 1) { return ArraySize.error() } - return new ArraySize(width, height) + return new ArraySize(effectiveWidth, effectiveHeight) } /** From 9e381163b31e2f6e2a783034d7111ed80a2c8880 Mon Sep 17 00:00:00 2001 From: tobiadefami Date: Mon, 17 Aug 2026 19:54:54 +0100 Subject: [PATCH 20/20] docs: document localized stack function names --- CHANGELOG.md | 4 ++++ docs/guide/release-notes.md | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5851706f4..90a754210 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Added the `TAKE` dynamic-array function. [#1722](https://github.com/handsontable/hyperformula/pull/1722) +### Changed + +- **Breaking change**: Replaced the placeholder English `VSTACK` and `HSTACK` names with their official localized names in 14 language packs. Formulas parsed with one of those language packs must use the localized function names. [#1722](https://github.com/handsontable/hyperformula/pull/1722) + ## [3.4.0] - 2026-08-10 ### Added diff --git a/docs/guide/release-notes.md b/docs/guide/release-notes.md index db77299be..c74f6ca7d 100644 --- a/docs/guide/release-notes.md +++ b/docs/guide/release-notes.md @@ -6,6 +6,12 @@ This page lists HyperFormula release notes. The format is based on HyperFormula adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- **Breaking change**: The Czech, Danish, German, Spanish, Finnish, French, Hungarian, Italian, Norwegian Bokmål, Dutch, Polish, Portuguese, Russian, and Turkish language packs now use the official localized names for `VSTACK` and `HSTACK`. If an existing formula uses the English `VSTACK` or `HSTACK` name while one of these language packs is active, update that formula to the localized function name exported by the language pack. + ## 3.4.0 **Release date: August 10, 2026**