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
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ export interface IFormulaDependencyGenerator {
generate(): Promise<IFormulaDependencyTree[]>;
}

export type { IFormulaDependencyTree };

export const IFormulaDependencyGenerator = createIdentifier<IFormulaDependencyGenerator>('engine-formula.dependency-generator');

export class FormulaDependencyGenerator extends Disposable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@
IRuntimeUnitDataType,
IUnitExcludedCell,
} from '../basics/common';

import type { IUniverEngineFormulaConfig } from '../controller/config.schema';

import type { LexerNode } from '../engine/analysis/lexer-node';
import type { IFormulaDependencyTree } from '../engine/dependency/formula-dependency';
import type { FunctionVariantType } from '../engine/reference-object/base-reference-object';
import type { IAllRuntimeData, IExecutionInProgressParams } from './runtime.service';
import {
Expand Down Expand Up @@ -64,6 +65,7 @@
setRuntimeFeatureCellData(featureId: string, featureData: IRuntimeUnitDataType): void;
setRuntimeFeatureRange(featureId: string, featureRange: IFeatureDirtyRangeType): void;
execute(formulaDatasetConfig: IFormulaDatasetConfig): Promise<void>;
generateDependencyTrees(formulaDatasetConfig: IFormulaDatasetConfig): Promise<IFormulaDependencyTree[]>;
stopFormulaExecution(): void;
calculate(formulaString: string, transformSuffix?: boolean): void;
}
Expand Down Expand Up @@ -243,126 +245,157 @@
}

// eslint-disable-next-line max-lines-per-function
protected async _apply(isArrayFormulaState = false) {
if (isArrayFormulaState) {
this._runtimeService.setFormulaExecuteStage(FormulaExecuteStageType.START_DEPENDENCY_ARRAY_FORMULA);
} else {
this._runtimeService.setFormulaExecuteStage(FormulaExecuteStageType.START_DEPENDENCY);
}

this._executionInProgressListener$.next(this._runtimeService.getRuntimeState());

const treeList = (await this._formulaDependencyGenerator.generate()).reverse();

const interpreter = this._interpreter;

if (isArrayFormulaState) {
this._runtimeService.setFormulaExecuteStage(FormulaExecuteStageType.START_CALCULATION_ARRAY_FORMULA);

this._runtimeService.setTotalArrayFormulasToCalculate(treeList.length);
} else {
this._runtimeService.setFormulaExecuteStage(FormulaExecuteStageType.START_CALCULATION);

this._runtimeService.setTotalFormulasToCalculate(treeList.length);
}

this._executionInProgressListener$.next(this._runtimeService.getRuntimeState());

let pendingTasks: (() => void)[] = [];

const config = this._configService.getConfig(ENGINE_FORMULA_PLUGIN_CONFIG_KEY) as IUniverEngineFormulaConfig;
const intervalCount = config?.intervalCount || DEFAULT_INTERVAL_COUNT;

const treeCount = treeList.length;
for (let i = 0; i < treeCount; i++) {
const tree = treeList[i];
const nodeData = tree.nodeData;
const getDirtyData = tree.getDirtyData;

// Execute the await every 100 iterations
if (i % intervalCount === 0) {
/**
* For every functions, execute a setTimeout to wait for external command input.
*/
await new Promise((resolve) => {
const calCancelTask = requestImmediateMacroTask(resolve);
pendingTasks.push(calCancelTask);
});

if (isArrayFormulaState) {
this._runtimeService.setFormulaExecuteStage(
FormulaExecuteStageType.CURRENTLY_CALCULATING_ARRAY_FORMULA
);

this._runtimeService.setCompletedArrayFormulasCount(i + 1);
} else {
this._runtimeService.setFormulaExecuteStage(FormulaExecuteStageType.CURRENTLY_CALCULATING);

this._runtimeService.setCompletedFormulasCount(i + 1);
}

this._executionInProgressListener$.next(this._runtimeService.getRuntimeState());

if (this._runtimeService.isStopExecution() || (nodeData == null && getDirtyData == null)) {
this._runtimeService.setFormulaExecuteStage(FormulaExecuteStageType.IDLE);
this._runtimeService.markedAsStopFunctionsExecuted();
this._executionCompleteListener$.next(this._runtimeService.getAllRuntimeData());
return;
}
}

this._runtimeService.setCurrent(
tree.row,
tree.column,
tree.rowCount,
tree.columnCount,
tree.subUnitId,
tree.unitId
);

let value: FunctionVariantType;

if (getDirtyData != null && tree.featureId != null) {
/**
* Execute the dependencies registered by the feature,
* and return the dirty area marked by the feature,
* so as to allow the formulas depending on the dirty area to continue the calculation.
*/
const { runtimeCellData, dirtyRanges } = getDirtyData(this._currentConfigService.getDirtyData(), this._runtimeService.getAllRuntimeData());

this._runtimeService.setRuntimeFeatureCellData(tree.featureId, runtimeCellData);

this._runtimeService.setRuntimeFeatureRange(tree.featureId, dirtyRanges);
} else if (nodeData != null) {
if (interpreter.checkAsyncNode(nodeData.node)) {
value = await interpreter.executeAsync(nodeData);
} else {
value = interpreter.execute(nodeData);
}

if (tree.formulaId != null) {
this._runtimeService.setRuntimeOtherData(tree.formulaId, tree.refOffsetX, tree.refOffsetY, value);
} else {
this._runtimeService.setRuntimeData(value);
}
}
}

// clear all pending tasks
pendingTasks.forEach((cancel) => cancel());
pendingTasks = [];

if (treeCount > 0) {
this._runtimeService.markedAsSuccessfullyExecuted();
} else if (!isArrayFormulaState) {
this._runtimeService.markedAsNoFunctionsExecuted();
}

// treeList.length = 0;

return this._runtimeService.getAllRuntimeData();
}

