feat(formula): dependency tree and calculation api (#6258)

This commit is contained in:
Univer
2025-12-08 12:04:56 +08:00
committed by GitHub
parent 300e60ba7e
commit e67908fc29
10 changed files with 764 additions and 36 deletions
@@ -146,6 +146,19 @@ export interface IFormulaIdMap {
c: number;
}
export interface IFormulaStringMap {
[unitId: string]: Nullable<{ [sheetId: string]: IObjectMatrixPrimitiveType<string[]> }>;
}
export interface IFormulaExecuteResultItem {
value: Nullable<number | string | boolean | Array<Array<number | string | boolean | null>>>;
formula: string;
}
export interface IFormulaExecuteResultMap {
[unitId: string]: Nullable<{ [sheetId: string]: IObjectMatrixPrimitiveType<IFormulaExecuteResultItem[]> }>;
}
export interface IFormulaIdMapData {
[unitId: string]: Nullable<{ [subUnitId: string]: Nullable<{ [formulaId: string]: IFormulaIdMap }> }>;
}
@@ -16,10 +16,13 @@
import type { IExecutionOptions, IMutation, Nullable } from '@univerjs/core';
import type {
IFormulaExecuteResultMap,
IFormulaStringMap,
IRuntimeOtherUnitDataType,
IRuntimeUnitDataPrimitiveType,
} from '../../basics/common';
import type { IFormulaDependencyTreeFullJson, IFormulaDependencyTreeJson } from '../../engine/dependency/dependency-tree';
import type { IFormulaDirtyData } from '../../services/current-data.service';
import type { FormulaExecutedStateType, IExecutionInProgressParams } from '../../services/runtime.service';
import { CommandType } from '@univerjs/core';
@@ -27,6 +30,34 @@ import { CommandType } from '@univerjs/core';
export interface ISetFormulaCalculationStartMutation extends IFormulaDirtyData {
options: Nullable<IExecutionOptions>;
}
export interface ISetFormulaStringBatchCalculationMutation {
formulas: IFormulaStringMap;
}
export interface ISetFormulaStringBatchCalculationResultMutation {
result: IFormulaExecuteResultMap;
}
export interface ISetFormulaStringBatchCalculationResultMutation {
result: IFormulaExecuteResultMap;
}
export interface ISetFormulaDependencyCalculationMutation {
unitId: string;
sheetId: string;
row: number;
column: number;
}
export interface ISetFormulaDependencyCalculationResultMutation {
result: IFormulaDependencyTreeJson[];
}
export interface ISetCellFormulaDependencyCalculationResultMutation {
result: IFormulaDependencyTreeFullJson | undefined;
}
/**
* TODO: @DR-Univer
* Trigger the calculation of the formula and stop the formula
@@ -37,7 +68,19 @@ export const SetFormulaCalculationStartMutation: IMutation<ISetFormulaCalculatio
handler: () => true,
};
export interface ISetFormulaCalculationStopMutation {}
export const SetFormulaStringBatchCalculationMutation: IMutation<ISetFormulaStringBatchCalculationMutation> = {
id: 'formula.mutation.set-formula-string-batch-calculation',
type: CommandType.MUTATION,
handler: () => true,
};
export const SetFormulaStringBatchCalculationResultMutation: IMutation<ISetFormulaStringBatchCalculationResultMutation> = {
id: 'formula.mutation.set-formula-string-batch-calculation-result',
type: CommandType.MUTATION,
handler: () => true,
};
export interface ISetFormulaCalculationStopMutation { }
export const SetFormulaCalculationStopMutation: IMutation<ISetFormulaCalculationStopMutation> = {
id: 'formula.mutation.set-formula-calculation-stop',
@@ -67,3 +110,27 @@ export const SetFormulaCalculationResultMutation: IMutation<ISetFormulaCalculati
type: CommandType.MUTATION,
handler: () => true,
};
export const SetFormulaDependencyCalculationMutation: IMutation<{}> = {
id: 'formula.mutation.set-formula-dependency-calculation',
type: CommandType.MUTATION,
handler: () => true,
};
export const SetFormulaDependencyCalculationResultMutation: IMutation<ISetFormulaDependencyCalculationResultMutation> = {
id: 'formula.mutation.set-formula-dependency-calculation-result',
type: CommandType.MUTATION,
handler: () => true,
};
export const SetCellFormulaDependencyCalculationMutation: IMutation<ISetFormulaDependencyCalculationMutation> = {
id: 'formula.mutation.set-cell-formula-dependency-calculation',
type: CommandType.MUTATION,
handler: () => true,
};
export const SetCellFormulaDependencyCalculationResultMutation: IMutation<ISetCellFormulaDependencyCalculationResultMutation> = {
id: 'formula.mutation.set-cell-formula-dependency-calculation-result',
type: CommandType.MUTATION,
handler: () => true,
};
@@ -16,17 +16,23 @@
import type { ICommandInfo } from '@univerjs/core';
import type { ISetArrayFormulaDataMutationParams } from '../commands/mutations/set-array-formula-data.mutation';
import type { ISetFormulaCalculationStartMutation } from '../commands/mutations/set-formula-calculation.mutation';
import type { ISetFormulaCalculationStartMutation, ISetFormulaDependencyCalculationMutation, ISetFormulaStringBatchCalculationMutation } from '../commands/mutations/set-formula-calculation.mutation';
import type { IFormulaDirtyData } from '../services/current-data.service';
import type { IAllRuntimeData } from '../services/runtime.service';
import { Disposable, ICommandService, Inject } from '@univerjs/core';
import { convertRuntimeToUnitData } from '../basics/runtime';
import { SetArrayFormulaDataMutation } from '../commands/mutations/set-array-formula-data.mutation';
import {
SetCellFormulaDependencyCalculationMutation,
SetCellFormulaDependencyCalculationResultMutation,
SetFormulaCalculationNotificationMutation,
SetFormulaCalculationResultMutation,
SetFormulaCalculationStartMutation,
SetFormulaCalculationStopMutation,
SetFormulaDependencyCalculationMutation,
SetFormulaDependencyCalculationResultMutation,
SetFormulaStringBatchCalculationMutation,
SetFormulaStringBatchCalculationResultMutation,
} from '../commands/mutations/set-formula-calculation.mutation';
import { SetImageFormulaDataMutation } from '../commands/mutations/set-image-formula-data.mutation';
import { FormulaDataModel } from '../models/formula-data.model';
@@ -67,6 +73,12 @@ export class CalculateController extends Disposable {
// TODO@Dushusir: Merge the array formula data into the formulaDataModel
this._formulaDataModel.setArrayFormulaRange(arrayFormulaRange);
this._formulaDataModel.setArrayFormulaCellData(arrayFormulaCellData);
} else if (command.id === SetFormulaStringBatchCalculationMutation.id) {
this._calculateFormulaString(command.params as ISetFormulaStringBatchCalculationMutation);
} else if (command.id === SetFormulaDependencyCalculationMutation.id) {
this._generateAllDependencyTreeJson();
} else if (command.id === SetCellFormulaDependencyCalculationMutation.id) {
this._generateCellDependencyTreeJson(command.params as ISetFormulaDependencyCalculationMutation);
}
})
);
@@ -98,6 +110,62 @@ export class CalculateController extends Disposable {
});
}
private async _generateAllDependencyTreeJson() {
const result = await this._calculateFormulaService.getAllDependencyJson();
this._commandService.executeCommand(
SetFormulaDependencyCalculationResultMutation.id,
{
result,
},
{
onlyLocal: true,
}
);
}
private async _generateCellDependencyTreeJson(param: ISetFormulaDependencyCalculationMutation) {
const { unitId, sheetId, row, column } = param;
const result = await this._calculateFormulaService.getCellDependencyJson(unitId, sheetId, row, column);
this._commandService.executeCommand(
SetCellFormulaDependencyCalculationResultMutation.id,
{
result,
},
{
onlyLocal: true,
}
);
}
private async _calculateFormulaString(params: ISetFormulaStringBatchCalculationMutation) {
const formulaData = this._formulaDataModel.getFormulaData();
const arrayFormulaCellData = this._formulaDataModel.getArrayFormulaCellData();
// array formula range is used to check whether the newly added array formula conflicts with the existing array formula
const arrayFormulaRange = this._formulaDataModel.getArrayFormulaRange();
const rowData = this._formulaDataModel.getHiddenRowsFiltered();
const result = await this._calculateFormulaService.executeFormulas(
params.formulas,
formulaData,
arrayFormulaCellData,
arrayFormulaRange,
rowData
);
this._commandService.executeCommand(
SetFormulaStringBatchCalculationResultMutation.id,
{
result,
},
{
onlyLocal: true,
}
);
}
// Notification
private _initialExecuteFormulaListener() {
/**
@@ -25,10 +25,16 @@ import { SetArrayFormulaDataMutation } from '../commands/mutations/set-array-for
import { RemoveDefinedNameMutation, SetDefinedNameMutation } from '../commands/mutations/set-defined-name.mutation';
import { RemoveFeatureCalculationMutation, SetFeatureCalculationMutation } from '../commands/mutations/set-feature-calculation.mutation';
import {
SetCellFormulaDependencyCalculationMutation,
SetCellFormulaDependencyCalculationResultMutation,
SetFormulaCalculationNotificationMutation,
SetFormulaCalculationResultMutation,
SetFormulaCalculationStartMutation,
SetFormulaCalculationStopMutation,
SetFormulaDependencyCalculationMutation,
SetFormulaDependencyCalculationResultMutation,
SetFormulaStringBatchCalculationMutation,
SetFormulaStringBatchCalculationResultMutation,
} from '../commands/mutations/set-formula-calculation.mutation';
import { SetFormulaDataMutation } from '../commands/mutations/set-formula-data.mutation';
import { SetImageFormulaDataMutation } from '../commands/mutations/set-image-formula-data.mutation';
@@ -76,10 +82,17 @@ export class FormulaController extends Disposable {
SetArrayFormulaDataMutation,
SetImageFormulaDataMutation,
SetFormulaCalculationStartMutation,
SetFormulaStringBatchCalculationMutation,
SetFormulaStringBatchCalculationResultMutation,
SetFormulaCalculationStopMutation,
SetFormulaCalculationNotificationMutation,
SetFormulaCalculationResultMutation,
SetFormulaDependencyCalculationMutation,
SetFormulaDependencyCalculationResultMutation,
SetCellFormulaDependencyCalculationMutation,
SetCellFormulaDependencyCalculationResultMutation,
SetDefinedNameMutation,
RemoveDefinedNameMutation,
SetFeatureCalculationMutation,
@@ -511,3 +511,111 @@ export class FormulaDependencyTree extends FormulaDependencyTreeCalculator {
// this.parents.add(tree.treeId);
// }
}
interface IFormulaDependencyTreeJsonBase {
treeId: number;
formula: string;
row: number;
column: number;
unitId: string;
subUnitId: string;
refOffsetX: number;
refOffsetY: number;
rangeList: IUnitRange[];
refTreeId: number | undefined;
}
export interface IFormulaDependencyTreeJson extends IFormulaDependencyTreeJsonBase {
children: number[];
parents: number[];
}
export interface IFormulaDependencyTreeFullJson extends IFormulaDependencyTreeJsonBase {
children: IFormulaDependencyTreeJson[];
parents: IFormulaDependencyTreeJson[];
}
export class FormulaDependencyTreeModel {
children: Set<FormulaDependencyTreeModel> = new Set();
parents: Set<FormulaDependencyTreeModel> = new Set();
treeId: number = -1;
formula: string = '';
refOffsetX: number = 0;
refOffsetY: number = 0;
row: number = -1;
column: number = -1;
unitId: string = '';
subUnitId: string = '';
rangeList: IUnitRange[] = [];
refTreeId: number | undefined;
constructor(tree: IFormulaDependencyTree) {
this.treeId = tree.treeId;
this.row = tree.row;
this.column = tree.column;
this.unitId = tree.unitId;
this.subUnitId = tree.subUnitId;
this.refOffsetX = tree.refOffsetX;
this.refOffsetY = tree.refOffsetY;
this.rangeList = tree.rangeList;
if (tree.isVirtual) {
this.refTreeId = (tree as FormulaDependencyTreeVirtual).refTree?.treeId ?? -1;
} else {
this.formula = tree.formula;
}
}
toJson(): IFormulaDependencyTreeJson {
return {
children: Array.from(this.children).map((child) => child.treeId),
parents: Array.from(this.parents).map((parent) => parent.treeId),
treeId: this.treeId,
formula: this.formula,
row: this.row,
column: this.column,
unitId: this.unitId,
subUnitId: this.subUnitId,
refOffsetX: this.refOffsetX,
refOffsetY: this.refOffsetY,
rangeList: this.rangeList,
refTreeId: this.refTreeId,
};
}
toFullJson(): IFormulaDependencyTreeFullJson {
return {
children: Array.from(this.children).map((child) => child.toJson()),
parents: Array.from(this.parents).map((parent) => parent.toJson()),
treeId: this.treeId,
formula: this.formula,
row: this.row,
column: this.column,
unitId: this.unitId,
subUnitId: this.subUnitId,
refOffsetX: this.refOffsetX,
refOffsetY: this.refOffsetY,
rangeList: this.rangeList,
refTreeId: this.refTreeId,
};
}
addParent(parent: FormulaDependencyTreeModel) {
this.parents.add(parent);
}
addChild(child: FormulaDependencyTreeModel) {
this.children.add(child);
}
}
@@ -25,21 +25,22 @@ import type { BaseAstNode } from '../ast-node/base-ast-node';
import type { BaseReferenceObject, FunctionVariantType } from '../reference-object/base-reference-object';
import type { IExecuteAstNodeData } from '../utils/ast-node-tool';
import type { PreCalculateNodeType } from '../utils/node-type';
import type { IFormulaDependencyTree } from './dependency-tree';
import type { IFormulaDependencyTree, IFormulaDependencyTreeFullJson, IFormulaDependencyTreeJson } from './dependency-tree';
import { createIdentifier, Disposable, Inject, ObjectMatrix, RTree } from '@univerjs/core';
import { prefixToken, suffixToken } from '../../basics/token';
import { IFormulaCurrentConfigService } from '../../services/current-data.service';
import { IDependencyManagerService } from '../../services/dependency-manager.service';
import { IFeatureCalculationManagerService } from '../../services/feature-calculation-manager.service';
import { IOtherFormulaManagerService } from '../../services/other-formula-manager.service';
import { IFormulaRuntimeService } from '../../services/runtime.service';
import { Lexer } from '../analysis/lexer';
import { LexerTreeBuilder } from '../analysis/lexer-tree-builder';
import { AstTreeBuilder } from '../analysis/parser';
import { NodeType } from '../ast-node/node-type';
import { Interpreter } from '../interpreter/interpreter';
import { FORMULA_AST_CACHE, generateAstNode, includeDefinedName } from '../utils/generate-ast-node';
import { FormulaDependencyTree, FormulaDependencyTreeType, FormulaDependencyTreeVirtual } from './dependency-tree';
import { FormulaDependencyTree, FormulaDependencyTreeModel, FormulaDependencyTreeType, FormulaDependencyTreeVirtual } from './dependency-tree';
const FORMULA_CACHE_LRU_COUNT = 5000;
@@ -56,6 +57,8 @@ export function generateRandomDependencyTreeId(dependencyManagerService: IDepend
export interface IFormulaDependencyGenerator {
generate(): Promise<IFormulaDependencyTree[]>;
getAllDependencyJson(): Promise<IFormulaDependencyTreeJson[]>;
getCellDependencyJson(unitId: string, sheetId: string, row: number, column: number): Promise<IFormulaDependencyTreeFullJson | undefined>;
}
export const IFormulaDependencyGenerator = createIdentifier<IFormulaDependencyGenerator>('engine-formula.dependency-generator');
@@ -74,7 +77,8 @@ export class FormulaDependencyGenerator extends Disposable {
@Inject(Interpreter) private readonly _interpreter: Interpreter,
@Inject(AstTreeBuilder) protected readonly _astTreeBuilder: AstTreeBuilder,
@Inject(Lexer) protected readonly _lexer: Lexer,
@IDependencyManagerService protected readonly _dependencyManagerService: IDependencyManagerService
@IDependencyManagerService protected readonly _dependencyManagerService: IDependencyManagerService,
@Inject(LexerTreeBuilder) protected readonly _lexerTreeBuilder: LexerTreeBuilder
) {
super();
}
@@ -1298,4 +1302,140 @@ export class FormulaDependencyGenerator extends Disposable {
return formulaRunList;
}
protected async _initializeGenerateTreeList() {
const formulaData = this._currentConfigService.getFormulaData();
const otherFormulaData = this._otherFormulaManagerService.getOtherFormulaData();
const unitData = this._currentConfigService.getUnitData();
const treeList = await this._generateTreeList(formulaData, otherFormulaData, unitData);
return treeList;
}
protected async _getAllTreeList() {
const treeList: IFormulaDependencyTree[] = await this._initializeGenerateTreeList();
const allTree: IFormulaDependencyTree[] = this._dependencyManagerService.buildDependencyTree(treeList);
return allTree;
}
protected _formulaDependencyTreeModel = new Map<number, FormulaDependencyTreeModel>();
protected _getTreeModel(treeId: number): FormulaDependencyTreeModel {
let treeModel = this._formulaDependencyTreeModel.get(treeId);
if (!treeModel) {
const tree = this._getTreeById(treeId);
if (!tree) {
throw new Error('FormulaDependencyTree is null');
}
treeModel = new FormulaDependencyTreeModel(tree);
// if (tree.isVirtual) {
// const formula = this._lexerTreeBuilder.moveFormulaRefOffset(
// tree.formula,
// tree.refOffsetX,
// tree.refOffsetY
// );
// treeModel.formula = formula;
// }
this._formulaDependencyTreeModel.set(treeId, treeModel);
}
return treeModel;
}
protected _getDependencyTreeParenIds(tree: IFormulaDependencyTree): Set<number> {
return tree.parents;
}
protected _getDependencyTreeChildrenIds(tree: IFormulaDependencyTree): Set<number> {
return tree.children;
}
protected _getFormulaDependencyTreeModel(tree: IFormulaDependencyTree): FormulaDependencyTreeModel {
const treeModel = this._getTreeModel(tree.treeId);
const parentIds = this._getDependencyTreeParenIds(tree);
for (const parentId of parentIds) {
const parentTreeModel = this._getTreeModel(parentId);
treeModel.addParent(parentTreeModel);
parentTreeModel.addChild(treeModel);
}
return treeModel;
}
protected _endFormulaDependencyTreeModel() {
this._formulaDependencyTreeModel.clear();
}
protected _startFormulaDependencyTreeModel() {
}
async getAllDependencyJson(): Promise<IFormulaDependencyTreeJson[]> {
const treeList = await this._getAllTreeList();
this._startFormulaDependencyTreeModel();
const results: FormulaDependencyTreeModel[] = [];
for (const tree of treeList) {
const treeModel = this._getFormulaDependencyTreeModel(tree);
results[tree.treeId] = treeModel;
}
const resultsJson: IFormulaDependencyTreeJson[] = [];
for (const result of results) {
if (result) {
resultsJson.push(result.toJson());
}
}
this._endFormulaDependencyTreeModel();
return resultsJson;
}
async getCellDependencyJson(unitId: string, sheetId: string, row: number, column: number): Promise<IFormulaDependencyTreeFullJson | undefined> {
await this._initializeGenerateTreeList();
const treeId = this._dependencyManagerService.getFormulaDependency(unitId, sheetId, row, column);
if (treeId == null) {
return;
}
const tree = this._getTreeById(treeId);
if (!tree) {
return;
}
this._startFormulaDependencyTreeModel();
const treeModel = this._getFormulaDependencyTreeModel(tree);
const formula = this._lexerTreeBuilder.moveFormulaRefOffset(
tree.formula,
tree.refOffsetX,
tree.refOffsetY
);
treeModel.formula = formula;
const childIds = this._getDependencyTreeChildrenIds(tree);
for (const childId of childIds) {
const childTreeModel = this._getTreeModel(childId);
const tree = this._getTreeById(treeId);
if (!tree) {
continue;
}
const formula = this._lexerTreeBuilder.moveFormulaRefOffset(
tree.formula,
tree.refOffsetX,
tree.refOffsetY
);
childTreeModel.formula = formula;
treeModel.addChild(childTreeModel);
}
this._endFormulaDependencyTreeModel();
return treeModel.toFullJson();
}
}
+171 -2
View File
@@ -15,10 +15,10 @@
*/
import type { ICommandInfo, IDisposable } from '@univerjs/core';
import type { FormulaExecutedStateType, IExecutionInProgressParams, ISequenceNode, ISetFormulaCalculationNotificationMutation, ISetFormulaCalculationStartMutation } from '@univerjs/engine-formula';
import type { FormulaExecutedStateType, IExecutionInProgressParams, IFormulaDependencyTreeFullJson, IFormulaDependencyTreeJson, IFormulaExecuteResultMap, IFormulaStringMap, ISequenceNode, ISetCellFormulaDependencyCalculationResultMutation, ISetFormulaCalculationNotificationMutation, ISetFormulaCalculationStartMutation, ISetFormulaDependencyCalculationResultMutation, ISetFormulaStringBatchCalculationResultMutation } from '@univerjs/engine-formula';
import { ICommandService, IConfigService, Inject, Injector } from '@univerjs/core';
import { FBase } from '@univerjs/core/facade';
import { ENGINE_FORMULA_CYCLE_REFERENCE_COUNT, GlobalComputingStatusService, LexerTreeBuilder, SetFormulaCalculationNotificationMutation, SetFormulaCalculationStartMutation, SetFormulaCalculationStopMutation } from '@univerjs/engine-formula';
import { ENGINE_FORMULA_CYCLE_REFERENCE_COUNT, GlobalComputingStatusService, LexerTreeBuilder, SetCellFormulaDependencyCalculationMutation, SetCellFormulaDependencyCalculationResultMutation, SetFormulaCalculationNotificationMutation, SetFormulaCalculationStartMutation, SetFormulaCalculationStopMutation, SetFormulaDependencyCalculationMutation, SetFormulaDependencyCalculationResultMutation, SetFormulaStringBatchCalculationMutation, SetFormulaStringBatchCalculationResultMutation } from '@univerjs/engine-formula';
import { filter, firstValueFrom, map, race, timer } from 'rxjs';
/**
@@ -248,4 +248,173 @@ export class FFormula extends FBase {
setMaxIteration(maxIteration: number): void {
this._configService.setConfig(ENGINE_FORMULA_CYCLE_REFERENCE_COUNT, maxIteration);
}
/**
* Execute a batch of formulas asynchronously and receive computed results.
*
* Each formula cell is represented as a string array:
* [fullFormula, ...subFormulas]
*
* Where:
* - fullFormula (index 0) is the complete formula expression written in the cell.
* Example: "=SUM(A1:A10) + SQRT(D7)".
*
* - subFormulas (index 1+) are **optional decomposed expressions** extracted from
* the full formula. Each of them can be independently computed by the formula engine.
*
* These sub-expressions can include:
* - Single-cell references: "A2", "B2", "C5"
* - Range references: "A1:A10"
* - Function calls: "SQRT(D7)", "ABS(A2-B2)"
* - Any sub-formula that was parsed out of the original formula and can be
* evaluated on its own.
*
* The batch execution engine may use these sub-formulas for dependency resolution,
* incremental computation, or performance optimizations.
*
* @param {IFormulaStringMap} formulas
* Nested structure (unit → sheet → row → column) describing formulas and
* their decomposed sub-expressions.
*
* @param {(result: IFormulaExecuteResultMap) => void} callback
* Receives the computed value map mirroring the input structure.
*
* @returns {IDisposable}
* A disposer to stop listening for batch results.
*
* @example
* ```ts
* const formulaEngine = univerAPI.getFormula();
* const formulas = {
* Book1: {
* Sheet1: {
* 2: {
* 3: [
* // Full formula:
* "=SUM(A1:A10) + SQRT(D7)",
*
* // Decomposed sub-formulas (each one can be evaluated independently):
* "SUM(A1:A10)", // sub-formula 1
* "SQRT(D7)", // sub-formula 2
* "A1:A10", // range reference
* "D7", // single-cell reference
* ],
* },
* 4: {
* 5: [
* "=A2 + B2 + SQRT(C5)",
* "A2",
* "B2",
* "SQRT(C5)",
* ],
* }
* },
* },
* };
*
* const disposer = formulaEngine.executeFormulas(formulas, (result) => {
* console.log(result);
* });
*
* ```
*/
executeFormulas(formulas: IFormulaStringMap, callback: (result: IFormulaExecuteResultMap) => void): void {
this._commandService.executeCommand(SetFormulaStringBatchCalculationMutation.id, { formulas }, { onlyLocal: true });
const disposable = this._commandService.onCommandExecuted((command: ICommandInfo) => {
if (command.id !== SetFormulaStringBatchCalculationResultMutation.id) {
return;
}
const params = command.params as ISetFormulaStringBatchCalculationResultMutation;
if (params.result != null) {
callback(params.result);
}
disposable.dispose();
});
}
/**
* Retrieve all formula dependency trees that were produced during the latest
* dependency-analysis run. This triggers a local dependency-calculation command
* and returns the complete set of dependency trees once the calculation finishes.
*
* @param callback A function invoked with the resulting array of dependency trees.
*
* @returns {IDisposable} An object that disposes the internal event listener.
*
* @example
* ```ts
* const formulaEngine = univerAPI.getFormula();
*
* // Fetch all dependency trees generated for the current workbook.
* const disposable = formulaEngine.getAllDependencyTrees((trees) => {
* console.log('All dependency trees:', trees);
* });
*
* ```
*/
getAllDependencyTrees(callback: (result: IFormulaDependencyTreeJson[]) => void): void {
this._commandService.executeCommand(SetFormulaDependencyCalculationMutation.id, undefined, { onlyLocal: true });
const disposable = this._commandService.onCommandExecuted((command: ICommandInfo) => {
if (command.id !== SetFormulaDependencyCalculationResultMutation.id) {
return;
}
const params = command.params as ISetFormulaDependencyCalculationResultMutation;
if (params.result != null) {
callback(params.result);
}
disposable.dispose();
});
}
/**
* Retrieve the dependency tree of a specific cell. This triggers a local
* dependency-calculation command for the given unit, sheet, and cell location,
* and returns the computed dependency tree when the calculation is completed.
*
* @param param The target cell location:
* - `unitId` The workbook ID.
* - `sheetId` The sheet ID.
* - `row` The zero-based row index.
* - `column` The zero-based column index.
*
* @param callback A function invoked with the resulting dependency tree or
* `undefined` if no dependency tree exists for that cell.
*
* @returns {IDisposable} An object that disposes the internal event listener.
*
* @example
* ```ts
* const formulaEngine = univerAPI.getFormula();
*
* // Query the dependency tree for cell B2 in a specific sheet.
* const disposable = formulaEngine.getCellDependencyTree(
* { unitId: 'workbook1', sheetId: 'sheet1', row: 1, column: 1 },
* (tree) => {
* console.log('Cell dependency tree:', tree);
* }
* );
*
* ```
*/
getCellDependencyTree(param: { unitId: string; sheetId: string; row: number; column: number }, callback: (result: IFormulaDependencyTreeFullJson | undefined) => void): void {
this._commandService.executeCommand(SetCellFormulaDependencyCalculationMutation.id, param, { onlyLocal: true });
const disposable = this._commandService.onCommandExecuted((command: ICommandInfo) => {
if (command.id !== SetCellFormulaDependencyCalculationResultMutation.id) {
return;
}
const params = command.params as ISetCellFormulaDependencyCalculationResultMutation;
if (params.result !== undefined) {
callback(params.result);
}
disposable.dispose();
});
}
}
+13 -1
View File
@@ -26,6 +26,8 @@ export type {
IFormulaData,
IFormulaDataItem,
IFormulaDatasetConfig,
IFormulaExecuteResultMap,
IFormulaStringMap,
IRuntimeImageFormulaDataType,
IRuntimeUnitDataType,
ISheetData,
@@ -53,13 +55,23 @@ export { type ISetDefinedNameMutationParam, type ISetDefinedNameMutationSearchPa
export { SetDefinedNameMutationFactory } from './commands/mutations/set-defined-name.mutation';
export { RemoveFeatureCalculationMutation, SetFeatureCalculationMutation } from './commands/mutations/set-feature-calculation.mutation';
export {
type ISetCellFormulaDependencyCalculationResultMutation,
type ISetFormulaCalculationNotificationMutation,
type ISetFormulaCalculationResultMutation,
type ISetFormulaCalculationStartMutation,
type ISetFormulaDependencyCalculationMutation,
type ISetFormulaDependencyCalculationResultMutation,
type ISetFormulaStringBatchCalculationResultMutation,
SetCellFormulaDependencyCalculationMutation,
SetCellFormulaDependencyCalculationResultMutation,
SetFormulaCalculationNotificationMutation,
SetFormulaCalculationResultMutation,
SetFormulaCalculationStartMutation,
SetFormulaCalculationStopMutation,
SetFormulaDependencyCalculationMutation,
SetFormulaDependencyCalculationResultMutation,
SetFormulaStringBatchCalculationMutation,
SetFormulaStringBatchCalculationResultMutation,
} from './commands/mutations/set-formula-calculation.mutation';
export { type ISetFormulaDataMutationParams, SetFormulaDataMutation } from './commands/mutations/set-formula-data.mutation';
export { type ISetImageFormulaDataMutationParams, SetImageFormulaDataMutation } from './commands/mutations/set-image-formula-data.mutation';
@@ -83,7 +95,7 @@ export { ReferenceNodeFactory } from './engine/ast-node/reference-node';
export { SuffixNodeFactory } from './engine/ast-node/suffix-node';
export { UnionNodeFactory } from './engine/ast-node/union-node';
export { ValueNodeFactory } from './engine/ast-node/value-node';
export { FormulaDependencyTree, type IFormulaDependencyTree } from './engine/dependency/dependency-tree';
export { FormulaDependencyTree, FormulaDependencyTreeModel, type IFormulaDependencyTree, type IFormulaDependencyTreeFullJson, type IFormulaDependencyTreeJson } from './engine/dependency/dependency-tree';
export { FormulaDependencyTreeType } from './engine/dependency/dependency-tree';
export { FormulaDependencyTreeVirtual } from './engine/dependency/dependency-tree';
export { FormulaDependencyGenerator, IFormulaDependencyGenerator } from './engine/dependency/formula-dependency';
@@ -18,15 +18,24 @@ import type { IUnitRange } from '@univerjs/core';
import type { Observable } from 'rxjs';
import type {
IArrayFormulaRangeType,
IArrayFormulaUnitCellType,
IFeatureDirtyRangeType,
IFormulaData,
IFormulaDatasetConfig,
IFormulaExecuteResultItem,
IFormulaExecuteResultMap,
IFormulaStringMap,
IRuntimeUnitDataType,
IUnitExcludedCell,
IUnitRowData,
} from '../basics/common';
import type { IUniverEngineFormulaConfig } from '../controller/config.schema';
import type { LexerNode } from '../engine/analysis/lexer-node';
import type { FunctionVariantType } from '../engine/reference-object/base-reference-object';
import type { IFormulaDependencyTreeFullJson, IFormulaDependencyTreeJson } from '../engine/dependency/dependency-tree';
import type { BaseReferenceObject, FunctionVariantType } from '../engine/reference-object/base-reference-object';
import type { ArrayValueObject } from '../engine/value-object/array-value-object';
import type { BaseValueObject } from '../engine/value-object/base-value-object';
import type { IAllRuntimeData, IExecutionInProgressParams } from './runtime.service';
import {
AsyncLock,
@@ -43,7 +52,6 @@ import { CELL_INVERTED_INDEX_CACHE } from '../basics/inverted-index-cache';
import { DEFAULT_CYCLE_REFERENCE_COUNT, ENGINE_FORMULA_PLUGIN_CONFIG_KEY } from '../controller/config.schema';
import { Lexer } from '../engine/analysis/lexer';
import { AstTreeBuilder } from '../engine/analysis/parser';
import { ErrorNode } from '../engine/ast-node/base-ast-node';
import { IFormulaDependencyGenerator } from '../engine/dependency/formula-dependency';
import { Interpreter } from '../engine/interpreter/interpreter';
import { FORMULA_REF_TO_ARRAY_CACHE } from '../engine/reference-object/base-reference-object';
@@ -66,6 +74,9 @@ export interface ICalculateFormulaService {
execute(formulaDatasetConfig: IFormulaDatasetConfig): Promise<void>;
stopFormulaExecution(): void;
calculate(formulaString: string, transformSuffix?: boolean): void;
executeFormulas(formulas: IFormulaStringMap, formulaData: IFormulaData, arrayFormulaCellData: IArrayFormulaUnitCellType, arrayFormulaRange: IArrayFormulaRangeType, rowData: IUnitRowData): Promise<IFormulaExecuteResultMap>;
getAllDependencyJson(): Promise<IFormulaDependencyTreeJson[]>;
getCellDependencyJson(unitId: string, sheetId: string, row: number, column: number): Promise<IFormulaDependencyTreeFullJson | undefined>;
}
export const ICalculateFormulaService = createIdentifier<ICalculateFormulaService>('engine-formula.calculate-formula.service');
@@ -367,42 +378,147 @@ export class CalculateFormulaService extends Disposable implements ICalculateFor
return this._runtimeService.getAllRuntimeData();
}
calculate(formulaString: string, transformSuffix: boolean = true) {
// TODO how to observe @alex
// this.getObserver('onBeforeFormulaCalculateObservable')?.notifyObservers(formulaString);
const lexerNode = this._lexer.treeBuilder(formulaString, transformSuffix);
async executeFormulas(formulas: IFormulaStringMap, formulaData: IFormulaData, arrayFormulaCellData: IArrayFormulaUnitCellType, arrayFormulaRange: IArrayFormulaRangeType, rowData?: IUnitRowData) {
this._currentConfigService.loadDataLite(
formulaData,
arrayFormulaCellData,
arrayFormulaRange,
rowData
);
if (Object.values(ErrorType).includes(lexerNode as ErrorType)) {
return ErrorNode.create(lexerNode as ErrorType);
const unitData = this._currentConfigService.getUnitData();
this._runtimeService.reset();
const result: IFormulaExecuteResultMap = {};
for (const unitId of Object.keys(formulas)) {
const sheetFormulas = formulas[unitId];
if (sheetFormulas == null) continue;
result[unitId] = {};
for (const sheetId of Object.keys(sheetFormulas)) {
const rowFormulas = sheetFormulas[sheetId];
if (rowFormulas == null) continue;
const sheetItem = unitData[unitId]?.[sheetId];
if (sheetItem == null) continue;
result[unitId]![sheetId] = {};
for (const rowString of Object.keys(rowFormulas)) {
const row = Number.parseInt(rowString);
result[unitId]![sheetId]![row] = {};
const cellData = rowFormulas[row];
if (!cellData) continue;
for (const columnString of Object.keys(cellData)) {
const column = Number.parseInt(columnString);
const formulaStrings = cellData[column];
if (!formulaStrings) continue;
const rowCount = sheetItem.rowCount || 0;
const columnCount = sheetItem.columnCount || 0;
this._runtimeService.setCurrent(
row,
column,
rowCount,
columnCount,
sheetId,
unitId
);
const item: IFormulaExecuteResultItem[] = [];
for (const formulaString of formulaStrings) {
let resultCell = await this.calculate(formulaString);
if (!resultCell) {
item.push({
value: null,
formula: formulaString,
});
continue;
}
if (resultCell.isReferenceObject()) {
resultCell = (resultCell as BaseReferenceObject).toArrayValueObject();
}
const resultRefObject = resultCell as BaseValueObject;
if (resultRefObject.isArray()) {
const resultRefArrayObject = resultRefObject as ArrayValueObject;
if (resultRefArrayObject.getRowCount() === 1 && resultRefArrayObject.getColumnCount() === 1) {
item.push({
value: resultRefArrayObject.getFirstCell().getValue(),
formula: formulaString,
});
continue;
}
item.push({
value: resultRefArrayObject.toValue(),
formula: formulaString,
});
continue;
}
item.push({
value: resultRefObject.getValue(),
formula: formulaString,
});
}
result[unitId]![sheetId]![row]![column] = item;
}
}
}
}
// this.lexerTreeBuilder.suffixExpressionHandler(lexerNode); // suffix Express, 1+(3*4=4)*5+1 convert to 134*4=5*1++
return result;
}
// console.log('sequence', this.lexerTreeBuilder.sequenceNodesBuilder(formulaString));
async calculate(formulaString: string) {
// TODO how to observe @alex
// this.getObserver('onBeforeFormulaCalculateObservable')?.notifyObservers(formulaString);
const lexerNode = this._lexer.treeBuilder(formulaString);
// this.getObserver('onAfterFormulaLexerObservable')?.notifyObservers(lexerNode);
// const astTreeBuilder = new AstTreeBuilder();
if (Object.values(ErrorType).includes(lexerNode as ErrorType)) {
return;
}
const astNode = this._astTreeBuilder.parse(lexerNode as LexerNode);
// console.log('astNode', astNode?.serialize());
astNode?.serialize();
const interpreter = this._interpreter;
// const interpreter = Interpreter.create();
if (astNode == null) {
return;
}
// if (astNode == null) {
// return;
// }
const nodeData = {
node: astNode,
refOffsetX: 0,
refOffsetY: 0,
};
// if (interpreter.checkAsyncNode(astNode)) {
// const resultPromise = interpreter.executeAsync(astNode);
if (interpreter.checkAsyncNode(astNode)) {
return await interpreter.executeAsync(nodeData);
} else {
return interpreter.execute(nodeData);
}
}
// resultPromise.then((value) => {
// console.log('formulaResult', value);
// });
// } else {
// console.log(interpreter.execute(astNode));
// }
async getAllDependencyJson(): Promise<IFormulaDependencyTreeJson[]> {
return this._formulaDependencyGenerator.getAllDependencyJson();
}
async getCellDependencyJson(unitId: string, sheetId: string, row: number, column: number): Promise<IFormulaDependencyTreeFullJson | undefined> {
return this._formulaDependencyGenerator.getCellDependencyJson(unitId, sheetId, row, column);
}
}
@@ -17,6 +17,7 @@
import type { IUnitRange, LocaleType, Nullable, Workbook } from '@univerjs/core';
import type {
IArrayFormulaRangeType,
IArrayFormulaUnitCellType,
IDirtyUnitFeatureMap,
IDirtyUnitOtherFormulaMap,
IDirtyUnitSheetDefinedNameMap,
@@ -113,6 +114,8 @@ export interface IFormulaCurrentConfigService {
getFilteredOutRows(unitId: string, sheetId: string, startRow: number, endRow: number): number[];
setSheetNameMap(sheetIdToNameMap: IUnitSheetIdToNameMap): void;
loadDataLite(formulaData: IFormulaData, arrayFormulaCellData: IArrayFormulaUnitCellType, arrayFormulaRange: IArrayFormulaRangeType, rowData?: IUnitRowData): void;
}
export class FormulaCurrentConfigService extends Disposable implements IFormulaCurrentConfigService {
@@ -345,6 +348,25 @@ export class FormulaCurrentConfigService extends Disposable implements IFormulaC
this._mergeNameMap(this._sheetNameMap, this._dirtyNameMap);
}
loadDataLite(formulaData: IFormulaData, arrayFormulaCellData: IArrayFormulaUnitCellType, arrayFormulaRange: IArrayFormulaRangeType, rowData?: IUnitRowData) {
const { allUnitData, unitSheetNameMap, unitStylesData } = this._loadSheetData();
this._unitData = allUnitData;
this._unitStylesData = unitStylesData;
this._sheetNameMap = unitSheetNameMap;
this._formulaData = formulaData;
this._arrayFormulaCellData = convertUnitDataToRuntime(arrayFormulaCellData);
this._arrayFormulaRange = arrayFormulaRange;
// apply row data, including rows hidden by filters
rowData && this._applyUnitRowData(rowData);
}
getDirtyData(): IFormulaDirtyData {
return {
forceCalculation: this._forceCalculate,