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

Filter by extension

Filter by extension

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

## [Unreleased]

### Fixed

- Fixed an issue where overwriting a non-computed cell caused the `Value of the formula cell is not computed` error. [#1194](https://github.com/handsontable/hyperformula/issues/1194)

## [3.1.0] - 2025-10-14

### Changed
Expand Down
83 changes: 70 additions & 13 deletions src/Operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ import {
ParsingErrorVertex,
SheetMapping,
SparseStrategy,
ValueCellVertex
ValueCellVertex,
} from './DependencyGraph'
import { FormulaVertex } from './DependencyGraph/FormulaCellVertex'
import { RawAndParsedValue } from './DependencyGraph/ValueCellVertex'
import { RawAndParsedValue, ValueCellVertexValue } from './DependencyGraph/ValueCellVertex'
import { AddColumnsTransformer } from './dependencyTransformers/AddColumnsTransformer'
import { AddRowsTransformer } from './dependencyTransformers/AddRowsTransformer'
import { CleanOutOfScopeDependenciesTransformer } from './dependencyTransformers/CleanOutOfScopeDependenciesTransformer'
Expand Down Expand Up @@ -459,8 +459,6 @@ export class Operations {

/**
* Restores a single cell.
* @param {SimpleCellAddress} address
* @param {ClipboardCell} clipboardCell
*/
public restoreCell(address: SimpleCellAddress, clipboardCell: ClipboardCell): void {
switch (clipboardCell.type) {
Expand Down Expand Up @@ -570,7 +568,6 @@ export class Operations {

this.setFormulaToCell(address, size, parserResult)
} catch (error) {

if (!(error as Error).message) {
throw error
}
Expand Down Expand Up @@ -598,45 +595,66 @@ export class Operations {
}
}

/**
* Sets cell content to an instance of parsing error.
* Creates a ParsingErrorVertex and updates the dependency graph and column search index.
*/
public setParsingErrorToCell(rawInput: string, errors: ParsingError[], address: SimpleCellAddress) {
const oldValue = this.dependencyGraph.getCellValue(address)
this.removeCellValueFromColumnSearch(address)

const vertex = new ParsingErrorVertex(errors, rawInput)
const arrayChanges = this.dependencyGraph.setParsingErrorToCell(address, vertex)
this.columnSearch.remove(getRawValue(oldValue), address)

this.columnSearch.applyChanges(arrayChanges.getChanges())
this.changes.addAll(arrayChanges)
this.changes.addChange(vertex.getCellValue(), address)
}

/**
* Sets cell content to a formula.
* Creates a FormulaCellVertex and updates the dependency graph and column search index.
*/
public setFormulaToCell(address: SimpleCellAddress, size: ArraySize, {
ast,
hasVolatileFunction,
hasStructuralChangeFunction,
dependencies
}: ParsingResult) {
const oldValue = this.dependencyGraph.getCellValue(address)
this.removeCellValueFromColumnSearch(address)

const arrayChanges = this.dependencyGraph.setFormulaToCell(address, ast, absolutizeDependencies(dependencies, address), size, hasVolatileFunction, hasStructuralChangeFunction)
this.columnSearch.remove(getRawValue(oldValue), address)

this.columnSearch.applyChanges(arrayChanges.getChanges())
this.changes.addAll(arrayChanges)
}

/**
* Sets cell content to a value.
* Creates a ValueCellVertex and updates the dependency graph and column search index.
*/
public setValueToCell(value: RawAndParsedValue, address: SimpleCellAddress) {
const oldValue = this.dependencyGraph.getCellValue(address)
this.changeCellValueInColumnSearch(address, value.parsedValue)

const arrayChanges = this.dependencyGraph.setValueToCell(address, value)
this.columnSearch.change(getRawValue(oldValue), getRawValue(value.parsedValue), address)

this.columnSearch.applyChanges(arrayChanges.getChanges().filter(change => !equalSimpleCellAddress(change.address, address)))
this.changes.addAll(arrayChanges)
this.changes.addChange(value.parsedValue, address)
}

/**
* Sets cell content to an empty value.
* Creates an EmptyCellVertex and updates the dependency graph and column search index.
*/
public setCellEmpty(address: SimpleCellAddress) {
if (this.dependencyGraph.isArrayInternalCell(address)) {
return
}
const oldValue = this.dependencyGraph.getCellValue(address)

this.removeCellValueFromColumnSearch(address)

const arrayChanges = this.dependencyGraph.setCellEmpty(address)
this.columnSearch.remove(getRawValue(oldValue), address)

this.columnSearch.applyChanges(arrayChanges.getChanges())
this.changes.addAll(arrayChanges)
this.changes.addChange(EmptyValue, address)
Expand Down Expand Up @@ -923,6 +941,45 @@ export class Operations {
}
return this.dependencyGraph.fetchCellOrCreateEmpty(expression.address).vertex
}

/**
* Removes a cell value from the columnSearch index.
* Ignores the non-computed formula vertices.
*/
private removeCellValueFromColumnSearch(address: SimpleCellAddress): void {
if (this.isNotComputed(address)) {
return
}

const oldValue = this.dependencyGraph.getCellValue(address)
this.columnSearch.remove(getRawValue(oldValue), address)
}

/**
* Changes a cell value in the columnSearch index.
* Ignores the non-computed formula vertices.
*/
private changeCellValueInColumnSearch(address: SimpleCellAddress, newValue: ValueCellVertexValue): void {
if (this.isNotComputed(address)) {
return
}

const oldValue = this.dependencyGraph.getCellValue(address)
this.columnSearch.change(getRawValue(oldValue), getRawValue(newValue), address)
}

/**
* Checks if the FormulaCellVertex or ArrayVertex at the given address is not computed.
*/
private isNotComputed(address: SimpleCellAddress): boolean {
const vertex = this.dependencyGraph.getCell(address)

if (!vertex) {
return false
}

return 'isComputed' in vertex && !vertex.isComputed()
}
}

export function normalizeRemovedIndexes(indexes: ColumnRowIndex[]): ColumnRowIndex[] {
Expand Down
5 changes: 4 additions & 1 deletion test/unit/column-index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {ColumnIndex} from '../../src/Lookup/ColumnIndex'
import {NamedExpressions} from '../../src/NamedExpressions'
import {ColumnsSpan, RowsSpan} from '../../src/Span'
import {Statistics} from '../../src/statistics'
import {adr, expectColumnIndexToMatchSheet} from './testUtils'
import {adr, detailedError, expectColumnIndexToMatchSheet} from './testUtils'
import { ErrorMessage } from '../../src/error-message'

function buildEmptyIndex(transformingService: LazilyTransformingAstService, config: Config, statistics: Statistics): ColumnIndex {
const functionRegistry = new FunctionRegistry(config)
Expand Down Expand Up @@ -600,6 +601,8 @@ describe('Arrays', () => {

engine.setCellContents(adr('D1'), [['foo']])

expect(engine.getCellValue(adr('C1'))).toEqualError(detailedError(ErrorType.SPILL, ErrorMessage.NoSpaceForArrayResult))

expectColumnIndexToMatchSheet([
[1, 2, null, 'foo']
], engine)
Expand Down
18 changes: 18 additions & 0 deletions test/unit/computation-suspension.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,4 +206,22 @@ describe('Evaluation suspension', () => {
expect(changes).toContainEqual(new ExportedCellChange(adr('B1'), 4))
expect(changes).toContainEqual(new ExportedCellChange(adr('C1'), 5))
})

it('allows to update cell content of a not computed formula cell (#1194)', () => {
const hf = HyperFormula.buildFromArray([], {
licenseKey: 'gpl-v3'
})

hf.suspendEvaluation()
hf.setCellContents(adr('A1'), '=42')
hf.setCellContents(adr('A1'), 42)
hf.setCellContents(adr('A1'), '=42')
hf.setCellContents(adr('A1'), null)
hf.setCellContents(adr('A1'), '=42')
hf.setCellContents(adr('A1'), '==')
hf.setCellContents(adr('A1'), '=42')
hf.setCellContents(adr('A1'), '=42')
hf.resumeEvaluation()
expect(hf.getCellValue(adr('A1'))).toBe(42)
})
})
10 changes: 10 additions & 0 deletions test/unit/rebuild.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,14 @@ describe('Rebuilding engine', () => {

expect(rebuildEngineSpy).toHaveBeenCalled()
})

it('doesn\'t throw after adding named expression (#1194)', () => {
const hf = HyperFormula.buildFromArray([['=42']], {
licenseKey: 'gpl-v3'
})


hf.addNamedExpression('ABC', '=Sheet1!$A$1')
expect(() => hf.rebuildAndRecalculate()).not.toThrow()
})
})
2 changes: 1 addition & 1 deletion test/unit/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ export function columnIndexToSheet(columnIndex: ColumnIndex, width: number, heig
for (const row of index) {
result[row] = result[row] ?? []
if (result[row][col] !== undefined) {
throw new Error('ColumnIndex ambiguity.')
throw new Error(`ColumnIndex ambiguity. Expected ${JSON.stringify(result[row][col])}, but found ${JSON.stringify(value)} at row ${JSON.stringify(row)}, column ${JSON.stringify(col)}`)
}
result[row][col] = value
}
Expand Down
10 changes: 10 additions & 0 deletions test/unit/update-config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,14 @@ describe('update config', () => {
engine.updateConfig({ useColumnIndex: false, functionArgSeparator: ',', undoLimit: 20 })
expect(rebuildEngineSpy).not.toHaveBeenCalled()
})

it('doesn\'t throw after adding named expression (#1194)', () => {
const hf = HyperFormula.buildFromArray([['=42']], {
licenseKey: 'gpl-v3'
})


hf.addNamedExpression('ABC', '=Sheet1!$A$1')
expect(() => hf.updateConfig({ maxRows: 101 })).not.toThrow()
})
})
Loading