mirror of
https://github.com/dream-num/univer.git
synced 2026-09-19 02:18:42 +08:00
feat(engine-formula): support cross-workbook references (#7292)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+89
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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({});
|
||||
});
|
||||
});
|
||||
|
||||
+170
@@ -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;
|
||||
}
|
||||
}
|
||||
+43
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user