Check notice on line 366 in packages/engine-formula/src/services/calculate-formula.service.ts

View check run for this annotation

codefactor.io / CodeFactor

packages/engine-formula/src/services/calculate-formula.service.ts#L248-L366

Complex Method

async generateDependencyTrees(formulaDatasetConfig: IFormulaDatasetConfig): Promise<IFormulaDependencyTree[]> {
this._currentConfigService.load(formulaDatasetConfig);

this._runtimeService.reset();

const normalTrees = await this._generateTrees(false);

const executeState = this._runtimeService.getAllRuntimeData();

const { dirtyRanges, excludedCell } = this._getArrayFormulaDirtyRangeAndExcludedRange(
executeState.arrayFormulaRange,
executeState.runtimeFeatureRange
);

let arrayTrees: IFormulaDependencyTree[] = [];

if (dirtyRanges && dirtyRanges.length > 0) {
this._currentConfigService.loadDirtyRangesAndExcludedCell(dirtyRanges, excludedCell);

arrayTrees = await this._generateTrees(true);
}

return [...normalTrees, ...arrayTrees];
}

private async _generateTrees(isArrayFormulaState: boolean): Promise<IFormulaDependencyTree[]> {
const treeList = (await this._formulaDependencyGenerator.generate()).reverse();

return treeList;
}

