Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
add3c53
Add built-in function implementation workflow and Claude Code skill
claude Feb 25, 2026
a4ce9a3
feat: implement SEQUENCE built-in function
claude Feb 25, 2026
de1f362
fix: make SEQUENCE empty args match Excel default behaviour
claude Feb 25, 2026
4b43c6c
docs: add SEQUENCE to built-in functions reference and changelog
claude Feb 25, 2026
37cce52
feat: fix SEQUENCE — remove EmptyValue workaround, support string lit…
marcin-kordas-hoc Mar 5, 2026
a138c34
fix: SEQUENCE review fixes — i18n translations, emptyAsDefault, fetch…
marcin-kordas-hoc Mar 18, 2026
2ca7f0b
fix: SEQUENCE error types — negative dims return #VALUE!, zero dims r…
marcin-kordas-hoc Mar 18, 2026
29b7f29
fix: SEQUENCE cleanup — remove irrelevant files, add smoke tests, fix…
marcin-kordas-hoc Mar 27, 2026
e809c59
docs: add SEQUENCE tech rationale and Excel validation script
marcin-kordas-hoc Mar 27, 2026
ce2a218
Revert "docs: add SEQUENCE tech rationale and Excel validation script"
marcin-kordas-hoc Mar 27, 2026
4fb332c
fix: guard SEQUENCE against Infinity dimensions from string coercion
marcin-kordas-hoc Mar 27, 2026
1bfb26b
fix: SEQUENCE unary ops in parseLiteralDimension, check negativity be…
marcin-kordas-hoc Mar 27, 2026
f7350a1
fix: SEQUENCE zero dimensions return #VALUE! instead of #NUM! (agreed…
marcin-kordas-hoc Mar 31, 2026
1c8d402
feat: recognize TRUE()/FALSE() in parseLiteralDimension for SEQUENCE …
marcin-kordas-hoc Mar 31, 2026
d4c0e0c
fix: add max dimension guard using Config.maxRows/maxColumns (#1646)
marcin-kordas-hoc Mar 31, 2026
9e9a05f
docs: add parse-time array sizing limitation to known-limitations
marcin-kordas-hoc Mar 31, 2026
57307b2
docs: add SEQUENCE error type divergences to list-of-differences
marcin-kordas-hoc Mar 31, 2026
826ec41
fix: update smoke test SEQUENCE error types from NUM to VALUE
marcin-kordas-hoc Mar 31, 2026
3167a0b
fix: Infinity coercion returns #VALUE! (matches Excel), add CHANGELOG…
marcin-kordas-hoc Apr 2, 2026
a82a120
fix: use ValueLarge error message for max dimension guard
marcin-kordas-hoc Apr 2, 2026
732906e
ci: retrigger CI after hyperformula-tests fix/1629 merge into feature…
marcin-kordas-hoc Apr 2, 2026
b08cd79
Address sequba review: move docs, revert release-notes, remove smoke …
marcin-kordas-hoc Apr 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

- Added `maxPendingLazyTransformations` configuration option to control memory usage by limiting accumulated transformations before cleanup. [#1629](https://github.com/handsontable/hyperformula/issues/1629)
- Added a new function: TEXTJOIN. [#1640](https://github.com/handsontable/hyperformula/pull/1640)
- Added a new function: SEQUENCE. [#1645](https://github.com/handsontable/hyperformula/pull/1645)

### Fixed

Expand Down
1 change: 1 addition & 0 deletions docs/guide/built-in-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ Total number of functions: **{{ $page.functionsCount }}**
| ARRAYFORMULA | Enables the array arithmetic mode for a single formula. | ARRAYFORMULA(Formula) |
| FILTER | Filters an array, based on multiple conditions (boolean arrays). | FILTER(SourceArray, BoolArray1, BoolArray2, ...BoolArrayN) |
| ARRAY_CONSTRAIN | Truncates an array to given dimensions. | ARRAY_CONSTRAIN(Array, Height, Width) |
| SEQUENCE | Returns an array of sequential numbers. | SEQUENCE(Rows, [Cols], [Start], [Step]) |

### Date and time

Expand Down
1 change: 1 addition & 0 deletions docs/guide/known-limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ you can't compare the arguments in a formula like this:
* For certain inputs, the RATE function might have no solutions, or have multiple solutions. Our implementation uses an iterative algorithm (Newton's method) to find an approximation for one of the solutions to within 1e-7. If the approximation is not found after 50 iterations, the RATE function returns the `#NUM!` error.
* The INDEX function doesn't support returning whole rows or columns of the source range – it always returns the contents of a single cell.
* The FILTER function accepts either single rows of equal width or single columns of equal height. In other words, all arrays passed to the FILTER function must have equal dimensions, and at least one of those dimensions must be 1.
* Array-producing functions (e.g., SEQUENCE, FILTER) require their output dimensions to be determinable at parse time. Passing cell references or formulas as dimension arguments (e.g., `=SEQUENCE(A1)`) results in a `#VALUE!` error, because the output size cannot be resolved before evaluation.
1 change: 1 addition & 0 deletions docs/guide/list-of-differences.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,4 @@ To remove the differences, create [custom implementations](custom-functions.md)
| DEVSQ | =DEVSQ(A2, A3) | 0.0000 | 0.0000 | NUM |
| 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 |
1 change: 1 addition & 0 deletions src/i18n/languages/csCZ.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ const dictionary: RawTranslationPackage = {
SEC: 'SEC',
SECH: 'SECH',
SECOND: 'SEKUNDA',
SEQUENCE: 'SEQUENCE',
SHEET: 'SHEET',
SHEETS: 'SHEETS',
SIN: 'SIN',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/daDK.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ const dictionary: RawTranslationPackage = {
SEC: 'SEC',
SECH: 'SECH',
SECOND: 'SEKUND',
SEQUENCE: 'SEKVENS',
SHEET: 'ARK',
SHEETS: 'ARK.FLERE',
SIN: 'SIN',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/deDE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ const dictionary: RawTranslationPackage = {
SEC: 'SEC',
SECH: 'SECHYP',
SECOND: 'SEKUNDE',
SEQUENCE: 'SEQUENZ',
SHEET: 'BLATT',
SHEETS: 'BLÄTTER',
SIN: 'SIN',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/enGB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ const dictionary: RawTranslationPackage = {
SEC: 'SEC',
SECH: 'SECH',
SECOND: 'SECOND',
SEQUENCE: 'SEQUENCE',
SHEET: 'SHEET',
SHEETS: 'SHEETS',
SIN: 'SIN',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/esES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ export const dictionary: RawTranslationPackage = {
SEC: 'SEC',
SECH: 'SECH',
SECOND: 'SEGUNDO',
SEQUENCE: 'SECUENCIA',
SHEET: 'HOJA',
SHEETS: 'HOJAS',
SIN: 'SENO',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/fiFI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ const dictionary: RawTranslationPackage = {
SEC: 'SEK',
SECH: 'SEKH',
SECOND: 'SEKUNNIT',
SEQUENCE: 'JAKSO',
SHEET: 'TAULUKKO',
SHEETS: 'TAULUKOT',
SIN: 'SIN',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/frFR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ const dictionary: RawTranslationPackage = {
SEC: 'SEC',
SECH: 'SECH',
SECOND: 'SECONDE',
SEQUENCE: 'SEQUENCE',
SHEET: 'FEUILLE',
SHEETS: 'FEUILLES',
SIN: 'SIN',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/huHU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ const dictionary: RawTranslationPackage = {
SEC: 'SEC',
SECH: 'SECH',
SECOND: 'MPERC',
SEQUENCE: 'SOROZAT',
SHEET: 'LAP',
SHEETS: 'LAPOK',
SIN: 'SIN',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/itIT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ const dictionary: RawTranslationPackage = {
SEC: 'SEC',
SECH: 'SECH',
SECOND: 'SECONDO',
SEQUENCE: 'SEQUENZA',
SHEET: 'FOGLIO',
SHEETS: 'FOGLI',
SIN: 'SEN',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/nbNO.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ const dictionary: RawTranslationPackage = {
SEC: 'SEC',
SECH: 'SECH',
SECOND: 'SEKUND',
SEQUENCE: 'SEKVENS',
SHEET: 'ARK',
SHEETS: 'SHEETS',
SIN: 'SIN',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/nlNL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ const dictionary: RawTranslationPackage = {
SEC: 'SEC',
SECH: 'SECH',
SECOND: 'SECONDE',
SEQUENCE: 'REEKS',
SHEET: 'BLAD',
SHEETS: 'BLADEN',
SIN: 'SIN',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/plPL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ const dictionary: RawTranslationPackage = {
SEC: 'SEC',
SECH: 'SECH',
SECOND: 'SEKUNDA',
SEQUENCE: 'SEKWENCJA',
SHEET: 'ARKUSZ',
SHEETS: 'ARKUSZE',
SIN: 'SIN',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/ptPT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ const dictionary: RawTranslationPackage = {
SEC: 'SEC',
SECH: 'SECH',
SECOND: 'SEGUNDO',
SEQUENCE: 'SEQUÊNCIA',
SHEET: 'PLANILHA',
SHEETS: 'PLANILHAS',
SIN: 'SEN',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/ruRU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ const dictionary: RawTranslationPackage = {
SEC: 'SEC',
SECH: 'SECH',
SECOND: 'СЕКУНДЫ',
SEQUENCE: 'ПОСЛЕДОВ',
SHEET: 'ЛИСТ',
SHEETS: 'ЛИСТЫ',
SIN: 'SIN',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/svSE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ const dictionary: RawTranslationPackage = {
SEC: 'SEC',
SECH: 'SECH',
SECOND: 'SEKUND',
SEQUENCE: 'SEKVENS',
SHEET: 'SHEET',
SHEETS: 'SHEETS',
SIN: 'SIN',
Expand Down
1 change: 1 addition & 0 deletions src/i18n/languages/trTR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ const dictionary: RawTranslationPackage = {
SEC: 'SEC',
SECH: 'SECH',
SECOND: 'SANİYE',
SEQUENCE: 'SIRA',
SHEET: 'SAYFA',
SHEETS: 'SAYFALAR',
SIN: 'SİN',
Expand Down
176 changes: 176 additions & 0 deletions src/interpreter/plugin/SequencePlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/**
* @license
* Copyright (c) 2025 Handsoncode. All rights reserved.
*/

import { ArraySize } from '../../ArraySize'
import { CellError, ErrorType } from '../../Cell'
import { ErrorMessage } from '../../error-message'
import { Ast, AstNodeType, ProcedureAst } from '../../parser'
import { InterpreterState } from '../InterpreterState'
import { InterpreterValue } from '../InterpreterValue'
import { SimpleRangeValue } from '../../SimpleRangeValue'
import { FunctionArgumentType, FunctionPlugin, FunctionPluginTypecheck, ImplementedFunctions } from './FunctionPlugin'

/**
* Plugin implementing the SEQUENCE spreadsheet function.
*
* SEQUENCE(rows, [cols], [start], [step]) returns a rows×cols array of
* sequential numbers starting at `start` and incrementing by `step`.
*/
export class SequencePlugin extends FunctionPlugin implements FunctionPluginTypecheck<SequencePlugin> {
/**
* Minimum valid value for the `rows` and `cols` arguments.
* Extracted to avoid duplicating the check between `sequence()` (runtime) and
* `sequenceArraySize()` (parse time).
*/
private static readonly MIN_DIMENSION = 1

/** Returns true when `n` is a finite number at least {@link MIN_DIMENSION}. */
private static isValidDimension(n: number): boolean {
return Number.isFinite(n) && n >= SequencePlugin.MIN_DIMENSION
}

/**
* Parses a literal dimension from an AST node at parse time.
* Handles NUMBER nodes directly, STRING nodes via numeric coercion,
* PLUS/MINUS_UNARY_OP wrapping a NUMBER (e.g. `+3`, `-2`),
* and TRUE()/FALSE() function calls (returning 1/0).
* Returns undefined for non-literal nodes (cell refs, formulas, binary ops).
*/
private static parseLiteralDimension(node: Ast): number | undefined {
if (node.type === AstNodeType.NUMBER) {
return Math.trunc(node.value)
}
if (node.type === AstNodeType.STRING) {
const parsed = Number(node.value)
return Number.isFinite(parsed) ? Math.trunc(parsed) : undefined
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Locale numeric strings mis-sized at parse time

Medium Severity

parseLiteralDimension() uses JavaScript Number() for string literals, which ignores HyperFormula locale rules. Strings that runtime coercion accepts (for configured decimal/thousand separators) can be rejected at parse-time size prediction, causing ArraySize.error() and #VALUE! instead of a spilled SEQUENCE result.

Additional Locations (1)
Fix in Cursor Fix in Web

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Accepted limitation. Parse-time uses JavaScript Number() which doesn't respect HF locale settings. Documented in list-of-differences.

}
if (node.type === AstNodeType.PLUS_UNARY_OP && node.value.type === AstNodeType.NUMBER) {
return Math.trunc(node.value.value)
}
if (node.type === AstNodeType.MINUS_UNARY_OP && node.value.type === AstNodeType.NUMBER) {
return Math.trunc(-node.value.value)
}
if (node.type === AstNodeType.FUNCTION_CALL) {
if (node.procedureName === 'TRUE' && node.args.length === 0) {
return 1
}
if (node.procedureName === 'FALSE' && node.args.length === 0) {
return 0
}
}
return undefined
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Parenthesized dimensions rejected as non-literal

Medium Severity

sequenceArraySize() only accepts a narrow literal subset and parseLiteralDimension() doesn’t unwrap AstNodeType.PARENTHESIS. Expressions like =SEQUENCE((3)) become “unknown size”, return ArraySize.error(), and end up as #VALUE! instead of producing a valid sequence.

Additional Locations (1)
Fix in Cursor Fix in Web

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Accepted limitation. Parenthesized expressions like =SEQUENCE((3)) are not unwrapped at parse time. Minor edge case — users can write =SEQUENCE(3).


public static implementedFunctions: ImplementedFunctions = {
'SEQUENCE': {
method: 'sequence',
sizeOfResultArrayMethod: 'sequenceArraySize',
parameters: [
{ argumentType: FunctionArgumentType.NUMBER },
{ argumentType: FunctionArgumentType.NUMBER, defaultValue: 1, emptyAsDefault: true },
{ argumentType: FunctionArgumentType.NUMBER, defaultValue: 1, emptyAsDefault: true },
{ argumentType: FunctionArgumentType.NUMBER, defaultValue: 1, emptyAsDefault: true },
],
vectorizationForbidden: true,
},
}

/**
* Corresponds to SEQUENCE(rows, [cols], [start], [step])
*
* Returns a rows×cols array of sequential numbers starting at `start`
* and incrementing by `step`, filled row-major.
*
* Note: dynamic arguments (cell references, formulas) for `rows` or `cols`
* cause a size mismatch between parse-time prediction and runtime result,
* which results in a #VALUE! error. Use literal numbers for rows and cols.
*
* @param {ProcedureAst} ast - The parsed function call AST node.
* @param {InterpreterState} state - Current interpreter evaluation state.
*/
public sequence(ast: ProcedureAst, state: InterpreterState): InterpreterValue {
return this.runFunction(ast.args, state, this.metadata('SEQUENCE'),
(rows: number, cols: number, start: number, step: number) => {
if (!Number.isFinite(rows) || !Number.isFinite(cols)) {
return new CellError(ErrorType.VALUE, ErrorMessage.ValueLarge)
}

if (rows < 0 || cols < 0) {
return new CellError(ErrorType.VALUE, ErrorMessage.LessThanOne)
}

const numRows = Math.trunc(rows)
const numCols = Math.trunc(cols)

if (!SequencePlugin.isValidDimension(numRows) || !SequencePlugin.isValidDimension(numCols)) {
return new CellError(ErrorType.VALUE, ErrorMessage.LessThanOne)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Zero dimensions return wrong error type

Medium Severity

SEQUENCE maps zero-sized dimensions to ErrorType.VALUE via isValidDimension, so =SEQUENCE(0) and =SEQUENCE(1,0) return #VALUE! instead of the intended #NUM! behavior. This makes zero and negative dimensions indistinguishable and breaks the documented/tested error contract for SEQUENCE.

Additional Locations (1)
Fix in Cursor Fix in Web

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in commit f7350a1 — zero dimensions now return #VALUE! instead of #NUM!, matching Excel. Agreed with reviewer on the call.


if (numRows > this.config.maxRows || numCols > this.config.maxColumns) {
return new CellError(ErrorType.VALUE, ErrorMessage.ValueLarge)
}

const result: number[][] = []
for (let r = 0; r < numRows; r++) {
const row: number[] = []
for (let c = 0; c < numCols; c++) {
row.push(start + (r * numCols + c) * step)
}
result.push(row)
}

return SimpleRangeValue.onlyNumbers(result)
}
)
}

/**
* Predicts the output array size for SEQUENCE at parse time.
*
* Handles NUMBER and STRING literals for rows/cols via `parseLiteralDimension`.
* Non-literal args (cell refs, formulas, unary/binary ops) fall back to 1,
* which will cause a size mismatch at eval time when the actual result is larger.
*
* @param {ProcedureAst} ast - The parsed function call AST node.
* @param {InterpreterState} _state - Current interpreter evaluation state (unused).
*/
public sequenceArraySize(ast: ProcedureAst, _state: InterpreterState): ArraySize {
if (ast.args.length < 1 || ast.args.length > 4) {
return ArraySize.error()
}

const rowsArg = ast.args[0]
const colsArg = ast.args.length > 1 ? ast.args[1] : undefined

// Non-literal rows (cell ref, formula, unary/binary op): size unknown at parse time.
// Fall back to scalar so the engine creates a ScalarFormulaVertex instead of an
// ArrayFormulaVertex. The actual evaluation will propagate errors or return #VALUE!
// via the Exporter if the result is larger than 1×1.
if (rowsArg.type === AstNodeType.EMPTY) {
return ArraySize.error()
}
const rows = SequencePlugin.parseLiteralDimension(rowsArg)
if (rows === undefined) {
return ArraySize.error()
}

const cols = (colsArg === undefined || colsArg.type === AstNodeType.EMPTY)
? 1
: SequencePlugin.parseLiteralDimension(colsArg)
if (cols === undefined) {
return ArraySize.error()
}

if (!SequencePlugin.isValidDimension(rows) || !SequencePlugin.isValidDimension(cols)) {
return ArraySize.error()
}

if (rows > this.config.maxRows || cols > this.config.maxColumns) {
return ArraySize.error()
}

return new ArraySize(cols, rows)
}
}
1 change: 1 addition & 0 deletions src/interpreter/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export {PowerPlugin} from './PowerPlugin'
export {RadiansPlugin} from './RadiansPlugin'
export {RadixConversionPlugin} from './RadixConversionPlugin'
export {RandomPlugin} from './RandomPlugin'
export {SequencePlugin} from './SequencePlugin'
export {RoundingPlugin} from './RoundingPlugin'
export {SqrtPlugin} from './SqrtPlugin'
export {ConditionalAggregationPlugin} from './ConditionalAggregationPlugin'
Expand Down
2 changes: 1 addition & 1 deletion test/fetch-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ git fetch origin
if git show-ref --verify --quiet "refs/heads/$CURRENT_BRANCH" || \
git show-ref --verify --quiet "refs/remotes/origin/$CURRENT_BRANCH"; then
git checkout "$CURRENT_BRANCH"
git pull # pull latest changes
git pull origin "$CURRENT_BRANCH" # pull latest changes
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull command fails for local-only branches

Low Severity

After checkout, git pull origin "$CURRENT_BRANCH" now requires the branch to exist on origin. The guard allows entering this branch when only a local branch exists, so the script can exit under set -e with “remote ref not found” for local-only branches.

Fix in Cursor Fix in Web

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed — fetch-tests.sh now uses explicit branch in git pull.

else
echo "Branch $CURRENT_BRANCH not found in hyperformula-tests, creating from develop..."
git checkout develop
Expand Down
Loading