Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

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

### Added

- Added iterative calculation support for circular references. [#1545](https://github.com/handsontable/hyperformula/issues/1545)
- Added a new function: IRR. [#1591](https://github.com/handsontable/hyperformula/issues/1591)
- Added a new function: N. [#1585](https://github.com/handsontable/hyperformula/issues/1585)
- Added a new function: VALUE. [#1592](https://github.com/handsontable/hyperformula/issues/1592)
Expand Down
1 change: 1 addition & 0 deletions docs/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ module.exports = {
['/guide/volatile-functions', 'Volatile functions'],
['/guide/named-expressions', 'Named expressions'],
['/guide/arrays', 'Array formulas'],
['/guide/iterative-calculation', 'Iterative calculation'],
]
},
{
Expand Down
160 changes: 160 additions & 0 deletions docs/guide/iterative-calculation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# Iterative calculation

Use iterative calculation to resolve circular references in your spreadsheet formulas.

## What is iterative calculation?

A **circular reference** occurs when a formula refers back to its own cell, either directly or through a chain of other cells. For example:

- Direct: `A1` contains `=A1+1`
- Indirect: `A1` contains `=B1+1` and `B1` contains `=A1+1`

By default, HyperFormula returns a [`#CYCLE!`](types-of-errors.md) error for circular references. However, some spreadsheet models intentionally use circular references for iterative calculations, such as:

- **Financial modeling**: calculating loan payments where interest depends on the balance
- **Goal seeking**: finding values that satisfy a target condition
- **Numerical methods**: implementing Newton-Raphson or other iterative algorithms
- **Feedback systems**: modeling systems where outputs influence inputs

When iterative calculation is enabled, HyperFormula repeatedly evaluates the circular formulas until the values stabilize (converge) or a maximum number of iterations is reached.

## How it works

HyperFormula uses the **Gauss-Seidel iteration method**:

1. All cells in the cycle are initialized to a starting value (default: `0`)
2. Cells are evaluated in address order (by sheet, then row, then column)
3. Each cell immediately uses the most recently computed values from other cells
4. After each complete pass, HyperFormula checks if all values have converged
5. Iteration stops when values converge or the maximum iterations are reached

### Convergence

A cell is considered **converged** when the change between iterations is below the threshold:

- **Numeric values**: `|new - old| < threshold`
- **Non-numeric values**: exact equality (strings, booleans, errors)

All cells in the cycle must converge for iteration to stop early.

## Enabling iterative calculation

To enable iterative calculation, set the [`iterativeCalculationEnable`](../api/interfaces/configparams.html#iterativecalculationenable) option to `true`:

```javascript
const hfInstance = HyperFormula.buildFromArray(
[
['=A1+1'], // A1 references itself
],
{
licenseKey: 'gpl-v3',
iterativeCalculationEnable: true,
}
);

// With default settings (100 iterations, threshold 0.001, initial value 0):
// A1 will equal 100 after iteration completes
console.log(hfInstance.getCellValue({ sheet: 0, row: 0, col: 0 })); // 100
```

::: tip
Iterative calculation settings are configured at engine initialization and apply to all sheets.
:::

## Configuration options

| Option | Type | Default | Description |
|:-------|:-----|:--------|:------------|
| [`iterativeCalculationEnable`](../api/interfaces/configparams.html#iterativecalculationenable) | `boolean` | `false` | Enable iterative calculation for circular references |
| [`iterativeCalculationMaxIterations`](../api/interfaces/configparams.html#iterativecalculationmaxiterations) | `number` | `100` | Maximum number of iterations before stopping |
| [`iterativeCalculationConvergenceThreshold`](../api/interfaces/configparams.html#iterativecalculationconvergencethreshold) | `number` | `0.001` | Values must change by less than this to be considered converged |
| [`iterativeCalculationInitialValue`](../api/interfaces/configparams.html#iterativecalculationinitialvalue) | `number \| string \| boolean \| Date` | `0` | Starting value for cells in circular references |

## Examples

### Simple accumulator

A cell that adds 1 on each iteration:

```javascript
const hf = HyperFormula.buildFromArray(
[['=A1+1']],
{
licenseKey: 'gpl-v3',
iterativeCalculationEnable: true,
iterativeCalculationMaxIterations: 50,
}
);

// A1 = 50 (after 50 iterations starting from 0)
console.log(hf.getCellValue({ sheet: 0, row: 0, col: 0 })); // 50
```

### Converging system

Two cells that converge to a stable value:

```javascript
const hf = HyperFormula.buildFromArray(
[
['=B1/2 + 1', '=A1/2 + 1'], // A1 and B1 reference each other
],
{
licenseKey: 'gpl-v3',
iterativeCalculationEnable: true,
iterativeCalculationConvergenceThreshold: 0.0001,
}
);

// Both cells converge to 2
console.log(hf.getCellValue({ sheet: 0, row: 0, col: 0 })); // ~2 (A1)
console.log(hf.getCellValue({ sheet: 0, row: 0, col: 1 })); // ~2 (B1)
```

### Custom initial value

Start iteration from a specific value:

```javascript
const hf = HyperFormula.buildFromArray(
[['=A1*0.5']], // Halves on each iteration
{
licenseKey: 'gpl-v3',
iterativeCalculationEnable: true,
iterativeCalculationInitialValue: 1000,
iterativeCalculationConvergenceThreshold: 0.01,
}
);

// Converges toward 0, starting from 1000
console.log(hf.getCellValue({ sheet: 0, row: 0, col: 0 })); // ~0
```

## Behavior notes

### Non-converging formulas

::: warning
Some formulas never converge (e.g., `=A1+1` always increases). In these cases, iteration runs until `iterativeCalculationMaxIterations` is reached, and the final value is used.
:::

### Error handling

If a formula in the cycle produces an error during iteration (e.g., `#DIV/0!`), the error becomes the cell's final value. Cells depending on error values will propagate the error.

### Ranges containing cycles

If a range reference (e.g., `SUM(A1:A10)`) includes cells that are part of a cycle, the range is recalculated on each iteration to reflect the updated values.

### Recalculation behavior

When any cell in a circular reference is modified, the entire cycle is re-evaluated from the initial value. Previous converged values are not preserved between recalculations.

## Compatibility

This feature is compatible with:

- **Microsoft Excel**: [Iterative calculation settings](https://support.microsoft.com/en-gb/office/remove-or-allow-a-circular-reference-in-excel-8540bd0f-6e97-4483-bcf7-1b49cd50d123)
- **Google Sheets**: [Iterative calculation settings](https://workspaceupdates.googleblog.com/2016/12/new-iterative-calculation-settings-and.html)

The default configuration values match Excel's defaults.
42 changes: 42 additions & 0 deletions src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {FunctionPluginDefinition} from './interpreter'
import {Maybe} from './Maybe'
import {ParserConfig} from './parser/ParserConfig'
import {ConfigParams, ConfigParamsList} from './ConfigParams'
import { RawCellContent } from '.'

const privatePool: WeakMap<Config, { licenseKeyValidityState: LicenseKeyValidityState }> = new WeakMap()

Expand Down Expand Up @@ -67,6 +68,10 @@ export class Config implements ConfigParams, ParserConfig {
useColumnIndex: false,
useStats: false,
useArrayArithmetic: false,
iterativeCalculationEnable: false,
iterativeCalculationMaxIterations: 100,
iterativeCalculationConvergenceThreshold: 0.001,
iterativeCalculationInitialValue: 0,
}

/** @inheritDoc */
Expand Down Expand Up @@ -160,6 +165,14 @@ export class Config implements ConfigParams, ParserConfig {
public readonly useWildcards: boolean
/** @inheritDoc */
public readonly matchWholeCell: boolean
/** @inheritDoc */
public readonly iterativeCalculationEnable: boolean
/** @inheritDoc */
public readonly iterativeCalculationMaxIterations: number
/** @inheritDoc */
public readonly iterativeCalculationConvergenceThreshold: number
/** @inheritDoc */
public readonly iterativeCalculationInitialValue: RawCellContent

constructor(options: Partial<ConfigParams> = {}, showDeprecatedWarns: boolean = true) {
const {
Expand Down Expand Up @@ -201,6 +214,10 @@ export class Config implements ConfigParams, ParserConfig {
useColumnIndex,
useRegularExpressions,
useWildcards,
iterativeCalculationEnable,
iterativeCalculationMaxIterations,
iterativeCalculationConvergenceThreshold,
iterativeCalculationInitialValue,
} = options

if (showDeprecatedWarns) {
Expand Down Expand Up @@ -255,6 +272,14 @@ export class Config implements ConfigParams, ParserConfig {
validateNumberToBeAtLeast(this.maxColumns, 'maxColumns', 1)
this.context = context

// Iterative calculation config
this.iterativeCalculationEnable = configValueFromParam(iterativeCalculationEnable, 'boolean', 'iterativeCalculationEnable')
this.iterativeCalculationMaxIterations = configValueFromParam(iterativeCalculationMaxIterations, 'number', 'iterativeCalculationMaxIterations')
this.validateIterativeCalculationMaxIterations(this.iterativeCalculationMaxIterations)
this.iterativeCalculationConvergenceThreshold = configValueFromParam(iterativeCalculationConvergenceThreshold, 'number', 'iterativeCalculationConvergenceThreshold')
validateNumberToBeAtLeast(this.iterativeCalculationConvergenceThreshold, 'iterativeCalculationConvergenceThreshold', 0)
this.iterativeCalculationInitialValue = this.validateIterativeCalculationInitialValue(iterativeCalculationInitialValue)

privatePool.set(this, {
licenseKeyValidityState: checkLicenseKeyValidity(this.licenseKey)
})
Expand Down Expand Up @@ -287,6 +312,23 @@ export class Config implements ConfigParams, ParserConfig {
return valueAfterCheck as string[]
}

private validateIterativeCalculationMaxIterations(value: number): void {
if (!Number.isInteger(value) || value < 1) {
throw new ExpectedValueOfTypeError('positive integer', 'iterativeCalculationMaxIterations')
}
}

private validateIterativeCalculationInitialValue(value: unknown): RawCellContent {
if (value === undefined) {
return Config.defaultConfig.iterativeCalculationInitialValue
}
if (typeof value === 'number' || typeof value === 'string' || typeof value === 'boolean' || value instanceof Date) {
return value as RawCellContent
}

throw new ExpectedValueOfTypeError('number, string, boolean or Date object', 'iterativeCalculationInitialValue')
}

/**
* Proxied property to its private counterpart. This makes the property
* as accessible as the other Config options but without ability to change the value.
Expand Down
42 changes: 41 additions & 1 deletion src/ConfigParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import {ChooseAddressMapping} from './DependencyGraph/AddressMapping/ChooseAddressMappingPolicy'
import {DateTime, SimpleDate, SimpleDateTime, SimpleTime} from './DateTimeHelper'
import {Maybe} from './Maybe'
import { RawCellContent } from '.'

export interface ConfigParams {
/**
Expand Down Expand Up @@ -391,7 +392,7 @@ export interface ConfigParams {
*
* Useful for testing and benchmarking.
* @default false
* @category Engine
* @internal
*/
useStats: boolean,
/**
Expand All @@ -414,6 +415,45 @@ export interface ConfigParams {
* @category String
*/
useWildcards: boolean,
/**
* When set to `true`, enables iterative calculation for circular references.
*
* When disabled, circular references result in a #CYCLE! error.
*
* For more information, see the Excel and Google Sheets documentation on iterative calculations.
* @default false
* @category Engine
*/
iterativeCalculationEnable: boolean,
/**
* Sets the maximum number of iterations for iterative calculation.
*
* Must be a positive integer (>= 1).
*
* @default 100
* @category Engine
*/
iterativeCalculationMaxIterations: number,
/**
* Sets the convergence threshold for iterative calculation.
*
* Iteration stops when the change between successive values is strictly less than this threshold.
*
* Must be >= 0.
*
* @default 0.001
* @category Engine
*/
iterativeCalculationConvergenceThreshold: number,
/**
* Sets the initial value used for cells in circular references during iterative calculation.
*
* Can be a number, string, boolean or date/time value.
*
* @default 0
* @category Engine
*/
iterativeCalculationInitialValue: RawCellContent,
}

export type ConfigParamsList = keyof ConfigParams
Loading
Loading