calculate(formulaString: string, transformSuffix: boolean = true) {
// TODO how to observe @alex
// this.getObserver('onBeforeFormulaCalculateObservable')?.notifyObservers(formulaString);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,10 @@ import enUS from '@univerjs/sheets/locale/en-US';
import zhCN from '@univerjs/sheets/locale/zh-CN';

import '@univerjs/sheets/facade';
import '@univerjs/engine-formula/facade';
import '../f-workbook';
import '../f-range';
import '../f-formula';

function getTestWorkbookDataDemo(): IWorkbookData {
return {
Expand Down
49 changes: 49 additions & 0 deletions packages/sheets-formula/src/facade/__tests__/f-formula.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import type { Mocked } from 'vitest';

import type { IFormulaCellAndFeatureItem } from '../../services/remote/remote-formula-dependency-generator.service';
import { describe, expect, it, vi } from 'vitest';
import { IRemoteFormulaDependencyGenerator } from '../../services/remote/remote-formula-dependency-generator.service';
import { createFacadeTestBed } from './create-test-bed';

describe('FFormula', () => {
describe('getFormulaCellsAndFeatures', () => {
it('should return formula cells and features from dependency tree', async () => {
const testBed = createFacadeTestBed();
const mockDependencyGenerator: Mocked<IRemoteFormulaDependencyGenerator> = {
generate: vi.fn(),
};

testBed.injector.add([IRemoteFormulaDependencyGenerator, { useValue: mockDependencyGenerator }]);

const mockTrees: Array<IFormulaCellAndFeatureItem> = [
{ unitId: 'unit1', subUnitId: 'sheet1', row: 0, column: 0 },
{ unitId: 'unit1', subUnitId: 'sheet1', featureId: 'feature1' },
];
mockDependencyGenerator.generate.mockResolvedValue(mockTrees);

const fFormula = testBed.univerAPI.getFormula();
const result = await fFormula.getFormulaCellsAndFeatures();

expect(result).toEqual([
{ unitId: 'unit1', subUnitId: 'sheet1', row: 0, column: 0 },
{ unitId: 'unit1', subUnitId: 'sheet1', featureId: 'feature1' },
]);
});
});
});
25 changes: 22 additions & 3 deletions packages/sheets-formula/src/facade/f-formula.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@
* limitations under the License.
*/

import type { IDisposable, ILocales } from '@univerjs/core';
import type { IDisposable, ILocales, IUnitRange } from '@univerjs/core';

import type { IFunctionInfo } from '@univerjs/engine-formula';
import type { CalculationMode, IRegisterAsyncFunction, IRegisterFunction, ISingleFunctionRegisterParams, IUniverSheetsFormulaBaseConfig } from '@univerjs/sheets-formula';
import type { CalculationMode, IFormulaCellAndFeatureItem, IRegisterAsyncFunction, IRegisterFunction, ISingleFunctionRegisterParams, IUniverSheetsFormulaBaseConfig } from '@univerjs/sheets-formula';
import { debounce, IConfigService, ILogService, LifecycleService, LifecycleStages } from '@univerjs/core';
import { SetFormulaCalculationStartMutation } from '@univerjs/engine-formula';
import { FFormula } from '@univerjs/engine-formula/facade';
import { IRegisterFunctionService, PLUGIN_CONFIG_KEY_BASE, RegisterFunctionService } from '@univerjs/sheets-formula';
import { IRegisterFunctionService, IRemoteFormulaDependencyGenerator, PLUGIN_CONFIG_KEY_BASE, RegisterFunctionService } from '@univerjs/sheets-formula';

/**
* @ignore
Expand All @@ -39,6 +39,20 @@ export interface IFFormulaSheetsMixin {
*/
setInitialFormulaComputing(calculationMode: CalculationMode): void;

/**
* Get the list of formula cells and feature IDs from the dependency tree.
* @param {IUnitRange} [range] - Optional range to filter the results.
* @returns {Promise<Array<IFormulaCellAndFeatureItem>>} An array of objects containing unitId, subUnitId, and either cell coordinates or featureId.
*
* @example
* ```ts
* const formulaEngine = univerAPI.getFormula();
* const cellsAndFeatures = await formulaEngine.getFormulaCellsAndFeatures();
* console.log(cellsAndFeatures);
* ```
*/
getFormulaCellsAndFeatures(range?: IUnitRange): Promise<Array<IFormulaCellAndFeatureItem>>;

/**
* Register a custom synchronous formula function.
* @param {string} name - The name of the function to register. This will be used in formulas (e.g., =MYFUNC()).
Expand Down Expand Up @@ -319,6 +333,11 @@ export class FFormulaSheetsMixin extends FFormula implements IFFormulaSheetsMixi
config.initialFormulaComputing = calculationMode;
}

override async getFormulaCellsAndFeatures(range?: IUnitRange): Promise<Array<IFormulaCellAndFeatureItem>> {
const dependencyGenerator = this._injector.get(IRemoteFormulaDependencyGenerator);
return dependencyGenerator.generate(range);
}

override registerFunction(name: string, func: IRegisterFunction): IDisposable;
override registerFunction(name: string, func: IRegisterFunction, description: string): IDisposable;
override registerFunction(
Expand Down
1 change: 1 addition & 0 deletions packages/sheets-formula/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@ export type { IRegisterFunctionParams, IUnregisterFunctionParams } from './servi
export { RegisterFunctionService } from './services/register-function.service';
export { IRegisterFunctionService } from './services/register-function.service';
export { RegisterOtherFormulaService } from './services/register-other-formula.service';
export { type IFormulaCellAndFeatureItem, IRemoteFormulaDependencyGenerator, RemoteFormulaDependencyGeneratorService } from './services/remote/remote-formula-dependency-generator.service';
export { IRemoteRegisterFunctionService, RemoteRegisterFunctionService } from './services/remote/remote-register-function.service';
export { calculateFormula } from './util/calculate';
11 changes: 11 additions & 0 deletions packages/sheets-formula/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { DescriptionService, IDescriptionService } from './services/description.
import { FormulaRefRangeService } from './services/formula-ref-range.service';
import { IRegisterFunctionService, RegisterFunctionService } from './services/register-function.service';
import { RegisterOtherFormulaService } from './services/register-other-formula.service';
import { IRemoteFormulaDependencyGenerator, RemoteFormulaDependencyGeneratorService, RemoteFormulaDependencyGeneratorServiceName } from './services/remote/remote-formula-dependency-generator.service';
import { IRemoteRegisterFunctionService, RemoteRegisterFunctionService, RemoteRegisterFunctionServiceName } from './services/remote/remote-register-function.service';

@DependentOn(UniverFormulaEnginePlugin)
Expand Down Expand Up @@ -67,6 +68,12 @@ export class UniverRemoteSheetsFormulaPlugin extends Plugin {
RemoteRegisterFunctionServiceName,
fromModule(this._injector.get(RemoteRegisterFunctionService))
);

this._injector.add([RemoteFormulaDependencyGeneratorService]);
this._injector.get(IRPCChannelService).registerChannel(
RemoteFormulaDependencyGeneratorServiceName,
fromModule(this._injector.get(RemoteFormulaDependencyGeneratorService))
);
}
}

Expand Down Expand Up @@ -114,6 +121,10 @@ export class UniverSheetsFormulaPlugin extends Plugin {
dependencies.push([IRemoteRegisterFunctionService, {
useFactory: () => toModule<IRemoteRegisterFunctionService>(rpcChannelService.requestChannel(RemoteRegisterFunctionServiceName)),
}]);

dependencies.push([IRemoteFormulaDependencyGenerator, {
useFactory: () => toModule<IRemoteFormulaDependencyGenerator>(rpcChannelService.requestChannel(RemoteFormulaDependencyGeneratorServiceName)),
}]);
}

dependencies.forEach((dependency) => j.add(dependency));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* Copyright 2023-present DreamNum Co., Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import type { IUnitRange } from '@univerjs/core';
import { createIdentifier, Inject, isFormulaId, isFormulaString } from '@univerjs/core';
import { FormulaDataModel, ICalculateFormulaService, IFormulaCurrentConfigService } from '@univerjs/engine-formula';

export interface IFormulaCellAndFeatureItem {
unitId: string;
subUnitId: string;
row?: number;
column?: number;
featureId?: string;
formula?: string;
formulaId?: string;
}

export interface IRemoteFormulaDependencyGenerator {
generate(range?: IUnitRange): Promise<Array<IFormulaCellAndFeatureItem>>;
}

export const RemoteFormulaDependencyGeneratorServiceName = 'sheets-formula.remote-formula-dependency-generator.service';
export const IRemoteFormulaDependencyGenerator = createIdentifier<IRemoteFormulaDependencyGenerator>(RemoteFormulaDependencyGeneratorServiceName);

/**
* This class should resident in the main process.
*/
export class RemoteFormulaDependencyGeneratorService implements IRemoteFormulaDependencyGenerator {
constructor(
@ICalculateFormulaService private readonly _calculateFormulaService: ICalculateFormulaService,
@Inject(FormulaDataModel) private readonly _formulaDataModel: FormulaDataModel,
@IFormulaCurrentConfigService private readonly _currentConfigService: IFormulaCurrentConfigService
) {}

async generate(range?: IUnitRange): Promise<Array<IFormulaCellAndFeatureItem>> {
const formulaData = this._formulaDataModel.getFormulaData();
const arrayFormulaCellData = this._formulaDataModel.getArrayFormulaCellData();
const arrayFormulaRange = this._formulaDataModel.getArrayFormulaRange();

const formulaDatasetConfig = {
formulaData,
arrayFormulaCellData,
arrayFormulaRange,
forceCalculate: false,
dirtyRanges: range ? [range] : [],
dirtyNameMap: {},
dirtyDefinedNameMap: {},
dirtyUnitFeatureMap: {},
dirtyUnitOtherFormulaMap: {},
clearDependencyTreeCache: {},
maxIteration: 1,
rowData: undefined,
};

const trees = await this._calculateFormulaService.generateDependencyTrees(formulaDatasetConfig);

const result: Array<IFormulaCellAndFeatureItem> = [];
for (let i = 0; i < trees.length; i++) {
const tree = trees[i];
if ((isFormulaString(tree.formula) || isFormulaId(tree.formulaId)) || tree.featureId != null) {
result.push({
unitId: tree.unitId,
subUnitId: tree.subUnitId,
row: tree.row,
column: tree.column,
featureId: tree.featureId || undefined,
formula: tree.formula || undefined,
formulaId: tree.formulaId || undefined,
});
}
}
return result;
}
}