fix(formula): fix where the formula calculation could not be recalculated after it was interrupted in certain situations (#6465)

This commit is contained in:
wpxp123456
2026-01-14 16:48:50 +08:00
committed by GitHub
parent fa83993ec7
commit 03342cf83f
9 changed files with 92 additions and 115 deletions
@@ -166,20 +166,7 @@ export class FFormula extends FBase {
}
/**
* Wait for computing in the Univer instance to complete. Please note that this does not only include formula calculation,
* but also other computing tasks, e.g. pivot table calculation.
* @param {number} [timeout] The maximum time to wait for the computing to complete, in milliseconds. The default
* value is 30,000 milliseconds.
* @returns {Promise<boolean>} This method returns `true` if the computing is complete. If the timeout is reached, this
* method returns `false`.
*
* @example
* ```ts
* const formulaEngine = univerAPI.getFormula();
* formulaEngine.whenComputingCompleteAsync(3000).then((isComplete) => {
* console.log('Computing complete:', isComplete);
* });
* ```
* @deprecated Use `onCalculationEnd` instead.
*/
whenComputingCompleteAsync(timeout?: number): Promise<boolean> {
const gcss = this._injector.get(GlobalComputingStatusService);
@@ -192,7 +179,7 @@ export class FFormula extends FBase {
}
/**
* @deprecated Use `whenComputingCompleteAsync` instead.
* Waits for the formula calculation to complete.
* @returns {Promise<void>} This method returns a promise that resolves when the calculation is complete.
*/
onCalculationEnd(): Promise<void> {
@@ -200,20 +200,6 @@ describe('Test formula data model', () => {
};
const result = {
1: {
3: {
f: '=SUM(A2)',
si: 'OSPtzm',
},
},
2: {
3: {
f: '=SUM(A2)',
si: 'OSPtzm',
x: 0,
y: 1,
},
},
3: {
3: null,
},
@@ -29,12 +29,14 @@ export function updateFormulaDataByCellValue(sheetFormulaDataMatrix: ObjectMatri
const currentFormulaInfo = sheetFormulaDataMatrix.getValue(r, c);
const f = currentFormulaInfo?.f || '';
const si = currentFormulaInfo?.si || '';
const x = currentFormulaInfo?.x || 0;
const y = currentFormulaInfo?.y || 0;
// Any data update may destroy the original correspondence between f and si, and the relationship between f and si needs to be re-bound.
function clearFormulaData() {
// The id that needs to be offset
// When the cell containing the formulas f and si is deleted, f and si lose their association, and f needs to be moved to the next cell containing the same si.
if (isFormulaString(f) && isFormulaId(si)) {
if (isFormulaString(f) && isFormulaId(si) && x === 0 && y === 0) {
const updatedFormula = formulaIdMap?.[si]?.f;
// The formula may have been updated. For example, when you delete a column referenced by a formula, it will become #REF and cannot take the original value.
@@ -639,19 +639,19 @@ describe('Test update formula ', () => {
expect(values).toStrictEqual([
[null],
[null],
[{ f: '=A1', t: 2, v: 1 }],
[{ f: '=A2', t: 2, v: 2 }],
[{ f: '=A3', t: 2, v: 3 }],
[{ f: '=A4', t: 2, v: 4 }],
[{ f: '=A1' }],
[{ f: '=A2' }],
[{ f: '=A3' }],
[{ f: '=A4' }],
]);
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
const valuesUndo = getValues(0, 1, 5, 1, sheetId);
expect(valuesUndo).toStrictEqual([
[{ f: '=A1', t: 2, v: 1 }],
[{ f: '=A2', si: 'W8Hdfc', t: 2, v: 2 }],
[{ si: 'W8Hdfc', t: 2, v: 3 }],
[{ si: 'W8Hdfc', t: 2, v: 4 }],
[{ f: '=A1' }],
[{ f: '=A2', si: 'W8Hdfc' }],
[{ si: 'W8Hdfc' }],
[{ si: 'W8Hdfc' }],
[null],
[null],
]);
@@ -661,10 +661,10 @@ describe('Test update formula ', () => {
expect(valuesRedo).toStrictEqual([
[null],
[null],
[{ f: '=A1', t: 2, v: 1 }],
[{ f: '=A2', t: 2, v: 2 }],
[{ f: '=A3', t: 2, v: 3 }],
[{ f: '=A4', t: 2, v: 4 }],
[{ f: '=A1' }],
[{ f: '=A2' }],
[{ f: '=A3' }],
[{ f: '=A4' }],
]);
});
@@ -696,12 +696,12 @@ describe('Test update formula ', () => {
expect(await commandService.executeCommand(MoveRangeCommand.id, params)).toBeTruthy();
const values1 = getValues(1, 3, 1, 3, sheetId);
expect(values1).toStrictEqual([[{ f: '=A2', t: 2, v: 2 }]]);
expect(values1).toStrictEqual([[{ f: '=A2' }]]);
const values2 = getValues(1, 1, 3, 1, sheetId);
expect(values2).toStrictEqual([
[null],
[{ f: '=A3', t: 2, v: 3 }],
[{ f: '=A4', t: 2, v: 4 }],
[{ f: '=A3' }],
[{ f: '=A4' }],
]);
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
@@ -709,19 +709,19 @@ describe('Test update formula ', () => {
expect(valuesUndo1).toStrictEqual([[null]]);
const valuesUndo2 = getValues(1, 1, 3, 1, sheetId);
expect(valuesUndo2).toStrictEqual([
[{ f: '=A2', si: 'W8Hdfc', t: 2, v: 2 }],
[{ si: 'W8Hdfc', t: 2, v: 3 }],
[{ si: 'W8Hdfc', t: 2, v: 4 }],
[{ f: '=A2', si: 'W8Hdfc' }],
[{ si: 'W8Hdfc' }],
[{ si: 'W8Hdfc' }],
]);
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
const valuesRedo1 = getValues(1, 3, 1, 3, sheetId);
expect(valuesRedo1).toStrictEqual([[{ f: '=A2', t: 2, v: 2 }]]);
expect(valuesRedo1).toStrictEqual([[{ f: '=A2' }]]);
const valuesRedo2 = getValues(1, 1, 3, 1, sheetId);
expect(valuesRedo2).toStrictEqual([
[null],
[{ f: '=A3', t: 2, v: 3 }],
[{ f: '=A4', t: 2, v: 4 }],
[{ f: '=A3' }],
[{ f: '=A4' }],
]);
});
@@ -753,7 +753,7 @@ describe('Test update formula ', () => {
expect(await commandService.executeCommand(MoveRangeCommand.id, params)).toBeTruthy();
const values1 = getValues(2, 3, 2, 3, sheetId);
expect(values1).toStrictEqual([[{ f: '=A3', t: 2, v: 3 }]]);
expect(values1).toStrictEqual([[{ f: '=A3' }]]);
const values2 = getValues(1, 1, 3, 1, sheetId);
expect(values2).toStrictEqual([
[{ f: '=A2', si: 'W8Hdfc', t: 2, v: 2 }],
@@ -767,13 +767,13 @@ describe('Test update formula ', () => {
const valuesUndo2 = getValues(1, 1, 3, 1, sheetId);
expect(valuesUndo2).toStrictEqual([
[{ f: '=A2', si: 'W8Hdfc', t: 2, v: 2 }],
[{ si: 'W8Hdfc', t: 2, v: 3 }],
[{ si: 'W8Hdfc' }],
[{ si: 'W8Hdfc', t: 2, v: 4 }],
]);
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
const valuesRedo1 = getValues(2, 3, 2, 3, sheetId);
expect(valuesRedo1).toStrictEqual([[{ f: '=A3', t: 2, v: 3 }]]);
expect(valuesRedo1).toStrictEqual([[{ f: '=A3' }]]);
const valuesRedo2 = getValues(1, 1, 3, 1, sheetId);
expect(valuesRedo2).toStrictEqual([
[{ f: '=A2', si: 'W8Hdfc', t: 2, v: 2 }],
@@ -810,30 +810,30 @@ describe('Test update formula ', () => {
expect(await commandService.executeCommand(MoveRangeCommand.id, params)).toBeTruthy();
const values1 = getValues(1, 3, 2, 3, sheetId);
expect(values1).toStrictEqual([[{ f: '=A2', t: 2, v: 2 }], [{ f: '=A3', t: 2, v: 3 }]]);
expect(values1).toStrictEqual([[{ f: '=A2' }], [{ f: '=A3' }]]);
const values2 = getValues(1, 1, 3, 1, sheetId);
expect(values2).toStrictEqual([
[null],
[null],
[{ f: '=A4', t: 2, v: 4 }],
[{ f: '=A4' }],
]);
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
const valuesUndo = getValues(1, 1, 3, 1, sheetId);
expect(valuesUndo).toStrictEqual([
[{ f: '=A2', si: 'W8Hdfc', t: 2, v: 2 }],
[{ si: 'W8Hdfc', t: 2, v: 3 }],
[{ si: 'W8Hdfc', t: 2, v: 4 }],
[{ f: '=A2', si: 'W8Hdfc' }],
[{ si: 'W8Hdfc' }],
[{ si: 'W8Hdfc' }],
]);
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
const valuesRedo1 = getValues(1, 3, 2, 3, sheetId);
expect(valuesRedo1).toStrictEqual([[{ f: '=A2', t: 2, v: 2 }], [{ f: '=A3', t: 2, v: 3 }]]);
expect(valuesRedo1).toStrictEqual([[{ f: '=A2' }], [{ f: '=A3' }]]);
const valuesRedo2 = getValues(1, 1, 3, 1, sheetId);
expect(valuesRedo2).toStrictEqual([
[null],
[null],
[{ f: '=A4', t: 2, v: 4 }],
[{ f: '=A4' }],
]);
});
@@ -858,25 +858,25 @@ describe('Test update formula ', () => {
expect(await commandService.executeCommand(MoveRangeCommand.id, params)).toBeTruthy();
const values = getValues(18, 1, 20, 2);
expect(values).toStrictEqual([
[null, { f: '=SUM(A19)', t: 2, v: 1 }],
[null, { f: '=SUM(A20)', t: 2, v: 2 }],
[null, { f: '=SUM(A21)', t: 2, v: 3 }],
[null, { f: '=SUM(A19)' }],
[null, { f: '=SUM(A20)' }],
[null, { f: '=SUM(A21)' }],
]);
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
const valuesUndo = getValues(18, 1, 20, 2);
expect(valuesUndo).toStrictEqual([
[{ f: '=SUM(A19)', t: 2, v: 1 }, null],
[{ f: '=SUM(A20)', si: 'id1', t: 2, v: 2 }, null],
[{ si: 'id1', t: 2, v: 3 }, null],
[{ f: '=SUM(A19)' }, null],
[{ f: '=SUM(A20)', si: 'id1' }, null],
[{ si: 'id1' }, null],
]);
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
const valuesRedo = getValues(18, 1, 20, 2);
expect(valuesRedo).toStrictEqual([
[null, { f: '=SUM(A19)', t: 2, v: 1 }],
[null, { f: '=SUM(A20)', t: 2, v: 2 }],
[null, { f: '=SUM(A21)', t: 2, v: 3 }],
[null, { f: '=SUM(A19)' }],
[null, { f: '=SUM(A20)' }],
[null, { f: '=SUM(A21)' }],
]);
});
@@ -1586,52 +1586,21 @@ describe('Test update formula ', () => {
const values = getValues(21, 0, 21, 4);
// Ignore the calculation results and only verify the offset information
expect(values).toStrictEqual([[{
f: '=OFFSET(#REF!,1,1)',
v: 0,
t: 2,
}, {
f: '=OFFSET(#REF!,1,1)',
v: 1,
t: 2,
}, {
f: '=OFFSET(A1,1,1)',
v: 1,
t: 2,
}, null, null]]);
expect(values).toStrictEqual([
[{ f: '=OFFSET(#REF!,1,1)' }, { f: '=OFFSET(#REF!,1,1)' }, { f: '=OFFSET(A1,1,1)' }, null, null],
]);
expect(await commandService.executeCommand(UndoCommand.id)).toBeTruthy();
const valuesUndo = getValues(21, 0, 21, 4);
expect(valuesUndo).toStrictEqual([[null, null, {
f: '=OFFSET(A1,1,1)',
v: 0,
t: 2,
}, {
f: '=OFFSET(B1,1,1)',
si: 'id2',
v: 1,
t: 2,
}, {
si: 'id2',
v: 1,
t: 2,
}]]);
expect(valuesUndo).toStrictEqual([
[null, null, { f: '=OFFSET(A1,1,1)' }, { f: '=OFFSET(B1,1,1)', si: 'id2' }, { si: 'id2' }],
]);
expect(await commandService.executeCommand(RedoCommand.id)).toBeTruthy();
const valuesRedo = getValues(21, 0, 21, 4);
expect(valuesRedo).toStrictEqual([[{
f: '=OFFSET(#REF!,1,1)',
v: 0,
t: 2,
}, {
f: '=OFFSET(#REF!,1,1)',
v: 1,
t: 2,
}, {
f: '=OFFSET(A1,1,1)',
v: 1,
t: 2,
}, null, null]]);
expect(valuesRedo).toStrictEqual([
[{ f: '=OFFSET(#REF!,1,1)' }, { f: '=OFFSET(#REF!,1,1)' }, { f: '=OFFSET(A1,1,1)' }, null, null],
]);
});
it('Remove column, removed column contains formula', async () => {
@@ -158,7 +158,7 @@ export class UpdateFormulaController extends Disposable {
{
unitId,
subUnitId: sheetId,
cellValue: formulaDataToCellData(newSheetFormulaData),
cellValue: formulaDataToCellData(newSheetFormulaData, cellValue),
},
{
onlyLocal: true,
@@ -589,13 +589,18 @@ function getUndoFormulaData(rangeList: IRangeChange[], oldFormulaMatrix: ObjectM
* └──────────────────┴─────┴───┴───┴───────────┴─────┘
*/
export function formulaDataItemToCellData(formulaDataItem: Nullable<IFormulaDataItem>): Nullable<ICellData> {
if (formulaDataItem == null) {
if (formulaDataItem === undefined) {
return;
}
if (formulaDataItem === null) {
// null presents clearing cell content
return {
f: null,
si: null,
};
}
const { f, si, x = 0, y = 0 } = formulaDataItem;
const checkFormulaString = isFormulaString(f);
const checkFormulaId = isFormulaId(si);
@@ -633,7 +638,7 @@ export function formulaDataItemToCellData(formulaDataItem: Nullable<IFormulaData
* @param formulaData
* @returns
*/
export function formulaDataToCellData(formulaData: IObjectMatrixPrimitiveType<IFormulaDataItem | null>): IObjectMatrixPrimitiveType<Nullable<ICellData>> {
export function formulaDataToCellData(formulaData: IObjectMatrixPrimitiveType<IFormulaDataItem | null>, changedCellValue?: IObjectMatrixPrimitiveType<Nullable<ICellData>>): IObjectMatrixPrimitiveType<Nullable<ICellData>> {
const cellData = new ObjectMatrix<Nullable<ICellData>>({});
const formulaDataMatrix = new ObjectMatrix(formulaData);
@@ -646,6 +651,14 @@ export function formulaDataToCellData(formulaData: IObjectMatrixPrimitiveType<IF
return;
}
/**
* If the cell value has been changed and contains a formula, clear the current cell value and type to avoid the formula calculation could not be recalculated after it was interrupted in certain situations.
*/
if (changedCellValue && changedCellValue[r]?.[c] && (cellDataItem?.f || cellDataItem?.si)) {
cellDataItem.v = null;
cellDataItem.t = null;
}
cellData.setValue(r, c, cellDataItem);
});
@@ -70,6 +70,9 @@ export async function expectMoveFormulaRowsResultMatchesSnapshot() {
const rowSpec = worksheet.getRange('3:3');
worksheet.moveRows(rowSpec, 4);
await testBed.api.getFormula().onCalculationResultApplied();
await new Promise((resolve) => setTimeout(resolve, 500));
const resultSnapshot = workbook.save();
const snapshotFilePath = path.resolve(snapshotRootDir, `${getTestFilePath()}-result.json`);
if (fs.existsSync(snapshotFilePath)) {
@@ -84,6 +87,8 @@ export async function expectMoveFormulaRowsResultMatchesSnapshot() {
// perform undo operation
await testBed.api.undo();
await testBed.api.getFormula().onCalculationResultApplied();
await new Promise((resolve) => setTimeout(resolve, 500));
// compare the result with the snapshot
const resultSnapshot_undo = workbook.save();
@@ -120,6 +125,9 @@ export async function expectMoveFormulaSiRowsResultMatchesSnapshot() {
const rowSpec = worksheet.getRange('3:3');
worksheet.moveRows(rowSpec, 4);
await testBed.api.getFormula().onCalculationResultApplied();
await new Promise((resolve) => setTimeout(resolve, 500));
const resultSnapshot = workbook.save();
const snapshotFilePath = path.resolve(snapshotRootDir, `${getTestFilePath()}-result.json`);
if (fs.existsSync(snapshotFilePath)) {
@@ -134,6 +142,8 @@ export async function expectMoveFormulaSiRowsResultMatchesSnapshot() {
// perform undo operation
await testBed.api.undo();
await testBed.api.getFormula().onCalculationResultApplied();
await new Promise((resolve) => setTimeout(resolve, 500));
// compare the result with the snapshot
const resultSnapshot_undo = workbook.save();
@@ -174,6 +184,9 @@ export async function expectMoveFormulaCellResultMatchesSnapshot() {
toRange,
});
await testBed.api.getFormula().onCalculationResultApplied();
await new Promise((resolve) => setTimeout(resolve, 500));
const resultSnapshot = workbook.save();
const snapshotFilePath = path.resolve(snapshotRootDir, `${getTestFilePath()}-result.json`);
if (fs.existsSync(snapshotFilePath)) {
@@ -188,6 +201,8 @@ export async function expectMoveFormulaCellResultMatchesSnapshot() {
// perform undo operation
await testBed.api.undo();
await testBed.api.getFormula().onCalculationResultApplied();
await new Promise((resolve) => setTimeout(resolve, 500));
// compare the result with the snapshot
const resultSnapshot_undo = workbook.save();
@@ -69,6 +69,9 @@ export async function expectRemoveRowsOfFilterRowsResultMatchesSnapshot() {
// remove rows 2 to 5, where the 3 to 4 rows are filtered rows
worksheet.deleteRows(1, 4);
await testBed.api.getFormula().onCalculationResultApplied();
await new Promise((resolve) => setTimeout(resolve, 500));
const resultSnapshot = workbook.save();
const snapshotFilePath = path.resolve(snapshotRootDir, `${getTestFilePath()}-result.json`);
if (fs.existsSync(snapshotFilePath)) {
@@ -83,6 +86,8 @@ export async function expectRemoveRowsOfFilterRowsResultMatchesSnapshot() {
// perform undo operation
await testBed.api.undo();
await testBed.api.getFormula().onCalculationResultApplied();
await new Promise((resolve) => setTimeout(resolve, 500));
// compare the result with the snapshot
const resultSnapshot_undo = workbook.save();
@@ -47,8 +47,8 @@ export async function expectCalculationResultMatchesSnapshot() {
const testSnapshot = JSON.parse(testSnapshotRaw) as IWorkbookData;
const workbook = testBed.api.createWorkbook(testSnapshot);
const formula = testBed.api.getFormula();
await formula.onCalculationEnd();
await testBed.api.getFormula().onCalculationResultApplied();
const resultSnapshot = workbook.save();
const snapshotFilePath = path.resolve(snapshotRootDir, `${getTestFilePath()}-result.json`);