feat(engine-formula): support cross-workbook references (#7292)

This commit is contained in:
Univer
2026-07-19 00:25:10 +08:00
committed by GitHub
parent 68ff703d07
commit eb512bef50
41 changed files with 1602 additions and 80 deletions
@@ -68,6 +68,14 @@ describe('Test ref regex', () => {
expect(new RegExp(REFERENCE_TABLE_MULTIPLE_COLUMN_REGEX).test('Table1[[#Title],[#Data],[Column1]:[Column10]]')).toBe(true);
});
it('distinguishes A1 workbook qualifiers from Table qualifiers', () => {
expect(regexTestSingeRange('[Book]Sheet1!A1')).toBe(true);
expect(regexTestSingeRange('[1]Sheet1!A1')).toBe(true);
expect(new RegExp(REFERENCE_TABLE_SINGLE_COLUMN_REGEX).test('[Book]Sheet1!A1')).toBe(false);
expect(new RegExp(REFERENCE_TABLE_SINGLE_COLUMN_REGEX).test('[1]!SalesTable[Amount]')).toBe(true);
expect(new RegExp(REFERENCE_TABLE_SINGLE_COLUMN_REGEX).test('Sales.xlsx!SalesTable[Amount]')).toBe(true);
});
it('isReferenceString', () => {
expect(isReferenceString('A1')).toBeTruthy();
expect(isReferenceString('Sheet1!A1')).toBeTruthy();
@@ -424,10 +432,13 @@ describe('Test ref regex', () => {
expect(TITLE_ONLY.test('Table1[#This Row]')).toBe(true);
});
// Should allow sheet/unit prefix if UNIT_NAME_REGEX supports it
it('rejects unit prefix', () => {
it('accepts external Unit qualifiers', () => {
TITLE_ONLY.lastIndex = 0;
expect(TITLE_ONLY.test('Sheet1!TableA[#Data]')).toBe(false);
expect(TITLE_ONLY.test('Book.xlsx!TableA[#Data]')).toBe(true);
TITLE_ONLY.lastIndex = 0;
expect(TITLE_ONLY.test("'Customer Base'!TableA[#Data]")).toBe(true);
TITLE_ONLY.lastIndex = 0;
expect(TITLE_ONLY.test('[1]!TableA[#Data]')).toBe(true);
});
// Should reject non-hash titles
@@ -25,6 +25,7 @@ import type {
Nullable,
ObjectMatrix,
Styles,
UniverInstanceType,
} from '@univerjs/core';
import type { sequenceNodeType } from '../engine/utils/sequence';
import type { IImageFormulaInfo } from '../engine/value-object/primitive-object';
@@ -92,6 +93,18 @@ export interface IUnitSheetNameMap {
[unitId: string]: Nullable<{ [sheetName: string]: string }>;
}
export type FormulaUnitType = UniverInstanceType.UNIVER_SHEET | UniverInstanceType.UNIVER_BASE;
export interface IFormulaUnitNameMapItem {
name: string;
unitType: FormulaUnitType;
}
/** Runtime Unit metadata keyed by stable unitId. */
export interface IFormulaUnitNameMap {
[unitId: string]: IFormulaUnitNameMapItem;
}
export interface IUnitSheetIdToNameMap {
[unitId: string]: Nullable<{ [sheetId: string]: string }>;
}
@@ -194,6 +207,11 @@ export interface ISuperTable {
sheetId: string;
titleMap: Map<string, number>;
range: IRange;
/**
* Whether the projected range contains a physical header row.
* Sheet tables default to true; Base virtual tables store records from row 0.
*/
showHeader?: boolean;
}
export enum TableOptionType {
@@ -228,6 +246,7 @@ export interface IFormulaDatasetConfig {
allUnitData?: IUnitData;
unitStylesData?: IUnitStylesData;
unitSheetNameMap?: IUnitSheetNameMap;
unitNameMap?: IFormulaUnitNameMap;
maxIteration?: number;
isCalculateTreeModel?: boolean;
rowData?: IUnitRowData; // Include rows hidden by filters
+8 -4
View File
@@ -70,13 +70,17 @@ const TABLE_CONTENT_REGEX = '\\[((?<!#)[\\s\\S])*\\]';
const TABLE_MULTIPLE_COLUMN_REGEX = `${TABLE_CONTENT_REGEX}${RANGE_SYMBOL}${TABLE_CONTENT_REGEX}`;
export const REFERENCE_TABLE_ALL_COLUMN_REGEX = `^(${UNIT_NAME_REGEX})?${TABLE_NAME_REGEX}$`;
// Display formulas use Book!Table[Column], OOXML formulas use [n]!Table[Column],
// and the legacy runtime-id form [unitId]Table[Column] remains accepted.
const TABLE_UNIT_QUALIFIER_REGEX = `(?:(?:${UNIT_NAME_REGEX}|'(?:[^']|'')+'|[^\\s!\\[\\]]+)!)?(?:${UNIT_NAME_REGEX})?`;
export const REFERENCE_TABLE_SINGLE_COLUMN_REGEX = `^(${UNIT_NAME_REGEX})?${TABLE_NAME_REGEX}(${TABLE_CONTENT_REGEX}|\\[${TABLE_TITLE_REGEX}${TABLE_CONTENT_REGEX}\\])+$`; // =Table1[Column1] | =Table1[[#Title],[Column1]]
export const REFERENCE_TABLE_ALL_COLUMN_REGEX = `^${TABLE_UNIT_QUALIFIER_REGEX}${TABLE_NAME_REGEX}$`;
export const REFERENCE_TABLE_MULTIPLE_COLUMN_REGEX = `^(${UNIT_NAME_REGEX})?${TABLE_NAME_REGEX}(\\[${TABLE_MULTIPLE_COLUMN_REGEX}\\])?$|^${TABLE_NAME_REGEX}(\\[${TABLE_TITLE_REGEX}${TABLE_MULTIPLE_COLUMN_REGEX}\\])?$`; // =Table1[[#Title],[Column1]:[Column2]] | =Table1[[Column1]:[Column2]]
export const REFERENCE_TABLE_SINGLE_COLUMN_REGEX = `^${TABLE_UNIT_QUALIFIER_REGEX}${TABLE_NAME_REGEX}(${TABLE_CONTENT_REGEX}|\\[${TABLE_TITLE_REGEX}${TABLE_CONTENT_REGEX}\\])+$`; // =Table1[Column1] | =Table1[[#Title],[Column1]]
export const REFERENCE_TABLE_TITLE_ONLY_ANY_HASH_REGEX = `^(${UNIT_NAME_REGEX})?${TABLE_NAME_REGEX}\\[\\s*#([^\\]]+)\\s*\\]$`; // =Table1[#All] | =Table1[#Data] | =Table1[#Headers] | =Table1[#Totals] | =Table1[#This Row]
export const REFERENCE_TABLE_MULTIPLE_COLUMN_REGEX = `^${TABLE_UNIT_QUALIFIER_REGEX}${TABLE_NAME_REGEX}(\\[${TABLE_MULTIPLE_COLUMN_REGEX}\\])?$|^${TABLE_UNIT_QUALIFIER_REGEX}${TABLE_NAME_REGEX}(\\[${TABLE_TITLE_REGEX}${TABLE_MULTIPLE_COLUMN_REGEX}\\])?$`; // =Table1[[#Title],[Column1]:[Column2]] | =Table1[[Column1]:[Column2]]
export const REFERENCE_TABLE_TITLE_ONLY_ANY_HASH_REGEX = `^${TABLE_UNIT_QUALIFIER_REGEX}${TABLE_NAME_REGEX}\\[\\s*#([^\\]]+)\\s*\\]$`; // =Table1[#All] | =Table1[#Data] | =Table1[#Headers] | =Table1[#Totals] | =Table1[#This Row]
export const REFERENCE_TABLE_ALL_COLUMN_REGEX_PRECOMPILING = new RegExp(REFERENCE_TABLE_ALL_COLUMN_REGEX);
@@ -51,6 +51,7 @@ import {
import { FormulaRuntimeService, IFormulaRuntimeService } from '../../../services/runtime.service';
import { ISheetRowFilteredService, SheetRowFilteredService } from '../../../services/sheet-row-filtered.service';
import { ISuperTableService, SuperTableService } from '../../../services/super-table.service';
import { FormulaUnitReferenceResolver, IFormulaUnitReferenceResolver } from '../../../services/unit-reference-resolver.service';
import { AstRootNodeFactory } from '../../ast-node/ast-root-node';
import { FunctionNodeFactory } from '../../ast-node/function-node';
import { LambdaNodeFactory } from '../../ast-node/lambda-node';
@@ -300,6 +301,7 @@ function registerFormulaDependencies(injector: Injector) {
injector.add([LexerTreeBuilder]);
injector.add([IFormulaCurrentConfigService, { useClass: FormulaCurrentConfigService }]);
injector.add([IFormulaUnitReferenceResolver, { useClass: FormulaUnitReferenceResolver }]);
injector.add([IHyperlinkEngineFormulaService, { useClass: HyperlinkEngineFormulaService }]);
injector.add([IFormulaRuntimeService, { useClass: FormulaRuntimeService }]);
injector.add([IFunctionService, { useClass: FunctionService }]);
@@ -19,6 +19,7 @@ import type { BaseAstNode } from '../../ast-node/base-ast-node';
import type { ArrayValueObject } from '../../value-object/array-value-object';
import type { BaseValueObject } from '../../value-object/base-value-object';
import type { LexerNode } from '../lexer-node';
import { ObjectMatrix, UniverInstanceType } from '@univerjs/core';
import { beforeEach, describe, expect, it } from 'vitest';
import { ErrorType } from '../../../basics/error-type';
import { FUNCTION_NAMES_LOGICAL } from '../../../functions/logical/function-names';
@@ -27,6 +28,8 @@ import { If } from '../../../functions/logical/if';
import { Percentof } from '../../../functions/logical/percentof';
import { FUNCTION_NAMES_LOOKUP } from '../../../functions/lookup/function-names';
import { Hstack } from '../../../functions/lookup/hstack';
import { Indirect } from '../../../functions/lookup/indirect';
import { Offset } from '../../../functions/lookup/offset';
import { FUNCTION_NAMES_MATH } from '../../../functions/math/function-names';
import { Pi } from '../../../functions/math/pi';
import { Subtotal } from '../../../functions/math/subtotal';
@@ -46,6 +49,7 @@ import { IFormulaCurrentConfigService } from '../../../services/current-data.ser
import { IFunctionService } from '../../../services/function.service';
import { IFormulaRuntimeService } from '../../../services/runtime.service';
import { ISuperTableService } from '../../../services/super-table.service';
import { IFormulaDependencyGenerator } from '../../dependency/formula-dependency';
import { Interpreter } from '../../interpreter/interpreter';
import { generateExecuteAstNodeData } from '../../utils/ast-node-tool';
import { Lexer } from '../lexer';
@@ -86,9 +90,52 @@ describe('Test indirect', () => {
dirtyUnitFeatureMap: {},
dirtyUnitOtherFormulaMap: {},
excludedCell: {},
allUnitData: {
[testBed.unitId]: testBed.sheetData,
});
formulaCurrentConfigService.registerUnitData({
[testBed.unitId]: testBed.sheetData,
'sales-source': {
'source-sheet': {
cellData: new ObjectMatrix({
0: { 0: { v: 42 }, 1: { v: 'Other' } },
1: { 0: { v: 10 }, 1: { v: 20 } },
}),
rowCount: 2,
columnCount: 2,
rowData: {},
columnData: {},
},
},
'base-source': {
'base-table-sheet': {
cellData: new ObjectMatrix({
0: { 0: { v: 'Value' } },
1: { 0: { v: 7 } },
}),
rowCount: 2,
columnCount: 1,
rowData: {},
columnData: {},
},
},
});
formulaCurrentConfigService.registerUnitNameMap({
[testBed.unitId]: {
name: 'Host.xlsx',
unitType: UniverInstanceType.UNIVER_SHEET,
},
'sales-source': {
name: 'Sales',
unitType: UniverInstanceType.UNIVER_SHEET,
},
'base-source': {
name: 'BaseData',
unitType: UniverInstanceType.UNIVER_BASE,
},
});
formulaCurrentConfigService.registerSheetNameMap({
[testBed.unitId]: { Sheet1: testBed.sheetId },
'sales-source': { Data: 'source-sheet' },
'base-source': { Records: 'base-table-sheet' },
});
const sheetItem = testBed.sheetData[testBed.sheetId];
@@ -114,6 +161,8 @@ describe('Test indirect', () => {
new StdevP(FUNCTION_NAMES_STATISTICAL.STDEV_P),
new Regexmatch(FUNCTION_NAMES_TEXT.REGEXMATCH),
new Hstack(FUNCTION_NAMES_LOOKUP.HSTACK),
new Indirect(FUNCTION_NAMES_LOOKUP.INDIRECT),
new Offset(FUNCTION_NAMES_LOOKUP.OFFSET),
new Groupby(FUNCTION_NAMES_LOGICAL.GROUPBY),
new If(FUNCTION_NAMES_LOGICAL.IF),
new Percentof(FUNCTION_NAMES_LOGICAL.PERCENTOF)
@@ -136,11 +185,33 @@ describe('Test indirect', () => {
endColumn: 4,
},
});
superTableService.registerTable('sales-source', 'SalesTable', {
sheetId: 'source-sheet',
titleMap: new Map([['Amount', 0]]),
range: {
startRow: 0,
endRow: 1,
startColumn: 0,
endColumn: 1,
},
});
superTableService.registerTable('base-source', 'BaseTable', {
sheetId: 'base-table-sheet',
titleMap: new Map([['Value', 0]]),
range: {
startRow: 0,
endRow: 1,
startColumn: 0,
endColumn: 0,
},
});
});
describe('normal', () => {
it('preserves xleta aggregator tokens inside GROUPBY', () => {
const lexerNode = lexer.treeBuilder('=_xlfn.GROUPBY(A1:A3,A1:A3,_xlfn.HSTACK(_xleta.COUNTA,_xleta.PERCENTOF),0)');
const lexerNode = lexer.treeBuilder(
'=_xlfn.GROUPBY(A1:A3,A1:A3,_xlfn.HSTACK(_xleta.COUNTA,_xleta.PERCENTOF),0)'
);
const astNode = astTreeBuilder.parse(lexerNode as LexerNode) as BaseAstNode;
const groupbyNode = astNode.getChildren()[0];
const hstackNode = groupbyNode.getChildren()[2];
@@ -160,6 +231,247 @@ describe('Test indirect', () => {
expect((result as BaseValueObject).getValue()).toStrictEqual(ErrorType.REF);
});
it('resolves a cross-workbook A1 qualifier by display name', () => {
const lexerNode = lexer.treeBuilder("=SUM('[sales.XLSX]Data'!A1)");
const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));
expect((result as BaseValueObject).getValue()).toBe(42);
});
it('stores the resolved source unit id in dependency ranges', async () => {
get(IFormulaCurrentConfigService).registerFormulaData({
test: {
sheet1: {
0: {
0: { f: "='[Sales.xlsx]Data'!A1" },
},
},
},
});
const dependencyGenerator = get(IFormulaDependencyGenerator);
await dependencyGenerator.generate();
const trees = await dependencyGenerator.getAllDependencyJson();
expect(trees[0]?.rangeList).toContainEqual({
unitId: 'sales-source',
sheetId: 'source-sheet',
range: expect.objectContaining({
startRow: 0,
endRow: 0,
startColumn: 0,
endColumn: 0,
}),
});
});
it('resolves display-name and legacy-id external Table references', () => {
for (const formula of [
'=SUM(Sales.xlsx!SalesTable[Amount])',
'=SUM(Sales.xlsx!SalesTable[[#Data],[Amount]])',
"=SUM('Sales.xlsx'!SalesTable[Amount])",
'=SUM([sales-source]SalesTable[Amount])',
]) {
const lexerNode = lexer.treeBuilder(formula);
const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));
expect((result as BaseValueObject).getValue()).toBe(10);
}
const sectionLexerNode = lexer.treeBuilder('=SUM(Sales.xlsx!SalesTable[#Data])');
const sectionAstNode = astTreeBuilder.parse(sectionLexerNode as LexerNode);
const sectionResult = interpreter.execute(generateExecuteAstNodeData(sectionAstNode as BaseAstNode));
expect((sectionResult as BaseValueObject).getValue()).toBe(30);
});
it('stores the resolved source Unit in external Table dependency ranges', async () => {
get(IFormulaCurrentConfigService).registerFormulaData({
test: {
sheet1: {
0: {
0: { f: '=SUM(Sales.xlsx!SalesTable[Amount])' },
},
},
},
});
const dependencyGenerator = get(IFormulaDependencyGenerator);
await dependencyGenerator.generate();
const trees = await dependencyGenerator.getAllDependencyJson();
expect(trees[0]?.rangeList).toContainEqual({
unitId: 'sales-source',
sheetId: 'source-sheet',
range: expect.objectContaining({
startRow: 1,
endRow: 1,
startColumn: 0,
endColumn: 0,
}),
});
});
it('reads current target Table metadata when the AST executes', () => {
const lexerNode = lexer.treeBuilder('=SUM(Sales.xlsx!SalesTable[Amount])');
const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
get(ISuperTableService).registerTable('sales-source', 'SalesTable', {
sheetId: 'source-sheet',
titleMap: new Map([['Amount', 1]]),
range: {
startRow: 0,
endRow: 1,
startColumn: 0,
endColumn: 1,
},
});
const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));
expect((result as BaseValueObject).getValue()).toBe(20);
});
it('uses one structured-reference path for Base and Sheet host/source combinations', () => {
const currentConfig = get(IFormulaCurrentConfigService);
const tableService = get(ISuperTableService);
currentConfig.registerUnitNameMap({
test: { name: 'Host Base', unitType: UniverInstanceType.UNIVER_BASE },
'sales-source': { name: 'Sales.xlsx', unitType: UniverInstanceType.UNIVER_SHEET },
'base-source': { name: 'BaseData', unitType: UniverInstanceType.UNIVER_BASE },
});
for (const formula of ['=SUM(BaseData!BaseTable[Value])', '=SUM(Sales.xlsx!SalesTable[Amount])']) {
const result = interpreter.execute(
generateExecuteAstNodeData(
astTreeBuilder.parse(lexer.treeBuilder(formula) as LexerNode) as BaseAstNode
)
);
expect((result as BaseValueObject).getValue()).toBe(formula.includes('BaseTable') ? 7 : 10);
}
for (const formula of ["=SUM('[Sales]Data'!A1:A2)", '=SUM([Sales]Data!A1:A2)']) {
const baseToSheetA1 = interpreter.execute(
generateExecuteAstNodeData(
astTreeBuilder.parse(lexer.treeBuilder(formula) as LexerNode) as BaseAstNode
)
);
expect((baseToSheetA1 as BaseValueObject).getValue()).toBe(52);
}
currentConfig.registerUnitNameMap({
test: { name: 'Host Sheet', unitType: UniverInstanceType.UNIVER_SHEET },
'sales-source': { name: 'Sales.xlsx', unitType: UniverInstanceType.UNIVER_SHEET },
'base-source': { name: 'BaseData', unitType: UniverInstanceType.UNIVER_BASE },
});
const sheetToBase = interpreter.execute(
generateExecuteAstNodeData(
astTreeBuilder.parse(
lexer.treeBuilder('=SUM(BaseData!BaseTable[Value])') as LexerNode
) as BaseAstNode
)
);
expect((sheetToBase as BaseValueObject).getValue()).toBe(7);
const oldAst = astTreeBuilder.parse(
lexer.treeBuilder('=SUM(BaseData!BaseTable[Value])') as LexerNode
) as BaseAstNode;
tableService.remove('base-source', 'BaseTable');
tableService.registerTable('base-source', 'RenamedTable', {
sheetId: 'base-table-sheet',
titleMap: new Map([['RenamedValue', 0]]),
range: { startRow: 0, endRow: 1, startColumn: 0, endColumn: 0 },
});
expect((interpreter.execute(generateExecuteAstNodeData(oldAst)) as BaseValueObject).getValue()).toBe(
ErrorType.REF
);
const renamed = interpreter.execute(
generateExecuteAstNodeData(
astTreeBuilder.parse(
lexer.treeBuilder('=SUM(BaseData!RenamedTable[RenamedValue])') as LexerNode
) as BaseAstNode
)
);
expect((renamed as BaseValueObject).getValue()).toBe(7);
});
it('does not parse an external Table-looking string literal as a reference', () => {
const lexerNode = lexer.treeBuilder('="Sales.xlsx!SalesTable[Amount]"');
const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));
expect((result as BaseValueObject).getValue()).toBe('Sales.xlsx!SalesTable[Amount]');
});
it('uses the shared qualifier resolver for INDIRECT and preserves the target through OFFSET', () => {
for (const formula of [
'=SUM(INDIRECT("\'[Sales.xlsx]Data\'!A2"))',
'=SUM(OFFSET(INDIRECT("\'[Sales.xlsx]Data\'!A1"),1,0))',
"=SUM(OFFSET('[Sales.xlsx]Data'!A1,1,0))",
]) {
const lexerNode = lexer.treeBuilder(formula);
const astNode = astTreeBuilder.parse(lexerNode as LexerNode);
const result = interpreter.execute(generateExecuteAstNodeData(astNode as BaseAstNode));
expect((result as BaseValueObject).getValue()).toBe(10);
}
});
it('second-schedules a dynamic INDIRECT formula when the resolved source range is dirty', async () => {
const currentConfigService = get(IFormulaCurrentConfigService);
currentConfigService.registerFormulaData({
test: {
sheet1: {
0: {
0: { f: '=INDIRECT("\'[Sales.xlsx]Data\'!A2")' },
},
},
},
});
currentConfigService.loadDirtyRangesAndExcludedCell(
[
{
unitId: 'sales-source',
sheetId: 'source-sheet',
range: {
startRow: 1,
endRow: 1,
startColumn: 0,
endColumn: 0,
},
},
],
{}
);
const trees = await get(IFormulaDependencyGenerator).generate();
expect(trees).toHaveLength(1);
expect(trees[0]).toMatchObject({
unitId: 'test',
subUnitId: 'sheet1',
formula: '=INDIRECT("\'[Sales.xlsx]Data\'!A2")',
});
});
it('returns REF for missing or ambiguous cross-workbook qualifiers', () => {
const missingLexerNode = lexer.treeBuilder("='[Missing.xlsx]Data'!A1");
const missingAstNode = astTreeBuilder.parse(missingLexerNode as LexerNode);
const missingResult = interpreter.execute(generateExecuteAstNodeData(missingAstNode as BaseAstNode));
expect((missingResult as BaseValueObject).getValue()).toBe(ErrorType.REF);
get(IFormulaCurrentConfigService).registerUnitNameMap({
first: { name: 'Duplicate.xlsx', unitType: UniverInstanceType.UNIVER_SHEET },
second: { name: 'DUPLICATE.XLSX', unitType: UniverInstanceType.UNIVER_SHEET },
});
const duplicateLexerNode = lexer.treeBuilder("='[Duplicate.xlsx]Data'!A1");
const duplicateAstNode = astTreeBuilder.parse(duplicateLexerNode as LexerNode);
const duplicateResult = interpreter.execute(generateExecuteAstNodeData(duplicateAstNode as BaseAstNode));
expect((duplicateResult as BaseValueObject).getValue()).toBe(ErrorType.REF);
});
it('Name error', async () => {
const lexerNode = lexer.treeBuilder(`=sum(${ErrorType.NAME} + 1, sum(${ErrorType.REF} + 1))`);
@@ -516,7 +828,9 @@ describe('Test indirect', () => {
});
it('supports totals row arithmetic with line-break column names', async () => {
const lexerNode = lexer.treeBuilder('=Table1[[#Totals],[CASH\r\nIN]]-Table1[[#Totals],[CASH\r\nOUT]]+A1') as LexerNode;
const lexerNode = lexer.treeBuilder(
'=Table1[[#Totals],[CASH\r\nIN]]-Table1[[#Totals],[CASH\r\nOUT]]+A1'
) as LexerNode;
const astNode = astTreeBuilder.parse(lexerNode) as BaseAstNode;
const result = interpreter.execute(generateExecuteAstNodeData(astNode));
expect((result as BaseValueObject).getValue()).toStrictEqual(101);
@@ -37,6 +37,7 @@ import { IFormulaCurrentConfigService } from '../../services/current-data.servic
import { IDefinedNamesService } from '../../services/defined-names.service';
import { IFunctionService } from '../../services/function.service';
import { IFormulaRuntimeService } from '../../services/runtime.service';
import { IFormulaUnitReferenceResolver } from '../../services/unit-reference-resolver.service';
import { prefixHandler } from '../utils/prefix-handler';
import { ArrayValueObject, transformToValueObject, ValueObjectFactory } from '../value-object/array-value-object';
import { ErrorValueObject } from '../value-object/base-value-object';
@@ -51,7 +52,8 @@ export class FunctionNode extends BaseAstNode {
private _currentConfigService: IFormulaCurrentConfigService,
private _runtimeService: IFormulaRuntimeService,
private _definedNamesService: IDefinedNamesService,
private _formulaDataModel: FormulaDataModel
private _formulaDataModel: FormulaDataModel,
private _unitReferenceResolver: IFormulaUnitReferenceResolver
) {
super(token);
@@ -74,6 +76,10 @@ export class FunctionNode extends BaseAstNode {
if (this._functionExecutor.needsFormulaDataModel) {
this._functionExecutor.setFormulaDataModel(this._formulaDataModel);
}
if (this._functionExecutor.needsUnitReferenceResolver) {
this._functionExecutor.setUnitReferenceResolver(this._unitReferenceResolver);
}
}
override get nodeType() {
@@ -464,7 +470,8 @@ export class FunctionNodeFactory extends BaseAstNodeFactory {
@IFormulaRuntimeService private readonly _runtimeService: IFormulaRuntimeService,
@IDefinedNamesService private readonly _definedNamesService: IDefinedNamesService,
@Inject(Injector) private readonly _injector: Injector,
@Inject(FormulaDataModel) private readonly _formulaDataModel: FormulaDataModel
@Inject(FormulaDataModel) private readonly _formulaDataModel: FormulaDataModel,
@IFormulaUnitReferenceResolver private readonly _unitReferenceResolver: IFormulaUnitReferenceResolver
) {
super();
}
@@ -486,7 +493,8 @@ export class FunctionNodeFactory extends BaseAstNodeFactory {
this._currentConfigService,
this._runtimeService,
this._definedNamesService,
this._formulaDataModel
this._formulaDataModel,
this._unitReferenceResolver
);
}
@@ -15,6 +15,7 @@
*/
import type { Nullable } from '@univerjs/core';
import type { BaseReferenceObject } from '../reference-object/base-reference-object';
import { ErrorType } from '../../basics/error-type';
import {
regexTestReferenceTableAllColumn,
@@ -30,6 +31,7 @@ import { IFormulaCurrentConfigService } from '../../services/current-data.servic
import { IFunctionService } from '../../services/function.service';
import { IFormulaRuntimeService } from '../../services/runtime.service';
import { ISuperTableService } from '../../services/super-table.service';
import { IFormulaUnitReferenceResolver } from '../../services/unit-reference-resolver.service';
import { LexerNode } from '../analysis/lexer-node';
import { TableReferenceObject } from '../reference-object/table-reference-object';
import { prefixHandler } from '../utils/prefix-handler';
@@ -40,6 +42,12 @@ import { BaseAstNode } from './base-ast-node';
import { BaseAstNodeFactory, DEFAULT_AST_NODE_FACTORY_Z_INDEX } from './base-ast-node-factory';
import { NODE_ORDER_MAP, NodeType } from './node-type';
interface ITableReferenceDescriptor {
unitQualifier: string;
tableName: string;
columnStruct: string | undefined;
}
export class ReferenceNode extends BaseAstNode {
private _refOffsetX = 0;
private _refOffsetY = 0;
@@ -49,8 +57,10 @@ export class ReferenceNode extends BaseAstNode {
private _runtimeService: IFormulaRuntimeService,
operatorString: string,
private _referenceObjectType: ReferenceObjectType,
private _unitReferenceResolver: IFormulaUnitReferenceResolver,
private _superTableService: ISuperTableService,
private _isPrepareMerge: boolean = false,
private _tableReferenceObject?: TableReferenceObject
private _tableReference?: ITableReferenceDescriptor
) {
super(operatorString);
}
@@ -63,37 +73,50 @@ export class ReferenceNode extends BaseAstNode {
const currentConfigService = this._currentConfigService;
const runtimeService = this._runtimeService;
const referenceObject = this._tableReferenceObject || getReferenceObjectFromCache(this.getToken(), this._referenceObjectType);
let referenceObject: BaseReferenceObject;
if (this._tableReference) {
const { unitQualifier, tableName, columnStruct } = this._tableReference;
const resolution = this._unitReferenceResolver.resolve({
hostUnitId: runtimeService.currentUnitId,
qualifier: unitQualifier,
referenceKind: 'table',
});
if (typeof resolution === 'string') {
this.setValue(ErrorValueObject.create(resolution));
return;
}
const tableMap = this._superTableService.getTableMap(resolution.unitId);
const tableData = Array.from(tableMap?.entries() || []).find(([name]) => name.toLocaleLowerCase() === tableName.toLocaleLowerCase())?.[1];
if (!tableData) {
this.setValue(ErrorValueObject.create(ErrorType.REF));
return;
}
referenceObject = new TableReferenceObject(
this.getToken(),
tableData,
columnStruct,
this._superTableService.getTableOptionMap()
);
referenceObject.setUnitQualifier(unitQualifier);
referenceObject.setForcedUnitIdDirect(resolution.unitId);
} else {
referenceObject = getReferenceObjectFromCache(this.getToken(), this._referenceObjectType);
const unitQualifier = referenceObject.getUnitQualifier();
if (unitQualifier) {
const resolution = this._unitReferenceResolver.resolve({
hostUnitId: runtimeService.currentUnitId,
qualifier: unitQualifier,
referenceKind: 'a1',
});
if (typeof resolution === 'string') {
this.setValue(ErrorValueObject.create(resolution));
return;
}
referenceObject.setForcedUnitIdDirect(resolution.unitId);
}
}
referenceObject.setDefaultUnitId(runtimeService.currentUnitId);
referenceObject.setDefaultSheetId(runtimeService.currentSubUnitId);
referenceObject.setForcedSheetId(currentConfigService.getSheetNameMap());
referenceObject.setUnitData(currentConfigService.getUnitData());
referenceObject.setArrayFormulaCellData(currentConfigService.getArrayFormulaCellData());
referenceObject.setArrayFormulaRange(currentConfigService.getArrayFormulaRange());
referenceObject.setRuntimeData(runtimeService.getUnitData());
referenceObject.setUnitStylesData(currentConfigService.getUnitStylesData());
referenceObject.setRuntimeArrayFormulaCellData(runtimeService.getRuntimeArrayFormulaCellData());
referenceObject.setRuntimeArrayFormulaRange(runtimeService.getUnitArrayFormula());
referenceObject.setRuntimeFeatureCellData(runtimeService.getRuntimeFeatureCellData());
const currentRow = runtimeService.currentRow;
const currentCol = runtimeService.currentColumn;
referenceObject.setCurrentRowAndColumn(currentRow, currentCol);
const { x, y } = this.getRefOffset();
referenceObject.setRefOffset(x, y);
this._configureReferenceObject(referenceObject, currentConfigService, runtimeService);
if (!this._isPrepareMerge && referenceObject.isExceedRange()) {
this.setValue(ErrorValueObject.create(ErrorType.NAME));
@@ -102,6 +125,27 @@ export class ReferenceNode extends BaseAstNode {
}
}
private _configureReferenceObject(
referenceObject: BaseReferenceObject,
currentConfigService: IFormulaCurrentConfigService,
runtimeService: IFormulaRuntimeService
): void {
referenceObject.setDefaultUnitId(runtimeService.currentUnitId);
referenceObject.setDefaultSheetId(runtimeService.currentSubUnitId);
referenceObject.setForcedSheetId(currentConfigService.getSheetNameMap());
referenceObject.setUnitData(currentConfigService.getUnitData());
referenceObject.setArrayFormulaCellData(currentConfigService.getArrayFormulaCellData());
referenceObject.setArrayFormulaRange(currentConfigService.getArrayFormulaRange());
referenceObject.setRuntimeData(runtimeService.getUnitData());
referenceObject.setUnitStylesData(currentConfigService.getUnitStylesData());
referenceObject.setRuntimeArrayFormulaCellData(runtimeService.getRuntimeArrayFormulaCellData());
referenceObject.setRuntimeArrayFormulaRange(runtimeService.getUnitArrayFormula());
referenceObject.setRuntimeFeatureCellData(runtimeService.getRuntimeFeatureCellData());
referenceObject.setCurrentRowAndColumn(runtimeService.currentRow, runtimeService.currentColumn);
const { x, y } = this.getRefOffset();
referenceObject.setRefOffset(x, y);
}
setRefOffset(x: number = 0, y: number = 0) {
this._refOffsetX = x;
this._refOffsetY = y;
@@ -120,7 +164,8 @@ export class ReferenceNodeFactory extends BaseAstNodeFactory {
@IFormulaCurrentConfigService private readonly _currentConfigService: IFormulaCurrentConfigService,
@IFormulaRuntimeService private readonly _formulaRuntimeService: IFormulaRuntimeService,
@IFunctionService private readonly _functionService: IFunctionService,
@ISuperTableService private readonly _superTableService: ISuperTableService
@ISuperTableService private readonly _superTableService: ISuperTableService,
@IFormulaUnitReferenceResolver private readonly _unitReferenceResolver: IFormulaUnitReferenceResolver
) {
super();
}
@@ -189,12 +234,12 @@ export class ReferenceNodeFactory extends BaseAstNodeFactory {
const runtimeService = this._formulaRuntimeService;
const makeRef = (type: ReferenceObjectType) =>
new ReferenceNode(currentConfigService, runtimeService, tokenTrim, type, isPrepareMerge);
new ReferenceNode(currentConfigService, runtimeService, tokenTrim, type, this._unitReferenceResolver, this._superTableService, isPrepareMerge);
const tableMap = this._getTableMap();
const isSuperTableDirect = tableMap?.has(tokenTrim) ?? false;
if (isSuperTableDirect) {
return this._getTableReferenceNode(tokenTrim, isLexerNode, isPrepareMerge, true);
return this._getTableReferenceNode(tokenTrim, isPrepareMerge, true);
}
const isCellRange = regexTestSingeRange(tokenTrim);
@@ -213,26 +258,26 @@ export class ReferenceNodeFactory extends BaseAstNodeFactory {
return makeRef(ReferenceObjectType.COLUMN);
}
return this._getTableReferenceNode(tokenTrim, isLexerNode, isPrepareMerge, false);
return this._getTableReferenceNode(tokenTrim, isPrepareMerge, false);
}
private _getTableReferenceNode(tokenTrim: string, isLexerNode: boolean, isPrepareMerge: boolean, isSuperTableDirectly: boolean = false) {
private _getTableReferenceNode(tokenTrim: string, isPrepareMerge: boolean, isSuperTableDirectly: boolean = false) {
if (!this._checkTokenIsTableReference(tokenTrim) && !isSuperTableDirectly) {
return;
}
const { tableName, columnStruct } = splitTableStructuredRef(tokenTrim);
const { unitQualifier, tableName, columnStruct } = splitTableStructuredRef(tokenTrim);
const tableMap = this._getTableMap();
if (!isLexerNode && tableMap?.has(tableName)) {
const columnDataString = columnStruct;
const tableData = tableMap.get(tableName)!;
const tableOption = this._superTableService.getTableOptionMap();
const hasLocalTable = Array.from(tableMap?.keys() || []).some((name) => name.toLocaleLowerCase() === tableName.toLocaleLowerCase());
if (unitQualifier || hasLocalTable) {
return new ReferenceNode(
this._currentConfigService,
this._formulaRuntimeService,
tokenTrim,
ReferenceObjectType.COLUMN,
this._unitReferenceResolver,
this._superTableService,
isPrepareMerge,
new TableReferenceObject(tokenTrim, tableData, columnDataString, tableOption)
{ unitQualifier, tableName, columnStruct }
);
}
}
@@ -0,0 +1,89 @@
/**
* 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 { ObjectMatrix } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { TableOptionType } from '../../../basics/common';
import { ErrorType } from '../../../basics/error-type';
import { TableReferenceObject } from '../table-reference-object';
const options = new Map([
['#Data', TableOptionType.DATA],
['#This Row', TableOptionType.THIS_ROW],
]);
describe('TableReferenceObject current row', () => {
it('includes row zero for a headerless Base virtual table', () => {
const reference = new TableReferenceObject('Orders[[#Data],[Amount]]', {
sheetId: 'orders',
titleMap: new Map([['Amount', 0]]),
range: { startRow: 0, endRow: 1, startColumn: 0, endColumn: 0 },
showHeader: false,
}, '[[#Data],[Amount]]', options);
reference.setDefaultUnitId('base');
reference.setUnitData({
base: {
orders: {
cellData: new ObjectMatrix({ 0: { 0: { v: 10 } }, 1: { 0: { v: 20 } } }),
rowCount: 2,
columnCount: 1,
rowData: {},
columnData: {},
},
},
});
expect(reference.toArrayValueObject(false).getFirstCell().getValue()).toBe(10);
});
it('returns N/A when the host row has no corresponding target table row', () => {
const reference = new TableReferenceObject('Table[[#This Row],[Amount]]', {
sheetId: 'sheet',
titleMap: new Map([['Amount', 0]]),
range: { startRow: 0, endRow: 2, startColumn: 0, endColumn: 0 },
}, '[[#This Row],[Amount]]', options);
reference.setDefaultUnitId('unit');
reference.setCurrentRowAndColumn(4, 0);
reference.setUnitData({
unit: {
sheet: {
cellData: new ObjectMatrix({ 1: { 0: { v: 10 } }, 2: { 0: { v: 20 } } }),
rowCount: 3,
columnCount: 1,
rowData: {},
columnData: {},
},
},
});
expect(reference.toArrayValueObject(false).getFirstCell().getValue()).toBe(ErrorType.NA);
});
});
/**
* 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.
*/
@@ -64,6 +64,8 @@ export class BaseReferenceObject extends ObjectClassType {
private _forcedUnitId: string = '';
private _unitQualifier: string = '';
private _runtimeData: IRuntimeUnitDataType = {};
private _arrayFormulaCellData: IRuntimeUnitDataType = {};
@@ -253,6 +255,14 @@ export class BaseReferenceObject extends ObjectClassType {
}
}
setUnitQualifier(unitQualifier: string) {
this._unitQualifier = unitQualifier;
}
getUnitQualifier() {
return this._unitQualifier;
}
getForcedUnitId() {
return this._forcedUnitId;
}
@@ -30,6 +30,7 @@ export class CellReferenceObject extends BaseReferenceObject {
constructor(token: string) {
super(token);
const grid = deserializeRangeWithSheetWithCache(token);
this.setUnitQualifier(grid.unitId);
this.setForcedUnitIdDirect(grid.unitId);
this.setForcedSheetName(grid.sheetName);
this.setRangeData(grid.range);
@@ -26,6 +26,7 @@ export class ColumnReferenceObject extends BaseReferenceObject {
constructor(token: string) {
super(token);
const grid = deserializeRangeWithSheetWithCache(token);
this.setUnitQualifier(grid.unitId);
this.setForcedUnitIdDirect(grid.unitId);
this.setForcedSheetName(grid.sheetName);
const range: IRange = {
@@ -28,6 +28,7 @@ export class RowReferenceObject extends BaseReferenceObject {
constructor(token: string) {
super(token);
const grid = deserializeRangeWithSheetWithCache(token);
this.setUnitQualifier(grid.unitId);
this.setForcedUnitIdDirect(grid.unitId);
this.setForcedSheetName(grid.sheetName);
const range: IRange = {
@@ -14,8 +14,10 @@
* limitations under the License.
*/
import type { ICellData, Nullable } from '@univerjs/core';
import type { ISuperTable, IUnitSheetNameMap } from '../../basics/common';
import { TableOptionType } from '../../basics/common';
import { ErrorType } from '../../basics/error-type';
import { matchToken } from '../../basics/token';
import { BaseReferenceObject } from './base-reference-object';
@@ -60,6 +62,7 @@ export class TableReferenceObject extends BaseReferenceObject {
const tableStartRow = range.startRow;
const tableEndRow = range.endRow;
const dataStartRow = tableStartRow + (this._tableData.showHeader === false ? 0 : 1);
let startRow = -1;
let endRow = -1;
@@ -70,11 +73,15 @@ export class TableReferenceObject extends BaseReferenceObject {
endRow = tableEndRow;
break;
case TableOptionType.DATA:
// Default: First row is header, data area = [startRow+1, endRow]
startRow = tableStartRow + 1;
startRow = dataStartRow;
endRow = tableEndRow;
break;
case TableOptionType.HEADERS:
if (this._tableData.showHeader === false) {
startRow = -1;
endRow = -1;
break;
}
startRow = tableStartRow;
endRow = tableStartRow;
break;
@@ -89,8 +96,7 @@ export class TableReferenceObject extends BaseReferenceObject {
break;
}
default:
// Defensive: Unknown type defaults to DATA
startRow = tableStartRow + 1;
startRow = dataStartRow;
endRow = tableEndRow;
break;
}
@@ -118,6 +124,16 @@ export class TableReferenceObject extends BaseReferenceObject {
return rangeData;
}
override getCellData(row: number, column: number): Nullable<ICellData> {
if (this._isCurrentRowForRange) {
const { startRow, endRow } = this._tableData.range;
if (row < startRow || row > endRow) {
return { v: ErrorType.NA };
}
}
return super.getCellData(row, column);
}
override getRefOffset() {
return {
x: 0,
@@ -26,9 +26,38 @@ import {
needsQuoting,
serializeRange,
serializeRangeToRefString,
splitTableStructuredRef,
} from '../reference';
describe('Test Reference', () => {
it('splits local, display, OOXML and legacy Table qualifiers', () => {
expect(splitTableStructuredRef('SalesTable[Amount]')).toEqual({
unitQualifier: '',
tableName: 'SalesTable',
columnStruct: '[Amount]',
});
expect(splitTableStructuredRef('Sales.xlsx!SalesTable[Amount]')).toEqual({
unitQualifier: 'Sales.xlsx',
tableName: 'SalesTable',
columnStruct: '[Amount]',
});
expect(splitTableStructuredRef("'Customer Base'!Orders[[#Data],[Total]]")).toEqual({
unitQualifier: 'Customer Base',
tableName: 'Orders',
columnStruct: '[[#Data],[Total]]',
});
expect(splitTableStructuredRef('[1]!SalesTable[Amount]')).toEqual({
unitQualifier: '1',
tableName: 'SalesTable',
columnStruct: '[Amount]',
});
expect(splitTableStructuredRef('[runtime-id]SalesTable[Amount]')).toEqual({
unitQualifier: 'runtime-id',
tableName: 'SalesTable',
columnStruct: '[Amount]',
});
});
it('getAbsoluteRefTypeWithSingleString', () => {
expect(getAbsoluteRefTypeWithSingleString('A4')).toEqual(AbsoluteRefType.NONE);
@@ -330,29 +359,34 @@ describe('Test Reference', () => {
expect(handleRefStringInfo('A1:A2')).toStrictEqual({
refBody: 'A1:A2',
sheetName: '',
unitQualifier: '',
unitId: '',
});
expect(handleRefStringInfo('sheet1!A1')).toStrictEqual({
refBody: 'A1',
sheetName: 'sheet1',
unitQualifier: '',
unitId: '',
});
expect(handleRefStringInfo('[Book1]Sheet1!A1')).toStrictEqual({
refBody: 'A1',
sheetName: 'Sheet1',
unitQualifier: 'Book1',
unitId: 'Book1',
});
expect(handleRefStringInfo("'[Book1]Sheet1'!R2C3")).toStrictEqual({
refBody: 'R2C3',
sheetName: 'Sheet1',
unitQualifier: 'Book1',
unitId: 'Book1',
});
expect(handleRefStringInfo("'sheet-1'!A1")).toStrictEqual({
refBody: 'A1',
sheetName: 'sheet-1',
unitQualifier: '',
unitId: '',
});
@@ -360,6 +394,7 @@ describe('Test Reference', () => {
expect(handleRefStringInfo("'sheet''1'!A1")).toStrictEqual({
refBody: 'A1',
sheetName: "sheet'1",
unitQualifier: '',
unitId: '',
});
@@ -367,12 +402,14 @@ describe('Test Reference', () => {
expect(handleRefStringInfo("'sheet''''1'!A1")).toStrictEqual({
refBody: 'A1',
sheetName: "sheet''1",
unitQualifier: '',
unitId: '',
});
expect(handleRefStringInfo("'[Book-1.xlsx]Sheet1'!$A$4")).toStrictEqual({
refBody: '$A$4',
sheetName: 'Sheet1',
unitQualifier: 'Book-1.xlsx',
unitId: 'Book-1.xlsx',
});
@@ -380,6 +417,7 @@ describe('Test Reference', () => {
expect(handleRefStringInfo("'[Book''1.xlsx]Sheet1'!$A$4")).toStrictEqual({
refBody: '$A$4',
sheetName: 'Sheet1',
unitQualifier: "Book'1.xlsx",
unitId: "Book'1.xlsx",
});
@@ -387,12 +425,14 @@ describe('Test Reference', () => {
expect(handleRefStringInfo("'[Book''''1.xlsx]Sheet1'!$A$4")).toStrictEqual({
refBody: '$A$4',
sheetName: 'Sheet1',
unitQualifier: "Book''1.xlsx",
unitId: "Book''1.xlsx",
});
expect(handleRefStringInfo("'[Book-1.xlsx]sheet-1'!$A$4")).toStrictEqual({
refBody: '$A$4',
sheetName: 'sheet-1',
unitQualifier: 'Book-1.xlsx',
unitId: 'Book-1.xlsx',
});
});
@@ -0,0 +1,59 @@
/**
* 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 { describe, expect, it } from 'vitest';
import { refactorFormulaUnitQualifier } from '../unit-qualifier';
describe('refactorFormulaUnitQualifier', () => {
it('updates A1 and Table qualifiers without touching similar names', () => {
expect(refactorFormulaUnitQualifier(
"=SUM([Sales.xlsx]Data!A1,'[Sales.xlsx]Q 1'!B2,Sales.xlsx!SalesTable[Amount],Sales.xlsx.bak!T[V])",
'Sales.xlsx',
'FY 2027.xlsx'
)).toBe("=SUM([FY 2027.xlsx]Data!A1,'[FY 2027.xlsx]Q 1'!B2,'FY 2027.xlsx'!SalesTable[Amount],Sales.xlsx.bak!T[V])");
});
it('updates INDIRECT reference literals but leaves ordinary string literals unchanged', () => {
expect(refactorFormulaUnitQualifier(
'=INDIRECT("[Sales.xlsx]Data!A1")&"[Sales.xlsx]Data!A1"',
'Sales.xlsx',
'Costs.xlsx'
)).toBe('=INDIRECT("[Costs.xlsx]Data!A1")&"[Sales.xlsx]Data!A1"');
});
it('preserves bracketed and quoted structured-reference styles', () => {
expect(refactorFormulaUnitQualifier(
"=SUM([Sales.xlsx]!T[A])+SUM('Sales.xlsx'!T[B])",
'sales.XLSX',
"Director's Plan"
)).toBe("=SUM([Director's Plan]!T[A])+SUM('Director''s Plan'!T[B])");
});
});
/**
* 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.
*/
@@ -50,7 +50,7 @@ function singleReference(refBody: string, currentRow = 0, currentColumn = 0) {
}
export function deserializeRangeForR1C1(refString: string, currentRow = 0, currentColumn = 0): IUnitRangeName {
const { refBody, sheetName, unitId } = handleRefStringInfo(refString);
const { refBody, sheetName, unitQualifier } = handleRefStringInfo(refString);
const colonIndex = refBody.indexOf(':');
@@ -78,7 +78,7 @@ export function deserializeRangeForR1C1(refString: string, currentRow = 0, curre
};
return {
unitId,
unitId: unitQualifier,
sheetName,
@@ -103,7 +103,7 @@ export function deserializeRangeForR1C1(refString: string, currentRow = 0, curre
const endColumn = endGrid.column;
return {
unitId,
unitId: unitQualifier,
sheetName,
@@ -198,11 +198,11 @@ export function singleReferenceToGrid(refBody: string) {
export function handleRefStringInfo(refString: string) {
const unitIdMatch = UNIT_NAME_REGEX_PRECOMPILING.exec(refString);
let unitId = '';
let unitQualifier = '';
if (unitIdMatch != null) {
unitId = unitIdMatch[0].trim();
unitId = unquoteSheetName(unitId.slice(1, unitId.length - 1));
unitQualifier = unitIdMatch[0].trim();
unitQualifier = unquoteSheetName(unitQualifier.slice(1, unitQualifier.length - 1));
refString = refString.replace(UNIT_NAME_REGEX_PRECOMPILING, '');
}
@@ -224,12 +224,14 @@ export function handleRefStringInfo(refString: string) {
return {
refBody,
sheetName,
unitId,
unitQualifier,
/** @deprecated Use unitQualifier. Kept for reference-grid compatibility. */
unitId: unitQualifier,
};
}
export function deserializeRangeWithSheet(refString: string): IUnitRangeName {
const { refBody, sheetName, unitId } = handleRefStringInfo(refString);
const { refBody, sheetName, unitQualifier } = handleRefStringInfo(refString);
const colonIndex = refBody.indexOf(':');
@@ -248,7 +250,7 @@ export function deserializeRangeWithSheet(refString: string): IUnitRangeName {
};
return {
unitId,
unitId: unitQualifier,
sheetName,
range,
};
@@ -275,7 +277,7 @@ export function deserializeRangeWithSheet(refString: string): IUnitRangeName {
}
return {
unitId,
unitId: unitQualifier,
sheetName,
range: {
startRow,
@@ -477,12 +479,55 @@ function startsWithNonAlphabetic(name: string) {
}
export function splitTableStructuredRef(ref: string) {
const idx = ref.indexOf('[');
let unitQualifier = '';
let tableRef = ref.trim();
let quoteOpen = false;
let bracketDepth = 0;
let qualifierEnd = -1;
for (let i = 0; i < tableRef.length; i++) {
const char = tableRef[i];
if (char === "'") {
if (quoteOpen && tableRef[i + 1] === "'") {
i++;
continue;
}
quoteOpen = !quoteOpen;
} else if (!quoteOpen && char === '[') {
bracketDepth++;
} else if (!quoteOpen && char === ']') {
bracketDepth--;
} else if (!quoteOpen && bracketDepth === 0 && char === '!') {
qualifierEnd = i;
break;
}
}
if (qualifierEnd >= 0) {
unitQualifier = tableRef.slice(0, qualifierEnd).trim();
tableRef = tableRef.slice(qualifierEnd + 1);
if (unitQualifier.startsWith("'") && unitQualifier.endsWith("'")) {
unitQualifier = unitQualifier.slice(1, -1);
}
if (unitQualifier.startsWith('[') && unitQualifier.endsWith(']')) {
unitQualifier = unitQualifier.slice(1, -1);
}
unitQualifier = unquoteSheetName(unitQualifier);
} else if (tableRef.startsWith('[')) {
const legacyQualifierEnd = tableRef.indexOf(']');
if (legacyQualifierEnd > 0) {
unitQualifier = unquoteSheetName(tableRef.slice(1, legacyQualifierEnd));
tableRef = tableRef.slice(legacyQualifierEnd + 1);
}
}
const idx = tableRef.indexOf('[');
if (idx === -1) {
return { tableName: ref, struct: '' };
return { unitQualifier, tableName: tableRef, columnStruct: '' };
}
return {
tableName: ref.slice(0, idx),
columnStruct: ref.slice(idx), // include [[...]]
unitQualifier,
tableName: tableRef.slice(0, idx),
columnStruct: tableRef.slice(idx), // include [[...]]
};
}
@@ -0,0 +1,96 @@
/**
* 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.
*/
const A1_UNIT_QUALIFIER = /('?)\[([^\]]+)\]((?:[^']|'')+)\1!(?=\$?[A-Z]{1,3}\$?\d+)/gi;
const TABLE_UNIT_QUALIFIER = /(^|[^\w.])(?:'((?:[^']|'')+)'|\[([^\]]+)\]|([A-Za-z0-9_.-]+))!(?=[^\s!\[\]]+\[)/g;
const INDIRECT_LITERAL = /\bINDIRECT\s*\(\s*"((?:[^"]|"")*)"/gi;
function equalsQualifier(actual: string, expected: string): boolean {
return actual.replace(/''/g, "'").toLocaleLowerCase() === expected.toLocaleLowerCase();
}
function quoteQualifier(name: string): string {
return /^[A-Za-z0-9_.-]+$/.test(name) ? name : `'${name.replace(/'/g, "''")}'`;
}
function refactorReferenceSegment(segment: string, oldName: string, newName: string): string {
const withA1 = segment.replace(
A1_UNIT_QUALIFIER,
(token, quote: string, qualifier: string, sheetName: string) => {
if (!equalsQualifier(qualifier, oldName)) return token;
const escapedName = newName.replace(/'/g, "''");
return quote ? `'[${escapedName}]${sheetName}'!` : `[${newName}]${sheetName}!`;
}
);
return withA1.replace(
TABLE_UNIT_QUALIFIER,
(token, boundary: string, quoted: string | undefined, bracketed: string | undefined, plain: string | undefined) => {
const qualifier = quoted ?? bracketed ?? plain ?? '';
if (!equalsQualifier(qualifier, oldName)) return token;
if (quoted != null) return `${boundary}'${newName.replace(/'/g, "''")}'!`;
if (bracketed != null) return `${boundary}[${newName}]!`;
return `${boundary}${quoteQualifier(newName)}!`;
}
);
}
function refactorOutsideStrings(formula: string, oldName: string, newName: string): string {
let result = '';
let chunk = '';
let inString = false;
for (let index = 0; index < formula.length; index++) {
const character = formula[index];
if (character !== '"') {
chunk += character;
continue;
}
if (inString && formula[index + 1] === '"') {
chunk += '""';
index++;
continue;
}
result += inString ? chunk : refactorReferenceSegment(chunk, oldName, newName);
result += '"';
chunk = '';
inString = !inString;
}
return result + (inString ? chunk : refactorReferenceSegment(chunk, oldName, newName));
}
/** Refactors only parsed reference qualifiers and INDIRECT literal references, never arbitrary text. */
export function refactorFormulaUnitQualifier(formula: string, oldName: string, newName: string): string {
if (!oldName || oldName === newName) return formula;
const withIndirect = formula.replace(INDIRECT_LITERAL, (token, literal: string) => {
const next = refactorReferenceSegment(literal.replace(/""/g, '"'), oldName, newName).replace(/"/g, '""');
return token.replace(literal, next);
});
return refactorOutsideStrings(withIndirect, oldName, newName);
}
/**
* 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.
*/
@@ -56,6 +56,7 @@ import { IOtherFormulaManagerService, OtherFormulaManagerService } from '../../s
import { FormulaRuntimeService, IFormulaRuntimeService } from '../../services/runtime.service';
import { ISheetRowFilteredService, SheetRowFilteredService } from '../../services/sheet-row-filtered.service';
import { ISuperTableService, SuperTableService } from '../../services/super-table.service';
import { FormulaUnitReferenceResolver, IFormulaUnitReferenceResolver } from '../../services/unit-reference-resolver.service';
const getTestWorkbookData = (): IWorkbookData => {
return {
@@ -176,6 +177,7 @@ export function createFunctionTestBed(workbookData?: IWorkbookData, dependencies
injector.add([LexerTreeBuilder]);
injector.add([IFormulaCurrentConfigService, { useClass: FormulaCurrentConfigService }]);
injector.add([IFormulaUnitReferenceResolver, { useClass: FormulaUnitReferenceResolver }]);
injector.add([IHyperlinkEngineFormulaService, { useClass: HyperlinkEngineFormulaService }]);
injector.add([IFormulaRuntimeService, { useClass: FormulaRuntimeService }]);
injector.add([IFunctionService, { useClass: FunctionService }]);
@@ -28,6 +28,7 @@ import type { BaseValueObject } from '../engine/value-object/base-value-object';
import type { FormulaFunctionResultValueType, FormulaFunctionValueType } from '../engine/value-object/primitive-object';
import type { FormulaDataModel } from '../models/formula-data.model';
import type { IDefinedNameMapItem } from '../services/defined-names.service';
import type { IFormulaUnitReferenceResolver } from '../services/unit-reference-resolver.service';
import { ErrorType } from '../basics/error-type';
import { regexTestSingeRange, regexTestSingleColumn, regexTestSingleRow } from '../basics/regex';
import { compareToken } from '../basics/token';
@@ -54,6 +55,7 @@ export class BaseFunction {
private _sheetOrder: string[];
private _sheetNameMap: { [sheetId: string]: string };
protected _formulaDataModel: Nullable<FormulaDataModel>;
protected _unitReferenceResolver: Nullable<IFormulaUnitReferenceResolver>;
protected _rowCount: number = -1;
protected _columnCount: number = -1;
@@ -82,6 +84,9 @@ export class BaseFunction {
*/
needsFormulaDataModel: boolean = false;
/** Whether the function resolves external Unit qualifiers. */
needsUnitReferenceResolver: boolean = false;
/**
* Whether the function needs the number of rows and columns in the sheet
*/
@@ -200,6 +205,10 @@ export class BaseFunction {
this._formulaDataModel = _formulaDataModel;
}
setUnitReferenceResolver(unitReferenceResolver: IFormulaUnitReferenceResolver) {
this._unitReferenceResolver = unitReferenceResolver;
}
setSheetRowColumnCount(rowCount: number, columnCount: number) {
this._rowCount = rowCount;
this._columnCount = columnCount;
@@ -47,6 +47,8 @@ export class Indirect extends BaseFunction {
override maxParams = 2;
override needsUnitReferenceResolver = true;
override isAddress() {
return true;
}
@@ -106,6 +108,7 @@ export class Indirect extends BaseFunction {
const rangeReferenceObject = new RangeReferenceObject(range);
rangeReferenceObject.setUnitQualifier(unitId);
rangeReferenceObject.setForcedUnitIdDirect(unitId);
rangeReferenceObject.setForcedSheetName(sheetName);
@@ -133,6 +136,7 @@ export class Indirect extends BaseFunction {
const rangeReferenceObject = new RangeReferenceObject(range);
rangeReferenceObject.setUnitQualifier(unitId);
rangeReferenceObject.setForcedUnitIdDirect(unitId);
rangeReferenceObject.setForcedSheetName(sheetName);
@@ -143,6 +147,18 @@ export class Indirect extends BaseFunction {
if (this.unitId == null || this.subUnitId == null) {
return ErrorValueObject.create(ErrorType.REF);
}
const unitQualifier = object.getUnitQualifier();
if (unitQualifier && this._unitReferenceResolver) {
const resolution = this._unitReferenceResolver.resolve({
hostUnitId: this.unitId,
qualifier: unitQualifier,
referenceKind: 'a1',
});
if (typeof resolution === 'string') {
return ErrorValueObject.create(resolution);
}
object.setForcedUnitIdDirect(resolution.unitId);
}
object.setDefaultUnitId(this.unitId);
object.setDefaultSheetId(this.subUnitId);
return object;
+14
View File
@@ -15,6 +15,7 @@
*/
export type {
FormulaUnitType,
IArrayFormulaEmbeddedMap,
IArrayFormulaRangeType,
IArrayFormulaUnitCellType,
@@ -29,6 +30,8 @@ export type {
IFormulaDatasetConfig,
IFormulaExecuteResultMap,
IFormulaStringMap,
IFormulaUnitNameMap,
IFormulaUnitNameMapItem,
IRuntimeImageFormulaDataType,
IRuntimeUnitDataType,
ISheetData,
@@ -200,6 +203,7 @@ export { handleRefStringInfo } from './engine/utils/reference';
export { deserializeRangeWithSheetWithCache } from './engine/utils/reference-cache';
export { generateStringWithSequence, sequenceNodeType } from './engine/utils/sequence';
export type { ISequenceNode } from './engine/utils/sequence';
export { refactorFormulaUnitQualifier } from './engine/utils/unit-qualifier';
export { ArrayValueObject, ValueObjectFactory } from './engine/value-object/array-value-object';
export { BaseValueObject, ErrorValueObject } from './engine/value-object/base-value-object';
export { LambdaValueObjectObject } from './engine/value-object/lambda-value-object';
@@ -296,3 +300,13 @@ export type { IAllRuntimeData, IExecutionInProgressParams } from './services/run
export { ISheetRowFilteredService, SheetRowFilteredService } from './services/sheet-row-filtered.service';
export { ISuperTableService } from './services/super-table.service';
export { SuperTableService } from './services/super-table.service';
export {
FormulaUnitReferenceResolver,
IFormulaUnitReferenceResolver,
normalizeFormulaUnitName,
} from './services/unit-reference-resolver.service';
export type {
FormulaUnitReferenceKind,
IFormulaUnitReferenceResolution,
IFormulaUnitReferenceResolveInput,
} from './services/unit-reference-resolver.service';
@@ -1027,6 +1027,10 @@ describe('Test formula data model', () => {
const calculateData = formulaDataModel.getCalculateData();
expect(calculateData.allUnitData.test?.sheet1.rowCount).toBeGreaterThan(0);
expect(calculateData.unitSheetNameMap.test?.Sheet1).toBe('sheet1');
expect(calculateData.unitNameMap.test).toEqual({
name: '',
unitType: UniverInstanceType.UNIVER_SHEET,
});
const sheetFormulaData = formulaDataModel.getSheetFormulaData('test', 'sheet1');
expect(sheetFormulaData?.[0]?.[0]?.f).toBe('=A1');
@@ -1060,6 +1064,10 @@ describe('Test formula data model', () => {
const calculateData = formulaDataModel.getCalculateData();
const tableData = calculateData.allUnitData['base-test']?.['table-main'];
expect(calculateData.unitNameMap['base-test']).toEqual({
name: 'Base',
unitType: UniverInstanceType.UNIVER_BASE,
});
expect(calculateData.unitSheetNameMap['base-test']?.Sales).toBe('tableOther');
expect(tableData?.rowCount).toBe(1);
expect(tableData?.columnCount).toBe(7);
@@ -1069,6 +1077,21 @@ describe('Test formula data model', () => {
expect(tableData?.cellData.getValue(0, 4)).toEqual({ v: 'Invoice', t: CellValueType.STRING });
expect(tableData?.cellData.getValue(0, 5)).toEqual({ v: '', t: CellValueType.STRING });
});
it('should preserve workbook-qualified A1 references in Base formulas', () => {
const univerInstanceService = get(IUniverInstanceService);
univerInstanceService.registerCtorForType(UniverInstanceType.UNIVER_BASE, BaseDataModel);
const snapshot = structuredClone(TEST_BASE_DATA);
snapshot.id = 'base-external-a1';
snapshot.tables!['table-main'].fields.total.config = {
formula: '=SUM([Host.xlsx]Sheet1!$A$1:$B$10)',
};
univer.createUnit(UniverInstanceType.UNIVER_BASE, snapshot);
expect(formulaDataModel.getFormulaData()['base-external-a1']?.['table-main']?.[0]?.[6]?.f).toBe(
'=SUM([Host.xlsx]Sheet1!$A$1:$B$10)'
);
});
});
});
@@ -21,6 +21,7 @@ import type {
IFormulaData,
IFormulaDataItem,
IFormulaIdMap,
IFormulaUnitNameMap,
IRuntimeUnitDataType,
ISheetData,
IUnitData,
@@ -356,9 +357,16 @@ export class FormulaDataModel extends Disposable {
const unitSheetNameMap: IUnitSheetNameMap = {};
const unitNameMap: IFormulaUnitNameMap = {};
for (const workbook of unitAllSheet) {
const unitId = workbook.getUnitId();
unitNameMap[unitId] = {
name: workbook.name,
unitType: UniverInstanceType.UNIVER_SHEET,
};
const sheets = workbook.getSheets();
const sheetData: ISheetData = {};
@@ -392,6 +400,10 @@ export class FormulaDataModel extends Disposable {
for (const base of unitAllBases) {
const snapshot = base.getSnapshot();
const unitId = base.getUnitId();
unitNameMap[unitId] = {
name: snapshot.name,
unitType: UniverInstanceType.UNIVER_BASE,
};
const baseData: ISheetData = {};
const tableNameMap: { [tableName: string]: string } = {};
@@ -415,6 +427,7 @@ export class FormulaDataModel extends Disposable {
allUnitData,
unitStylesData,
unitSheetNameMap,
unitNameMap,
};
}
@@ -926,6 +939,7 @@ export function initSheetFormulaData(
const BASE_LEGACY_FIELD_REF_PATTERN = /\{([^}]+)\}/g;
const BASE_TABLE_FIELD_REF_PATTERN = /\b([A-Z_]\w*)\[([^\]]+)\]/gi;
const BASE_BRACKET_FIELD_REF_PATTERN = /(^|[^A-Za-z0-9_\]\[])\[([^\]]+)\]/g;
const BASE_EXTERNAL_A1_REF_PATTERN = /(?:'\[[^\]]+\](?:[^']|'')+'|\[[^\]]+\][^\s'!]+)!\$?[A-Z]{1,3}\$?\d+(?::\$?[A-Z]{1,3}\$?\d+)?/gi;
function normalizeBaseFormulaForEngine(formula: string, currentTable: ITableSnapshot, snapshot: IBaseSnapshot): string {
const refs: string[] = [];
@@ -934,6 +948,7 @@ function normalizeBaseFormulaForEngine(formula: string, currentTable: ITableSnap
return `__BASE_FORMULA_REF_${index}__`;
};
const normalized = formula
.replace(BASE_EXTERNAL_A1_REF_PATTERN, (reference) => hold(reference))
.replace(BASE_LEGACY_FIELD_REF_PATTERN, (_match, fieldName: string) => hold(createEngineThisRowRef(currentTable, fieldName, snapshot)))
.replace(BASE_TABLE_FIELD_REF_PATTERN, (_match, sourceTableName: string, fieldName: string) => {
const targetTable = resolveBaseFormulaTable(sourceTableName, currentTable, snapshot);
+2
View File
@@ -65,6 +65,7 @@ import { RegisterOtherFormulaService } from './services/register-other-formula.s
import { FormulaRuntimeService, IFormulaRuntimeService } from './services/runtime.service';
import { ISheetRowFilteredService, SheetRowFilteredService } from './services/sheet-row-filtered.service';
import { ISuperTableService, SuperTableService } from './services/super-table.service';
import { FormulaUnitReferenceResolver, IFormulaUnitReferenceResolver } from './services/unit-reference-resolver.service';
export class UniverFormulaEnginePlugin extends Plugin {
static override pluginName = 'UNIVER_ENGINE_FORMULA_PLUGIN';
@@ -184,6 +185,7 @@ export class UniverFormulaEnginePlugin extends Plugin {
[ICalculateFormulaService, { useClass: CalculateFormulaService }],
[IDependencyManagerService, { useClass: DependencyManagerService }],
[IFormulaDependencyGenerator, { useClass: FormulaDependencyGenerator }],
[IFormulaUnitReferenceResolver, { useClass: FormulaUnitReferenceResolver }],
];
dependencies.forEach((dependency) => this._injector.add(dependency));
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { Injector, IUniverInstanceService, LocaleService, ObjectMatrix } from '@univerjs/core';
import { Injector, IUniverInstanceService, LocaleService, ObjectMatrix, UniverInstanceType } from '@univerjs/core';
import { describe, expect, it, vi } from 'vitest';
import { FormulaDataModel } from '../../models/formula-data.model';
import { FormulaCurrentConfigService, IFormulaCurrentConfigService } from '../current-data.service';
@@ -43,6 +43,7 @@ function createService() {
const formulaDataModel = {
getCalculateData: vi.fn(() => ({
allUnitData: {},
unitNameMap: {},
unitSheetNameMap: {},
unitStylesData: {},
})),
@@ -114,6 +115,12 @@ describe('FormulaCurrentConfigService', () => {
SheetA: 'sheetA',
},
} as never,
unitNameMap: {
unitA: {
name: 'Sales.xlsx',
unitType: UniverInstanceType.UNIVER_SHEET,
},
},
formulaData: { unitA: { sheetA: {} } } as never,
arrayFormulaCellData: {},
arrayFormulaRange: {},
@@ -132,6 +139,12 @@ describe('FormulaCurrentConfigService', () => {
expect(service.getUnitData().unitA.sheetB.rowData).toEqual({ 0: { h: 20 } });
expect(service.getSheetName('unitA', 'sheetA')).toBe('SheetA');
expect(service.getSheetName('unitA', 'sheetB')).toBe('SheetB');
expect(service.getUnitNameMap()).toEqual({
unitA: {
name: 'Sales.xlsx',
unitType: UniverInstanceType.UNIVER_SHEET,
},
});
expect(service.getClearDependencyTreeCache()).toEqual({ unitA: { sheetA: 'SheetA' } });
expect(service.getDirtyData()).toEqual(expect.objectContaining({
forceCalculation: true,
@@ -156,6 +169,12 @@ describe('FormulaCurrentConfigService', () => {
},
},
unitStylesData: { 'unit-current': {} },
unitNameMap: {
'unit-current': {
name: 'Current.xlsx',
unitType: UniverInstanceType.UNIVER_SHEET,
},
},
unitSheetNameMap: { 'unit-current': { Main: 'sheet-current' } },
});
@@ -175,6 +194,7 @@ describe('FormulaCurrentConfigService', () => {
expect(service.getExecuteUnitId()).toBe('unit-current');
expect(service.getExecuteSubUnitId()).toBe('sheet-current');
expect(service.getUnitNameMap()['unit-current']?.name).toBe('Current.xlsx');
expect(service.getSheetsInfo()).toEqual({
sheetOrder: ['sheet-current'],
sheetNameMap: { 'sheet-current': 'Main' },
@@ -204,6 +224,7 @@ describe('FormulaCurrentConfigService', () => {
formulaDataModel.getCalculateData.mockReturnValue({
allUnitData: {},
unitStylesData: {},
unitNameMap: {},
unitSheetNameMap: {},
});
formulaDataModel.getFormulaData.mockReturnValue({ unitLite: { sheetLite: { 1: { 1: { f: '=A1' } } } } });
@@ -221,6 +242,12 @@ describe('FormulaCurrentConfigService', () => {
service.registerUnitData({ unit: { sheet: { cellData: new ObjectMatrix({}), rowCount: 1, columnCount: 1, rowData: {}, columnData: {} } } } as never);
service.registerFormulaData({ unit: { sheet: { 1: { 1: { f: '=1' } } } } } as never);
service.registerSheetNameMap({ unit: { Sheet: 'sheet' } } as never);
service.registerUnitNameMap({
unit: {
name: 'Book.xlsx',
unitType: UniverInstanceType.UNIVER_SHEET,
},
});
service.loadDirtyRangesAndExcludedCell(
[{ unitId: 'unit', sheetId: 'sheet', range: { startRow: 0, endRow: 0, startColumn: 0, endColumn: 0 } }],
{ unit: { sheet: { 0: { 0: true } } } } as never
@@ -235,6 +262,7 @@ describe('FormulaCurrentConfigService', () => {
expect(service.getUnitData()).toEqual({});
expect(service.getFormulaData()).toEqual({});
expect(service.getSheetNameMap()).toEqual({});
expect(service.getUnitNameMap()).toEqual({});
expect(service.getExcludedRange()).toEqual({});
});
});
@@ -0,0 +1,170 @@
/**
* 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 { UniverInstanceType } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { ErrorType } from '../../basics/error-type';
import { FormulaUnitReferenceResolver } from '../unit-reference-resolver.service';
function createResolver(
unitNameMap: Record<
string,
{ name: string; unitType: UniverInstanceType.UNIVER_SHEET | UniverInstanceType.UNIVER_BASE }
>,
unitData: Record<string, unknown> = {}
) {
return new FormulaUnitReferenceResolver({
getUnitNameMap: () => unitNameMap,
getUnitData: () => unitData,
} as never);
}
describe('FormulaUnitReferenceResolver', () => {
it('keeps runtime unit ids compatible and resolves empty qualifiers to the host', () => {
const resolver = createResolver({
host: { name: 'Host.xlsx', unitType: UniverInstanceType.UNIVER_SHEET },
});
expect(resolver.resolve({ hostUnitId: 'host', qualifier: '', referenceKind: 'a1' })).toEqual({
unitId: 'host',
unitType: UniverInstanceType.UNIVER_SHEET,
});
expect(resolver.resolve({ hostUnitId: 'other', qualifier: 'host', referenceKind: 'a1' })).toEqual({
unitId: 'host',
unitType: UniverInstanceType.UNIVER_SHEET,
});
});
it('resolves Sheet and Base display names case-insensitively', () => {
const resolver = createResolver({
sheet: { name: 'Sales.xlsx', unitType: UniverInstanceType.UNIVER_SHEET },
base: { name: 'Customer Base', unitType: UniverInstanceType.UNIVER_BASE },
});
expect(resolver.resolve({ hostUnitId: 'host', qualifier: 'sales.XLSX', referenceKind: 'a1' })).toEqual({
unitId: 'sheet',
unitType: UniverInstanceType.UNIVER_SHEET,
});
expect(resolver.resolve({ hostUnitId: 'host', qualifier: 'customer base', referenceKind: 'table' })).toEqual({
unitId: 'base',
unitType: UniverInstanceType.UNIVER_BASE,
});
});
it('resolves imported workbook names without their Excel file extension', () => {
const resolver = createResolver({
sheet: { name: 'Sales 2026', unitType: UniverInstanceType.UNIVER_SHEET },
base: { name: 'Customer Base', unitType: UniverInstanceType.UNIVER_BASE },
unicode: { name: 'ÉTÉ 2026', unitType: UniverInstanceType.UNIVER_SHEET },
});
expect(resolver.resolve({ hostUnitId: 'host', qualifier: 'sales 2026.XLSX', referenceKind: 'a1' })).toEqual({
unitId: 'sheet',
unitType: UniverInstanceType.UNIVER_SHEET,
});
expect(
resolver.resolve({
hostUnitId: 'host',
qualifier: 'customer base.xls',
referenceKind: 'table',
})
).toEqual({
unitId: 'base',
unitType: UniverInstanceType.UNIVER_BASE,
});
expect(resolver.resolve({ hostUnitId: 'host', qualifier: 'sales 2026.xlsm', referenceKind: 'a1' })).toEqual({
unitId: 'sheet',
unitType: UniverInstanceType.UNIVER_SHEET,
});
expect(resolver.resolve({ hostUnitId: 'host', qualifier: 'été 2026.xlsx', referenceKind: 'a1' })).toEqual({
unitId: 'unicode',
unitType: UniverInstanceType.UNIVER_SHEET,
});
});
it('prefers an exact workbook name before extension-compatible aliases', () => {
const resolver = createResolver({
imported: { name: 'Sales', unitType: UniverInstanceType.UNIVER_SHEET },
explicit: { name: 'Sales.xlsx', unitType: UniverInstanceType.UNIVER_SHEET },
});
expect(resolver.resolve({ hostUnitId: 'host', qualifier: 'Sales.xlsx', referenceKind: 'a1' })).toMatchObject({
unitId: 'explicit',
});
});
it('does not treat empty names as aliases', () => {
const resolver = createResolver({
unnamed: { name: '', unitType: UniverInstanceType.UNIVER_SHEET },
});
expect(resolver.resolve({ hostUnitId: 'host', qualifier: '', referenceKind: 'a1' })).toBe(ErrorType.REF);
});
it('returns REF for missing or ambiguous names', () => {
const resolver = createResolver({
first: { name: 'Sales.xlsx', unitType: UniverInstanceType.UNIVER_SHEET },
second: { name: 'SALES.XLSX', unitType: UniverInstanceType.UNIVER_BASE },
});
expect(resolver.resolve({ hostUnitId: 'host', qualifier: 'missing.xlsx', referenceKind: 'a1' })).toBe(
ErrorType.REF
);
expect(resolver.resolve({ hostUnitId: 'host', qualifier: 'sales.xlsx', referenceKind: 'table' })).toBe(
ErrorType.REF
);
});
it('returns REF for ambiguous extension-compatible aliases', () => {
const resolver = createResolver({
first: { name: 'Sales', unitType: UniverInstanceType.UNIVER_SHEET },
second: { name: 'SALES', unitType: UniverInstanceType.UNIVER_BASE },
});
expect(resolver.resolve({ hostUnitId: 'host', qualifier: 'sales.xlsx', referenceKind: 'table' })).toBe(
ErrorType.REF
);
});
it('accepts synthetic runtime ids that only exist in unit data', () => {
const resolver = createResolver({}, { 'external:1': {} });
expect(resolver.resolve({ hostUnitId: 'host', qualifier: 'external:1', referenceKind: 'a1' })).toEqual({
unitId: 'external:1',
unitType: undefined,
});
});
it('allows Base-to-Sheet A1 but keeps A1 references to Base forbidden', () => {
const resolver = createResolver({
sheet: { name: 'Sales.xlsx', unitType: UniverInstanceType.UNIVER_SHEET },
base: { name: 'Customer Base', unitType: UniverInstanceType.UNIVER_BASE },
});
expect(resolver.resolve({ hostUnitId: 'sheet', qualifier: 'Customer Base', referenceKind: 'a1' })).toBe(
ErrorType.REF
);
expect(resolver.resolve({ hostUnitId: 'base', qualifier: 'Sales.xlsx', referenceKind: 'a1' })).toEqual({
unitId: 'sheet',
unitType: UniverInstanceType.UNIVER_SHEET,
});
expect(resolver.resolve({ hostUnitId: 'base', qualifier: 'Sales.xlsx', referenceKind: 'table' })).toEqual({
unitId: 'sheet',
unitType: UniverInstanceType.UNIVER_SHEET,
});
});
});
@@ -24,6 +24,7 @@ import type {
IDirtyUnitSuperTableMap,
IFormulaData,
IFormulaDatasetConfig,
IFormulaUnitNameMap,
IRuntimeUnitDataType,
IUnitData,
IUnitExcludedCell,
@@ -73,6 +74,8 @@ export interface IFormulaCurrentConfigService {
getSheetNameMap(): IUnitSheetNameMap;
getUnitNameMap(): IFormulaUnitNameMap;
isForceCalculate(): boolean;
getDirtyRanges(): IUnitRange[];
@@ -91,6 +94,8 @@ export interface IFormulaCurrentConfigService {
registerSheetNameMap(sheetNameMap: IUnitSheetNameMap): void;
registerUnitNameMap(unitNameMap: IFormulaUnitNameMap): void;
getExcludedRange(): Nullable<IUnitExcludedCell>;
loadDirtyRangesAndExcludedCell(dirtyRanges: IUnitRange[], excludedCell?: IUnitExcludedCell): void;
@@ -142,6 +147,8 @@ export class FormulaCurrentConfigService extends Disposable implements IFormulaC
private _sheetNameMap: IUnitSheetNameMap = {};
private _unitNameMap: IFormulaUnitNameMap = {};
private _forceCalculate: boolean = false;
private _clearDependencyTreeCache: IDirtyUnitSheetNameMap = {};
@@ -182,6 +189,7 @@ export class FormulaCurrentConfigService extends Disposable implements IFormulaC
this._arrayFormulaRange = {};
this._formulaData = {};
this._sheetNameMap = {};
this._unitNameMap = {};
this._clearDependencyTreeCache = {};
this._dirtyRanges = [];
this._dirtyNameMap = {};
@@ -237,6 +245,10 @@ export class FormulaCurrentConfigService extends Disposable implements IFormulaC
return this._sheetNameMap;
}
getUnitNameMap() {
return this._unitNameMap;
}
isForceCalculate() {
return this._forceCalculate;
}
@@ -326,14 +338,17 @@ export class FormulaCurrentConfigService extends Disposable implements IFormulaC
this._unitData = config.allUnitData;
this._unitStylesData = config.unitStylesData;
this._sheetNameMap = config.unitSheetNameMap;
this._unitNameMap = config.unitNameMap || {};
} else {
const { allUnitData, unitSheetNameMap, unitStylesData } = this._loadSheetData();
const { allUnitData, unitNameMap, unitSheetNameMap, unitStylesData } = this._loadSheetData();
this._unitData = allUnitData;
this._unitStylesData = unitStylesData;
this._sheetNameMap = unitSheetNameMap;
this._unitNameMap = unitNameMap;
}
// apply row data, including rows hidden by filters
@@ -369,7 +384,7 @@ export class FormulaCurrentConfigService extends Disposable implements IFormulaC
}
loadDataLite(rowData?: IUnitRowData) {
const { allUnitData, unitSheetNameMap, unitStylesData } = this._loadSheetData();
const { allUnitData, unitNameMap, unitSheetNameMap, unitStylesData } = this._loadSheetData();
this._unitData = allUnitData;
@@ -377,6 +392,8 @@ export class FormulaCurrentConfigService extends Disposable implements IFormulaC
this._sheetNameMap = unitSheetNameMap;
this._unitNameMap = unitNameMap;
this._formulaData = this._formulaDataModel.getFormulaData();
this._arrayFormulaCellData = convertUnitDataToRuntime(this._formulaDataModel.getArrayFormulaCellData());
this._arrayFormulaRange = this._formulaDataModel.getArrayFormulaRange();
@@ -421,6 +438,10 @@ export class FormulaCurrentConfigService extends Disposable implements IFormulaC
this._sheetNameMap = sheetNameMap;
}
registerUnitNameMap(unitNameMap: IFormulaUnitNameMap) {
this._unitNameMap = unitNameMap;
}
// private _loadOtherFormulaData() {
// const unitAllDoc = this._univerInstanceService.getAllUniverDocsInstance();
@@ -0,0 +1,125 @@
/**
* 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 { FormulaUnitType } from '../basics/common';
import { createIdentifier, UniverInstanceType } from '@univerjs/core';
import { ErrorType } from '../basics/error-type';
import { IFormulaCurrentConfigService } from './current-data.service';
export type FormulaUnitReferenceKind = 'a1' | 'table';
export interface IFormulaUnitReferenceResolveInput {
hostUnitId: string;
qualifier: string;
referenceKind: FormulaUnitReferenceKind;
}
export interface IFormulaUnitReferenceResolution {
unitId: string;
unitType?: FormulaUnitType;
}
export interface IFormulaUnitReferenceResolver {
resolve(input: IFormulaUnitReferenceResolveInput): IFormulaUnitReferenceResolution | ErrorType;
}
export const IFormulaUnitReferenceResolver = createIdentifier<IFormulaUnitReferenceResolver>(
'univer.formula.unit-reference-resolver'
);
const EXCEL_WORKBOOK_EXTENSION = /\.(?:xlsx|xlsm|xlsb|xltx|xltm|xls)$/i;
export function normalizeFormulaUnitName(name: string): string {
return name.replace(EXCEL_WORKBOOK_EXTENSION, '').toLowerCase();
}
export class FormulaUnitReferenceResolver implements IFormulaUnitReferenceResolver {
constructor(
@IFormulaCurrentConfigService
protected readonly _currentConfigService: IFormulaCurrentConfigService
) {}
resolve({
hostUnitId,
qualifier,
referenceKind,
}: IFormulaUnitReferenceResolveInput): IFormulaUnitReferenceResolution | ErrorType {
const unitNameMap = this._currentConfigService.getUnitNameMap();
const unitData = this._currentConfigService.getUnitData();
const address = qualifier || hostUnitId;
const direct = unitNameMap[address];
if (direct || unitData[address]) {
return this._validateReferenceKind(
hostUnitId,
referenceKind,
{
unitId: address,
unitType: direct?.unitType,
},
unitNameMap
);
}
if (!qualifier) {
return ErrorType.REF;
}
const namedUnits = Object.entries(unitNameMap).filter(([, item]) => item.name.length > 0);
const normalizedQualifier = qualifier.toLowerCase();
const exactMatches = namedUnits.filter(([, item]) => item.name.toLowerCase() === normalizedQualifier);
const matches =
exactMatches.length > 0
? exactMatches
: namedUnits.filter(
([, item]) => normalizeFormulaUnitName(item.name) === normalizeFormulaUnitName(qualifier)
);
if (matches.length !== 1) {
return ErrorType.REF;
}
const [unitId, item] = matches[0];
return this._validateReferenceKind(
hostUnitId,
referenceKind,
{
unitId,
unitType: item.unitType,
},
unitNameMap
);
}
private _validateReferenceKind(
hostUnitId: string,
referenceKind: FormulaUnitReferenceKind,
resolution: IFormulaUnitReferenceResolution,
unitNameMap: ReturnType<IFormulaCurrentConfigService['getUnitNameMap']>
): IFormulaUnitReferenceResolution | ErrorType {
if (referenceKind !== 'a1' || resolution.unitId === hostUnitId) {
return resolution;
}
const hostType = unitNameMap[hostUnitId]?.unitType;
if (resolution.unitType === UniverInstanceType.UNIVER_BASE) {
return ErrorType.REF;
}
if (hostType === UniverInstanceType.UNIVER_BASE && resolution.unitType !== UniverInstanceType.UNIVER_SHEET) {
return ErrorType.REF;
}
return resolution;
}
}
@@ -0,0 +1,43 @@
/**
* 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 { ObjectMatrix } from '@univerjs/core';
import { describe, expect, it } from 'vitest';
import { collectUnitQualifierFormulaPatches } from '../unit-qualifier-rename.controller';
describe('UnitQualifierRenameController', () => {
it('builds persisted Sheet cell patches for a renamed Base Unit', () => {
const workbook = {
getUnitId: () => 'host',
getSheets: () => [{
getSheetId: () => 'sheet',
getCellMatrix: () => new ObjectMatrix({
0: { 0: { f: '=SUM(BaseData!Tasks[Amount])' } },
1: { 0: { f: '=INDIRECT("[BaseData]Data!A1")&"BaseData!Tasks[Amount]"' } },
}),
}],
} as never;
expect(collectUnitQualifierFormulaPatches(workbook, 'BaseData', 'FY Base')).toEqual([{
unitId: 'host',
subUnitId: 'sheet',
cellValue: {
0: { 0: { f: "=SUM('FY Base'!Tasks[Amount])" } },
1: { 0: { f: '=INDIRECT("[FY Base]Data!A1")&"BaseData!Tasks[Amount]"' } },
},
}]);
});
});
@@ -0,0 +1,66 @@
/**
* 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 { IFormulaData } from '@univerjs/engine-formula';
import { describe, expect, it } from 'vitest';
import { UpdateFormulaController } from '../update-formula.controller';
import { FormulaReferenceMoveType } from '../utils/ref-range-formula';
describe('UpdateFormulaController Unit rename', () => {
it('refactors qualifiers across host workbooks and leaves ordinary strings intact', () => {
const controller = Object.create(UpdateFormulaController.prototype) as {
_getFormulaReferenceMoveInfo: (
data: IFormulaData,
sheetNames: Record<string, Record<string, string>>,
move: object
) => { newFormulaData: IFormulaData };
};
const formulaData = {
host: {
sheet: {
0: { 0: { f: '=SUM([Sales.xlsx]Data!A1)+SUM(Sales.xlsx!T[V])' } },
1: { 0: { f: '=INDIRECT("[Sales.xlsx]Data!B2")&"[Sales.xlsx]Data!B2"' } },
},
},
} as IFormulaData;
const { newFormulaData } = controller._getFormulaReferenceMoveInfo(formulaData, {}, {
type: FormulaReferenceMoveType.SetUnitName,
unitId: 'sales-unit',
sheetId: '',
oldUnitName: 'Sales.xlsx',
unitName: 'FY 2027.xlsx',
});
expect(newFormulaData.host?.sheet?.[0]?.[0]?.f).toBe("=SUM([FY 2027.xlsx]Data!A1)+SUM('FY 2027.xlsx'!T[V])");
expect(newFormulaData.host?.sheet?.[1]?.[0]?.f).toBe('=INDIRECT("[FY 2027.xlsx]Data!B2")&"[Sales.xlsx]Data!B2"');
});
});
/**
* 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.
*/
@@ -0,0 +1,119 @@
/**
* 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 { ICellData, IMutationInfo, UnitModel, Workbook } from '@univerjs/core';
import { Disposable, ICommandService, IUndoRedoService, IUniverInstanceService, ObjectMatrix, sequenceExecute, UniverInstanceType } from '@univerjs/core';
import { IDefinedNamesService, refactorFormulaUnitQualifier, SetDefinedNameMutation } from '@univerjs/engine-formula';
import { SetRangeValuesMutation } from '@univerjs/sheets';
export interface IUnitQualifierFormulaPatch {
unitId: string;
subUnitId: string;
cellValue: Record<number, Record<number, ICellData>>;
}
export function collectUnitQualifierFormulaPatches(
workbook: Workbook,
oldName: string,
newName: string
): IUnitQualifierFormulaPatch[] {
const unitId = workbook.getUnitId();
return workbook.getSheets().flatMap((sheet) => {
const updates = new ObjectMatrix<ICellData>();
sheet.getCellMatrix().forValue((row, column, cell) => {
if (!cell?.f) return;
const formula = refactorFormulaUnitQualifier(cell.f, oldName, newName);
if (formula !== cell.f) updates.setValue(row, column, { f: formula });
});
const cellValue = updates.getData();
return Object.keys(cellValue).length > 0
? [{ unitId, subUnitId: sheet.getSheetId(), cellValue }]
: [];
});
}
/** Keeps persisted Sheet formulas and defined names aligned when a Base Unit is renamed. */
export class UnitQualifierRenameController extends Disposable {
private readonly _names = new Map<string, string>();
constructor(
@ICommandService private readonly _commandService: ICommandService,
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService,
@IUniverInstanceService private readonly _univerInstanceService: IUniverInstanceService,
@IDefinedNamesService private readonly _definedNamesService: IDefinedNamesService
) {
super();
this._univerInstanceService.getAllUnitsForType<UnitModel>(UniverInstanceType.UNIVER_BASE)
.forEach((unit) => this._watch(unit));
this.disposeWithMe(this._univerInstanceService.getTypeOfUnitAdded$<UnitModel>(UniverInstanceType.UNIVER_BASE)
.subscribe(({ unit }) => this._watch(unit)));
this.disposeWithMe(this._univerInstanceService.getTypeOfUnitDisposed$<UnitModel>(UniverInstanceType.UNIVER_BASE)
.subscribe((unit) => this._names.delete(unit.getUnitId())));
}
private _watch(unit: UnitModel): void {
const unitId = unit.getUnitId();
this.disposeWithMe(unit.name$.subscribe((name) => {
const oldName = this._names.get(unitId);
this._names.set(unitId, name);
if (!oldName || oldName === name) return;
this._refactor(unitId, oldName, name);
}));
}
private _refactor(renamedUnitId: string, oldName: string, newName: string): void {
const redos: IMutationInfo[] = [];
const undos: IMutationInfo[] = [];
for (const workbook of this._univerInstanceService.getAllUnitsForType<Workbook>(UniverInstanceType.UNIVER_SHEET)) {
for (const patch of collectUnitQualifierFormulaPatches(workbook, oldName, newName)) {
const sheet = workbook.getSheetBySheetId(patch.subUnitId);
if (!sheet) continue;
const undoCellValue = new ObjectMatrix<ICellData | null>();
new ObjectMatrix(patch.cellValue).forValue((row, column) => {
undoCellValue.setValue(row, column, sheet.getCellRaw(row, column) ?? null);
});
redos.push({ id: SetRangeValuesMutation.id, params: patch });
undos.unshift({
id: SetRangeValuesMutation.id,
params: { ...patch, cellValue: undoCellValue.getData() },
});
}
const definedNames = this._definedNamesService.getDefinedNameMap(workbook.getUnitId());
for (const item of Object.values(definedNames ?? {})) {
const formulaOrRefString = refactorFormulaUnitQualifier(item.formulaOrRefString, oldName, newName);
if (formulaOrRefString !== item.formulaOrRefString) {
redos.push({ id: SetDefinedNameMutation.id, params: {
unitId: workbook.getUnitId(),
...item,
formulaOrRefString,
} });
undos.unshift({ id: SetDefinedNameMutation.id, params: {
unitId: workbook.getUnitId(),
...item,
} });
}
}
}
if (!redos.length || !sequenceExecute(redos, this._commandService).result) {
return;
}
this._undoRedoService.pushUndoRedo({
unitID: renamedUnitId,
undoMutations: undos,
redoMutations: redos,
});
}
}
@@ -24,7 +24,7 @@ import {
IUniverInstanceService,
UniverInstanceType,
} from '@univerjs/core';
import { deserializeRangeWithSheetWithCache, ErrorType, generateStringWithSequence, IDefinedNamesService, LexerTreeBuilder, sequenceNodeType, serializeRangeToRefString, SetDefinedNameMutation } from '@univerjs/engine-formula';
import { deserializeRangeWithSheetWithCache, ErrorType, generateStringWithSequence, IDefinedNamesService, LexerTreeBuilder, refactorFormulaUnitQualifier, sequenceNodeType, serializeRangeToRefString, SetDefinedNameMutation } from '@univerjs/engine-formula';
import { RemoveDefinedNameCommand, SetDefinedNameCommand, SheetInterceptorService } from '@univerjs/sheets';
import { FormulaReferenceMoveType, updateRefOffset } from './utils/ref-range-formula';
import { getNewRangeByMoveParam } from './utils/ref-range-move';
@@ -78,6 +78,12 @@ export class UpdateDefinedNameController extends Disposable {
};
}
if (result.type === FormulaReferenceMoveType.SetUnitName) {
result.oldUnitName = this._univerInstanceService
.getUnit<Workbook>(result.unitId, UniverInstanceType.UNIVER_SHEET)
?.getName();
}
return this._getUpdateDefinedNameMutations(workbook, result);
},
})
@@ -102,6 +108,15 @@ export class UpdateDefinedNameController extends Disposable {
// eslint-disable-next-line max-lines-per-function
Object.values(definedNames).forEach((item) => {
const { formulaOrRefString } = item;
if (type === FormulaReferenceMoveType.SetUnitName) {
const { oldUnitName, unitName } = moveParams;
if (!oldUnitName || !unitName) return true;
const nextFormula = refactorFormulaUnitQualifier(formulaOrRefString, oldUnitName, unitName);
if (nextFormula === formulaOrRefString) return true;
redoMutations.push({ id: SetDefinedNameMutation.id, params: { unitId, ...item, formulaOrRefString: nextFormula } });
undoMutations.push({ id: SetDefinedNameMutation.id, params: { unitId, ...item } });
return true;
}
const sequenceNodes = this._lexerTreeBuilder.sequenceNodesBuilder(formulaOrRefString);
if (sequenceNodes == null) {
return true;
@@ -44,7 +44,7 @@ import {
Tools,
UniverInstanceType,
} from '@univerjs/core';
import { deserializeRangeWithSheetWithCache, ErrorType, FormulaDataModel, generateStringWithSequence, IDefinedNamesService, initSheetFormulaData, LexerTreeBuilder, sequenceNodeType, serializeRangeToRefString, SetArrayFormulaDataMutation, SetFormulaDataMutation, SetTriggerFormulaCalculationStartMutation, splitTableStructuredRef } from '@univerjs/engine-formula';
import { deserializeRangeWithSheetWithCache, ErrorType, FormulaDataModel, generateStringWithSequence, IDefinedNamesService, initSheetFormulaData, LexerTreeBuilder, refactorFormulaUnitQualifier, sequenceNodeType, serializeRangeToRefString, SetArrayFormulaDataMutation, SetFormulaDataMutation, SetTriggerFormulaCalculationStartMutation, splitTableStructuredRef } from '@univerjs/engine-formula';
import {
ClearSelectionFormatCommand,
InsertSheetMutation,
@@ -320,7 +320,10 @@ export class UpdateFormulaController extends Disposable {
const result = getReferenceMoveParams(workbook, command);
if (result) {
const { unitSheetNameMap } = this._formulaDataModel.getCalculateData();
const { unitNameMap, unitSheetNameMap } = this._formulaDataModel.getCalculateData();
if (result.type === FormulaReferenceMoveType.SetUnitName) {
result.oldUnitName = unitNameMap?.[result.unitId]?.name;
}
const oldFormulaData = this._formulaDataModel.getFormulaData();
// change formula reference
@@ -390,6 +393,14 @@ export class UpdateFormulaController extends Disposable {
const { f: formulaString, x, y, si } = formulaDataItem;
if (type === FormulaReferenceMoveType.SetUnitName) {
const { oldUnitName, unitName } = formulaReferenceMoveParam;
if (!oldUnitName || !unitName) return true;
const nextFormula = refactorFormulaUnitQualifier(formulaString, oldUnitName, unitName);
if (nextFormula !== formulaString) newFormulaDataItem.setValue(row, column, { f: nextFormula });
return true;
}
const sequenceNodes = this._lexerTreeBuilder.sequenceNodesBuilder(formulaString);
if (sequenceNodes == null) {
@@ -35,6 +35,7 @@ export enum FormulaReferenceMoveType {
InsertMoveDown, // range
InsertMoveRight, // range
SetName,
SetUnitName,
RemoveSheet,
SetDefinedName, // update defined name
RemoveDefinedName, // remove defined name
@@ -54,6 +55,8 @@ export interface IFormulaReferenceMoveParam {
from?: IRange;
to?: IRange;
sheetName?: string;
oldUnitName?: string;
unitName?: string;
/**
* defined name id
*/
@@ -73,6 +76,7 @@ export interface IFormulaReferenceMoveParam {
const formulaReferenceSheetList = [
FormulaReferenceMoveType.SetName,
FormulaReferenceMoveType.SetUnitName,
FormulaReferenceMoveType.RemoveSheet,
FormulaReferenceMoveType.SetDefinedName,
FormulaReferenceMoveType.RemoveDefinedName,
@@ -28,6 +28,7 @@ import type {
IMoveRowsCommandParams,
IRemoveRowColCommandParams,
IRemoveSheetCommandParams,
ISetWorkbookNameCommandParams,
ISetWorksheetNameCommandParams,
} from '@univerjs/sheets';
import type { IFormulaReferenceMoveParam } from './ref-range-formula';
@@ -47,6 +48,7 @@ import {
RemoveRowCommand,
RemoveSheetCommand,
SetDefinedNameCommand,
SetWorkbookNameCommand,
SetWorksheetNameCommand,
} from '@univerjs/sheets';
import { FormulaReferenceMoveType } from './ref-range-formula';
@@ -116,6 +118,9 @@ export function getReferenceMoveParams(workbook: Workbook, command: ICommandInfo
case SetWorksheetNameCommand.id:
result = handleRefSetWorksheetName(command as ICommandInfo<ISetWorksheetNameCommandParams>, workbook);
break;
case SetWorkbookNameCommand.id:
result = handleRefSetWorkbookName(command as ICommandInfo<ISetWorkbookNameCommandParams>);
break;
case RemoveSheetCommand.id:
result = handleRefRemoveWorksheet(command as ICommandInfo<IRemoveSheetCommandParams>, workbook);
break;
@@ -396,6 +401,17 @@ function handleRefSetWorksheetName(command: ICommandInfo<ISetWorksheetNameComman
};
}
function handleRefSetWorkbookName(command: ICommandInfo<ISetWorkbookNameCommandParams>): Nullable<IFormulaReferenceMoveParam> {
const { params } = command;
if (!params) return null;
return {
type: FormulaReferenceMoveType.SetUnitName,
unitId: params.unitId,
sheetId: '',
unitName: params.name,
};
}
function handleRefRemoveWorksheet(command: ICommandInfo<IRemoveSheetCommandParams>, workbook: Workbook) {
const { params } = command;
if (!params) return null;
+1
View File
@@ -23,6 +23,7 @@ export { FormulaAutoFillController } from './controllers/formula-auto-fill.contr
export { FormulaCalculationSessionController } from './controllers/formula-calculation-session.controller';
export { ImageFormulaCellInterceptorController } from './controllers/image-formula-cell-interceptor.controller';
export { TriggerCalculationController } from './controllers/trigger-calculation.controller';
export { collectUnitQualifierFormulaPatches, UnitQualifierRenameController } from './controllers/unit-qualifier-rename.controller';
export { UpdateDefinedNameController } from './controllers/update-defined-name.controller';
export { UpdateFormulaController } from './controllers/update-formula.controller';
export { UniverRemoteSheetsFormulaPlugin, UniverSheetsFormulaPlugin } from './plugin';
+3
View File
@@ -37,6 +37,7 @@ import { FormulaController } from './controllers/formula.controller';
import { ImageFormulaCellInterceptorController } from './controllers/image-formula-cell-interceptor.controller';
import { SuperTableController } from './controllers/super-table.controller';
import { TriggerCalculationController } from './controllers/trigger-calculation.controller';
import { UnitQualifierRenameController } from './controllers/unit-qualifier-rename.controller';
import { UpdateDefinedNameController } from './controllers/update-defined-name.controller';
import { UpdateFormulaController } from './controllers/update-formula.controller';
import { DescriptionService, IDescriptionService } from './services/description.service';
@@ -118,6 +119,7 @@ export class UniverSheetsFormulaPlugin extends Plugin {
[UpdateDefinedNameController],
[SuperTableController],
[FormulaAutoFillController],
[UnitQualifierRenameController],
];
// If the plugin do not execute formula, it should delegate a remote proxy.
@@ -141,6 +143,7 @@ export class UniverSheetsFormulaPlugin extends Plugin {
[UpdateFormulaController],
[UpdateDefinedNameController],
[FormulaAutoFillController],
[UnitQualifierRenameController],
]);
// There is no rendering in the nodejs environment, so initialize it here
@@ -0,0 +1,47 @@
/**
* 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 { ICommandService } from '@univerjs/core';
import { SetFormulaCalculationResultMutation } from '@univerjs/engine-formula';
import { describe, expect, it } from 'vitest';
import { SetRangeValuesMutation } from '../../commands/mutations/set-range-values.mutation';
import { CalculateResultApplyController } from '../calculate-result-apply.controller';
import { createFunctionTestBed } from './formula/create-function-test-bed';
describe('CalculateResultApplyController', () => {
it('skips non-sheet unit results without blocking later sheet results', async () => {
const testBed = createFunctionTestBed();
const commandService = testBed.get(ICommandService);
commandService.registerCommand(SetFormulaCalculationResultMutation);
commandService.registerCommand(SetRangeValuesMutation);
testBed.get(CalculateResultApplyController);
await commandService.executeCommand(SetFormulaCalculationResultMutation.id, {
unitData: {
'base-unit': {
'table-1': { 0: { 0: { v: 900 } } },
},
[testBed.unitId]: {
[testBed.sheetId]: { 0: { 0: { v: 2760 } } },
},
},
unitOtherData: {},
});
expect(testBed.sheet.getSheetBySheetId(testBed.sheetId)?.getCellMatrix().getValue(0, 0)?.v).toBe(2760);
testBed.univer.dispose();
});
});
@@ -45,6 +45,7 @@ import {
FormulaDataModel,
FormulaDependencyGenerator,
FormulaRuntimeService,
FormulaUnitReferenceResolver,
FunctionNodeFactory,
FunctionService,
GlobalComputingStatusService,
@@ -56,6 +57,7 @@ import {
IFormulaCurrentConfigService,
IFormulaDependencyGenerator,
IFormulaRuntimeService,
IFormulaUnitReferenceResolver,
IFunctionService,
IHyperlinkEngineFormulaService,
Interpreter,
@@ -199,6 +201,7 @@ export function createFunctionTestBed(workbookData?: IWorkbookData, dependencies
injector.add([IFormulaCurrentConfigService, { useClass: FormulaCurrentConfigService }]);
injector.add([IHyperlinkEngineFormulaService, { useClass: HyperlinkEngineFormulaService }]);
injector.add([IFormulaRuntimeService, { useClass: FormulaRuntimeService }]);
injector.add([IFormulaUnitReferenceResolver, { useClass: FormulaUnitReferenceResolver }]);
injector.add([IFunctionService, { useClass: FunctionService }]);
injector.add([IOtherFormulaManagerService, { useClass: OtherFormulaManagerService }]);
injector.add([IFeatureCalculationManagerService, { useClass: FeatureCalculationManagerService }]);
@@ -48,6 +48,10 @@ export class CalculateResultApplyController extends Disposable {
for (let i = 0; i < unitIds.length; i++) {
const unitId = unitIds[i];
const workbook = this._univerInstanceService.getUniverSheetInstance(unitId);
if (!workbook) {
continue;
}
const sheetData = unitData[unitId];
if (sheetData == null) {
@@ -64,6 +68,10 @@ export class CalculateResultApplyController extends Disposable {
continue;
}
if (!workbook.getSheetBySheetId(sheetId)) {
continue;
}
const cellValue = this._getMergedCellData(unitId, sheetId, cellData);
const setRangeValuesMutation = {